如何避免 AI 代理外洩 API key 與機密
AI 代理只需一次工具呼叫就可能外洩 API key。了解如何用權限範圍明確、短效的 token,透過 credential gateway 取代真實金鑰。
為何不應將機密資訊交給 AI 代理
AI 代理本質上是執行指令的 Linux 行程。該行程持有的所有環境變數皆可被其執行的程式碼讀取,因此若將 API key 置於代理的環境變數中,代理便能將該金鑰傳送至任何其可存取的遠端主機。所謂不將機密交給代理,是指改為提供「控制代碼」(handle)而非金鑰本身:例如使用短效且具備權限範圍的 token,或是使用預留位置(placeholder),再由其他機制於網路邊界處替換為真實數值。
這並非關於模型變壞的故事,其運作機制更為單純。代理會讀取網頁、README 或包含指令的 issue 留言,並執行其中的內容,因為對語言模型而言,使用者撰寫的文字與其抓取的文字並無區別。這就是提示詞注入(prompt injection)。一旦發生此類事件,損害範圍僅取決於一件事:該行程具備哪些讀取權限。若您尚未設定邊界,在伺服器上安全執行程式碼代理 一文涵蓋了本指南所依據的隔離層級。
威脅模型簡述
請以代理程式(agent)所屬的使用者身分執行此指令。
tr '\0' '\n' < /proc/self/environ | grep -iE 'key|token|secret|password'該指令輸出的每一行內容,都可能透過一次 HTTP 請求傳送到陌生人的伺服器。現在,請檢視代理程式周邊磁碟上的資料。
grep -rIl --exclude-dir=.git -e 'API_KEY' -e 'SECRET' ~/projects
find ~ -maxdepth 3 -name '.env' -o -name 'credentials' -o -name '*.pem'擁有 shell 權限的代理程式,無需複雜的漏洞利用即可將資料外洩。以下四種常見路徑皆可達成,且在日誌中看起來與正常作業無異:
- 對任意主機發送 outbound
curl或fetch請求,並將資料置於查詢字串(query string)中。 - 對代理程式具備寫入權限的儲存庫執行
git commit與git push。 - 執行套件安裝腳本,該腳本會以代理程式的使用者身分執行任意程式碼。
- 對包含資料內容的主機名稱進行 DNS 查詢;即使 HTTP 出站流量被阻擋,此方法仍可運作。
單靠審核無法解決此問題。根本之道在於確保代理程式可存取的範圍內,不包含任何具價值的敏感資料。
工作樹中的 secret 會進入 context window
Agent 會讀取檔案。工作中的 repository 內若有 .env 檔案,Agent 就會讀取;一旦讀取,該檔案內容便會進入 context window,也就是會出現在 transcript、你保留的任何 log,以及 Agent 接下來產生的內容中。
之前,key 放在 Agent 工作的 tree 中:
cd ~/projects/billing
cat .env
# STRIPE_SECRET_KEY=sk_live_...
# DATABASE_URL=postgres://app:hunter2@db.internal:5432/billing之後,將檔案移出 Agent 可存取的範圍:
sudo install -d -m 750 -o root -g agent-review /etc/agent-review
sudo install -m 640 -o root -g agent-review ~/projects/billing/.env /etc/agent-review/billing.env
rm ~/projects/billing/.env由於 working tree 已不再包含該檔案,Agent 的使用者便無法開啟它。Agent 自身設定中的 deny 規則只是第二層防護,不是第一層。Claude Code 會從專案中的 .claude/settings.json 讀取權限規則:
{
"permissions": {
"deny": ["Read(./.env)", "Read(./secrets/**)", "Read(./**/*.pem)"]
}
}這可避免 Agent 在探索時誤開檔案,也能涵蓋檔案工具以外的操作:Claude Code 也會針對它在 Bash 中辨識的檔案指令檢查這些規則,例如 cat 和 head,以及重新導向的目標,例如 < .env。但這無法阻止注入的指令在專案目錄中執行 grep -r KEY .,也無法阻止簡短的 Python script 自行開啟 .env,因為權限文件指出,deny 規則不適用於未明確指定檔案名稱的讀取指令,也不適用於自行開啟檔案的 subprocess。對於這些情況,文件建議使用 Claude Code 的 sandbox,由作業系統層級強制執行封鎖。這類指令執行前是否會要求確認,取決於 session 的權限模式;而 auto mode 現已是互動式 Claude Code session 的內建起始模式,因此對於未持續監看的 server,這類指令會有更多機會在未提示的情況下執行。相同限制也適用於會影響 Agent 習慣而非權限的機制:要求 Agent 僅進行能解決問題的最小變更的 skill,可避免一次執行偏離到不應開啟的檔案,但它仍只是模型可以被說服違反的建議。應將設定視為護欄,將 filesystem permission 視為牆。相同的區分也適用於 container 內部:Docker Compose 中的 env 檔案與 secret 說明了這個問題在下一層的版本。
讓每個 agent 使用獨立的非特權使用者
如果 agent 以你的身分執行,它就會繼承你的 SSH 金鑰、雲端憑證和 shell 歷史記錄。建立獨立使用者只需一個命令,便能移除這些存取權。
sudo adduser --disabled-password --gecos "" agent-review
sudo chmod 700 /home/agent-review
sudo -u agent-review cat ~/.ssh/id_ed25519最後一行必須以 cat: /home/you/.ssh/id_ed25519: Permission denied 失敗。如果它輸出金鑰,表示 agent 的使用者可以存取該檔案。依名稱開啟檔案時,檔案上層的每個目錄都需要 search(x)權限,而不是 read 權限。因此,即使 home 目錄的模式為 711,其他使用者無法列出其中內容,只要金鑰檔案本身可讀,仍然能夠存取該金鑰。chmod 700 ~ 會封鎖這條路徑,chmod 600 ~/.ssh/id_ed25519 則會修正金鑰權限。不要將 agent 使用者加入 sudo,也不要給它比實際需要的單一命令更寬鬆的 NOPASSWD 規則。VPS 上的最小權限使用者詳細說明群組與 sudoers 設定。當你在同一台主機上執行多個工作階段後,請記住這項隔離,因為一個 Claude Code 工作階段可以直接將文字傳送給另一個工作階段,而第一個工作階段持有的任何內容都能透過單一訊息跨越這條通道。
在雲端 VPS 上,還應增加一道隔離邊界。執行個體中繼資料服務會在固定的 link local 位址上回應,通常也會將角色憑證提供給任何提出要求的程序。端點不會檢查呼叫者使用哪個本機使用者:在 EC2 上,AWS 文件指出 IMDSv1 呼叫不需要 token;AWS 自己建議使用以使用者 ID 為條件的本機防火牆規則,讓特定使用者無法存取該服務。
sudo iptables -I OUTPUT 1 -p tcp -m owner --uid-owner agent-review -d 169.254.169.254 -j REJECT使用 -I 將規則插入頂端,不要使用 -A 將它附加在末端。在採用預設允許輸出流量政策的 ufw 主機上,ufw 的 OUTPUT chain 最後一條規則會接受所有新的 TCP 連線,因此附加的規則永遠不會被套用。iptables 規則也會在重新開機後消失,除非你將它們持久化;如果你已啟用中繼資料服務的 IPv6 endpoint,請使用 [fd00:ec2::254],並以 ip6tables 加入相同規則。
從 agent 端進行檢查。sudo -u agent-review curl -s --max-time 3 http://169.254.169.254/ 應不輸出任何內容並以非零狀態結束,因為封包會在離開主機前遭到拒絕。如果它輸出清單,表示該規則未被套用。
在邊界注入憑證
實際能解決這個問題的模式是憑證注入。代理程式不會持有真正的金鑰。它會將請求送往本機 gateway,再由 gateway 在送出請求時,將預留位置替換為真正的 secret。secret 儲存在 gateway 的儲存區中,由不同的程序及不同的使用者持有。
OneCLI 是這種作法的其中一個開放原始碼實作。除了其 ee/ 目錄採用個別的企業授權,並要求訂閱才能用於正式環境外,OneCLI 採用 Apache-2.0 授權;它會在代理程式旁的容器中執行。截至 2026 年 10 月,專案文件提供了這套 Docker Compose 設定。該設定會從 compose 檔案旁的 .env 檔案讀取 3 個產生的 secret:
git clone https://github.com/onecli/onecli.git && cd onecli/docker
cat > .env <<EOF
SECRET_ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
GATEWAY_INTERNAL_SECRET=$(head -c 32 /dev/urandom | base64)
BETTER_AUTH_SECRET=$(head -c 32 /dev/urandom | base64)
COMPOSE_PROFILES=runner
EOF
chmod 600 .env
docker compose up -d --wait請備份該 .env 檔案,因為遺失 SECRET_ENCRYPTION_KEY 會使其中儲存的每個 secret 都失去管理。dashboard 會監聽 10254 埠,gateway 會監聽 10255 埠;兩者預設都繫結至 127.0.0.1。您只需儲存一次真正的憑證,接著為每個代理程式提供預留位置值,取代原本的金鑰,並提供其專用的受限存取權杖。代理程式會將該權杖放在 Proxy-Authorization 標頭中送出。gateway 會依主機與路徑比對傳出的請求,解密相符的憑證並替換預留位置。代理程式的環境中不會保留任何值得竊取的內容。
這裡的重點不在加密,而在於「這個代理程式使用了什麼,以及何時使用」會變成可查詢的日誌。您只需讀取一份稽核軌跡,不必猜測 6 個環境中的哪一個保留了金鑰副本。
將 secret 提供給程序,而不是環境
如果您在 systemd 下執行 agent,完全不需要使用環境變數。LoadCredential= 會將 secret 放在只有服務使用者能讀取的私有目錄中;在 unit 檔案中會以 %d 提供,在程序內則以 $CREDENTIALS_DIRECTORY 提供。這項限制是依使用者而非服務套用,因此應為每個 agent 服務建立專用使用者:任何以相同使用者身分執行的其他程序,都能開啟該目錄,除非 unit 另外啟用檔案系統命名空間,例如 PrivateMounts=yes,將該目錄從其他服務隱藏。該值永遠不會出現在 /proc/<pid>/environ 中,因此 ps eww 無法顯示它;服務停止時,該目錄也會消失。
先將 credential 加密給這台機器。以下命令來自 systemd 文件,適用於 systemd 250 或更新版本,涵蓋 Ubuntu 24.04 與 Debian 13:
echo -n 'sk-example-value' > /tmp/plain.txt
sudo systemd-creds encrypt --name=api_key /tmp/plain.txt /etc/credstore/api_key.cred
shred -u /tmp/plain.txt
sudo systemd-run -P --wait -p LoadCredentialEncrypted=api_key:/etc/credstore/api_key.cred \
systemd-creds cat api_key最後一個命令會輸出 sk-example-value。這表示加密檔案可以在此主機上解密。接著從 unit 引用它:
[Service]
User=agent-review
LoadCredentialEncrypted=api_key:/etc/credstore/api_key.cred
Environment=AGENT_KEY_FILE=%d/api_key
ExecStart=/usr/local/bin/agent-workeragent 程式需要該值時,會開啟 $AGENT_KEY_FILE 中的檔案。讀取檔案只發生在當下;環境變數則會在程序存續期間一直存在,並存在於它建立的每個子程序中。
優先使用短效憑證而非長效金鑰
永不過期的金鑰一旦出現在日誌或紀錄中,即便數月後仍具備效力。若服務提供 session token,請優先採用並設定該工作允許的最短存活時間。
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/agent-readonly \
--role-session-name agent-review \
--duration-seconds 900AWS STS (security token service) 接受的最短時間為 15 分鐘,這通常足以完成單一代理程式任務。針對 GitHub,請為代理程式使用者建立專屬的 gh 登入帳號,並核發僅限於該單一儲存庫的細粒度權限 token,確保 gh auth token 在該 session 內的操作無法存取其他資源。權限控管應優先考量資源範圍,其次才是時間限制。
驗證後,持續驗證
每次變更 agent 的設定後,都應執行以下 3 項檢查。請以 agent 的使用者身分執行,不要使用自己的身分。
sudo -u agent-review env | grep -iE 'key|token|secret'
sudo -u agent-review ls -la /home/you/ 2>&1 | head -3
sudo -u agent-review curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 https://api.github.com/user第 1 項不應輸出任何內容。第 2 項應輸出 ls: cannot open directory '/home/you/': Permission denied,但這只能表示目錄無法列出;稍早的 cat 測試才是證明目錄內檔案無法讀取的依據。第 3 項會顯示 agent 的網路路徑所呈現的身分,這正是 gateway pattern 要解決的問題:401 表示請求抵達 GitHub 時,沒有攜帶 GitHub 接受的憑證,可能是完全沒有憑證,也可能是憑證無效或已過期;200 表示請求攜帶有效憑證,因此你應確認使用的是哪個 token。若 agent 會在無人看管的情況下執行,控制 VPS 上的 AI agent 成本說明了可搭配這些存取限制使用的預算上限。
FAQ
我可以直接信任模型不會洩漏我的金鑰嗎?
不行,因為在此威脅模型中,模型並非攻擊者。代理程式會讀取網頁、儲存庫與問題追蹤系統中的文字,而這些文字可能包含指令。模型無法可靠地分辨您的指令與其抓取的文字。任何依賴模型正確選擇的控制機制,一旦遇到具說服力的注入指令就會失效,因此控制機制必須位於作業系統或網路層級。
環境變數對於代理程式的機密資訊來說真的很糟糕嗎?
它們在一個特定方面很糟糕:它們會被繼承。代理程式產生的每個子處理程序都會獲得一份副本,包括建置指令碼、測試執行器以及任何套件安裝掛鉤。這些變數也可以透過 /proc/<pid>/environ 被同一使用者讀取,因此代理程式執行的任何程式都可以在代理程式未傳遞的情況下讀取它們。若在需要時才讀取檔案(使用 LoadCredential= 或閘道),可將暴露範圍限制在該瞬間。
將機密資訊放入保險庫(Vault)就能解決問題嗎?
僅能解決部分問題。保險庫解決了儲存問題。如果您自架該保險庫,則需要進行額外的強化,因為 Vaultwarden 伺服器通常是透過管理權杖或備份檔案被攻破的,而非透過其持有的加密項目。這並未解決最後一步,即某個程式從保險庫取出機密並以環境變數形式交給代理程式,這會讓您回到原點。關鍵在於誰執行了替換動作。如果由代理程式抓取機密,代理程式就持有該機密。如果由閘道或初始化系統在代理程式處理程序之外執行替換,代理程式就永遠不會持有它。
我該如何知道代理程式是否已經洩漏了資訊?
通常事後無法得知,這正是使用閘道的理由。若沒有閘道,您的證據會散落在 shell 歷史記錄、代理程式的對話紀錄以及您可能未保存的對外連線日誌中。有了憑證閘道,每次使用憑證都會產生一行包含代理程式身分與時間戳記的紀錄。如果您懷疑發生洩漏,請先輪替金鑰,再進行調查。輪替成本低廉,而確定性則不然。
我今天至少該做什麼?
將所有 .env 檔案移出代理程式工作的目錄,並為每個代理程式建立一個無特權的使用者。這兩項變更只需約十分鐘,即可封鎖最常見的路徑,即代理程式讀取了本不該放在程式碼旁邊的憑證檔案。閘道與短效權杖是下一步,而非第一步。相同的起點適用於任何代理程式執行環境,包括 在 VPS 上安全地執行自主代理程式。