如何防止AI代理泄露API密钥
AI代理可从环境变量读取API key,并通过HTTP、Git或DNS外传。本文介绍凭据网关、短期限权令牌和文件系统隔离,避免代理接触真实密钥。
不让 AI 代理接触机密意味着什么
AI 代理是一个运行命令的普通 Linux 进程。该进程持有的每个环境变量都可被它运行的代码读取。因此,代理环境中的 API key 可能被代理发送到它能够访问的任何主机。不让代理接触机密,意味着向它提供句柄而不是密钥:短期、受限范围的令牌,或者由其他组件在网络边界将其替换为实际值的占位符。
这不是模型变得恶意的故事。实际机制更简单。代理读取包含指令的网页、README 或问题评论,并执行这些指令,因为对语言模型而言,你编写的文本与它获取的文本没有区别。这就是提示注入。一旦发生提示注入,损害范围完全取决于一件事:该进程能够读取什么。如果你尚未设置边界,请参阅在服务器上安全运行编码代理,了解本指南所依赖的隔离层级。
用通俗语言说明威胁模型
以 agent 运行所使用的用户身份执行此操作。
tr '\0' '\n' < /proc/self/environ | grep -iE 'key|token|secret|password'它输出的每一行,都可能让陌生服务器发起一次 HTTP 请求。现在查看 agent 附近磁盘上的内容。
grep -rIl --exclude-dir=.git -e 'API_KEY' -e 'SECRET' ~/projects
find ~ -maxdepth 3 -name '.env' -o -name 'credentials' -o -name '*.pem'具有 shell 访问权限的 agent 不需要复杂的漏洞利用,就能将这些数据传出。以下4条常见路径即可完成,而且在日志中都像正常操作:
- 向任意主机发起出站
curl或fetch请求,并将值放入查询字符串。 - 对 agent 具有写入权限的仓库执行
git commit和git push。 - 安装软件包脚本。该脚本会以 agent 用户身份运行任意代码。
- 查询包含该值的主机名对应的 DNS。即使 HTTP 出站流量被阻止,该查询仍会泄露数据。
无法仅靠审核来解决此问题。正确的做法是确保 agent 无法访问任何有价值的数据。
工作树中的机密也会进入上下文窗口
代理会读取文件。工作目录中的 .env 文件会被读取。读取后,它就会进入上下文窗口,也就是说,它会出现在记录中、出现在您保留的任何日志中,也会出现在代理接下来写入的内容中。
之前,密钥位于代理工作的工作树中:
cd ~/projects/billing
cat .env
# STRIPE_SECRET_KEY=sk_live_...
# DATABASE_URL=postgres://app:hunter2@db.internal:5432/billing之后,将文件移到代理无法访问的位置:
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代理的用户将无法再打开该文件,因为工作树中已不再包含它。代理自身配置中的拒绝规则是第二层防护,而不是第一层防护。Claude Code 会从项目中的 .claude/settings.json 读取权限规则:
{
"permissions": {
"deny": ["Read(./.env)", "Read(./secrets/**)", "Read(./**/*.pem)"]
}
}这样可以防止代理在探索过程中误打开文件。但它无法阻止注入的指令运行 base64 .env,因为这是 shell 命令,而不是文件读取操作。应将配置视为安全护栏,将文件系统权限视为隔离边界。容器内部也适用同样的分层方式:Docker Compose 中的环境文件和机密介绍了这一问题在下一层中的对应情况。
为每个代理分配独立的非特权用户
如果代理以您的身份运行,它会继承您的 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 失败。如果它输出密钥,说明您的主目录对组用户或所有用户可读,使用 chmod 700 ~ 可以修复。不要将代理用户加入 sudo,也不要为其授予范围超过实际所需单条命令的 NOPASSWD 规则。VPS 上的最小权限用户详细介绍了组和 sudoers 配置。
在云 VPS 上,还应增加另一道边界。实例元数据服务会在固定的链路本地地址上响应,并且通常会向任何发出请求的对象提供角色凭据。
sudo iptables -A OUTPUT -m owner --uid-owner agent-review -d 169.254.169.254 -j REJECT从代理的环境中检查它。sudo -u agent-review curl -s --max-time 3 http://169.254.169.254/ 应不输出任何内容并以非零状态退出,因为数据包会在离开主机前被拒绝。
在边界注入凭据
真正能解决此问题的模式是凭据注入。代理不会持有真实密钥。它通过本地网关发送请求,网关在请求发出时将占位值替换为真实密钥。密钥存储在网关的存储中,由另一个进程管理,并由另一个用户拥有。
OneCLI 是此模式的一个开源实现,采用 Apache-2.0 许可证,并作为与代理并行运行的容器。截至 2026 年 7 月,该项目记录了以下配置:
git clone https://github.com/onecli/onecli.git
cd onecli
docker compose -f docker/docker-compose.yml up -d --wait仪表板监听端口 10254,网关监听端口 10255。您只需存储一次真实凭据,然后为每个代理提供一个占位值来代替密钥,并提供该代理自己的受限访问令牌。代理会在 Proxy-Authorization 标头中发送此令牌。网关根据主机和路径匹配出站请求,解密匹配的凭据,并将其替换进去。代理的环境中不会存储任何有窃取价值的内容。
这里的价值不在于加密,而在于“此代理使用了什么,以及何时使用”可以通过日志查询得到答案。您只需读取一份审计记录,而不必猜测六个环境中的哪个环境保存了密钥副本。
将机密传递给进程,而不是环境
如果您在 systemd 下运行代理,则完全不需要环境变量。LoadCredential= 会将机密放在只有该服务可以读取的私有目录中。在单元文件中,该目录显示为 %d;在进程内部显示为 $CREDENTIALS_DIRECTORY。该值不会出现在 /proc/<pid>/environ 中,因此 ps eww 无法显示它;服务停止时,该目录也会消失。
先将凭据加密到本机。这些命令来自 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。这证明加密文件可以在此主机上解密。然后在单元中引用该文件:
[Service]
User=agent-review
LoadCredentialEncrypted=api_key:/etc/credstore/api_key.cred
Environment=AGENT_KEY_FILE=%d/api_key
ExecStart=/usr/local/bin/agent-worker代理代码需要该值时,会打开 $AGENT_KEY_FILE 中的文件。读取文件只发生在需要时。环境变量会在进程的整个生命周期内存在,并且会存在于该进程生成的每个子进程中。
优先使用短期令牌,而不是长期有效的密钥
永不过期的密钥在数月后出现在日志或会话记录中时,仍然有效。如果服务提供会话令牌,请使用会话令牌,并将有效期设置为工作所允许的最短时间。
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/agent-readonly \
--role-session-name agent-review \
--duration-seconds 900AWS STS(安全令牌服务)接受的最短有效期是 15 分钟,通常足以完成一个代理任务。对于 GitHub,为代理用户创建独立的 gh 登录,并使用仅限单个仓库的细粒度令牌。这样,在该会话中执行 gh auth token 时,返回的内容无法访问其他资源。先按资源限制权限,再按时间限制有效期。
验证,然后持续验证
对 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第一项检查完全不应输出任何内容。第二项检查应输出 ls: cannot open directory '/home/you/': Permission denied。第三项检查会告诉您 agent 的网络路径使用哪个身份,这正是网关模式要解决的问题:401 表示 agent 未携带自己的 GitHub 凭据,200 表示它携带了一个凭据,因此您应确认具体使用的是哪个令牌。如果您以无人值守方式运行 agent,控制 VPS 上的 AI agent 成本介绍了与这些访问限制配套的预算限制。
FAQ
我可以直接相信模型不会泄露我的密钥吗?
不能,因为在这个威胁模型中,模型不是攻击者。代理会读取网页、代码仓库和问题跟踪器中的文本,而这些文本可能包含指令。模型无法可靠地区分您的指令和它获取的文本。任何依赖模型做出正确选择的控制措施,都会在注入的指令具有足够说服力时首次失效。因此,控制措施必须位于操作系统或网络层,而不能依赖模型。
环境变量真的不适合存储代理密钥吗?
它们有一个明确的问题:会被继承。代理启动的每个子进程都会获得一份副本,包括构建脚本、测试运行器和任何软件包安装钩子。同一用户还可以通过 /proc/<pid>/environ 读取这些变量,因此代理运行的任何程序都能读取它们,无需代理显式传递。使用 LoadCredential= 或网关在需要时读取文件,可以将暴露范围限制在使用时刻。
将密钥存入保管库能单独解决这个问题吗?
只能解决一部分。保管库可以解决存储问题,但不能解决最后一步:某个程序从保管库取出密钥,并将其作为环境变量交给代理。这会让您回到原点。关键在于由谁执行替换。如果代理获取密钥,代理就持有密钥。如果由网关或 init 系统在代理进程之外执行替换,代理就不会持有密钥。
如何知道代理是否已经泄露了某些内容?
通常事后无法确定,这正是使用网关的理由。没有网关时,证据分散在 shell 历史记录、代理的会话记录以及出站连接日志中,而您可能根本没有保留这些日志。使用凭据网关时,每次使用凭据都会记录为一行,其中包含代理身份和时间戳。如果怀疑发生泄露,请先轮换密钥,再进行调查。轮换成本很低,而确定性并不容易获得。
我今天至少应该做什么?
将每个 .env 文件移出代理工作目录,并为每个代理创建一个非特权用户。这两项更改大约需要十分钟,可以阻断最常见的路径:代理读取原本没有理由放在代码旁边的凭据文件。下一步再部署网关和短期令牌,不要一开始就做这些。相同的起点适用于任何代理运行时,包括在 VPS 上安全运行自主代理。