SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

如何防止 AI 代理泄露 API 密钥和机密

AI 代理持有 API 密钥时,一次工具调用就可能将其泄露。本文介绍凭据网关、限定作用域的短期令牌,以及为何拒绝规则无法阻止 shell 命令窃取机密。

让机密信息不进入 AI 代理意味着什么

AI 代理本质上是一个执行命令的普通 Linux 进程。该进程持有的所有环境变量都可被它运行的代码读取。因此,代理环境中的 API key 也可以被代理发送到它能够访问的任意主机。让机密信息不进入代理,意味着只向它提供句柄,而不是密钥:例如短期、限定作用域的令牌,或在网络边界由其他组件替换为真实值的占位符。

这不是模型变得恶意的故事。实际机制更简单。代理读取包含指令的网页、README 或 issue 评论,并执行这些指令,因为对语言模型而言,你编写的文本与它获取的文本没有区别。这就是提示注入。一旦发生提示注入,损害范围只取决于一个因素:该进程可以读取什么。如果你还没有设置隔离边界,请参阅在服务器上安全运行编码代理,其中介绍了本指南所依赖的隔离层级。

用通俗语言说明威胁模型

以 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 出站流量被阻止,数据仍会通过 DNS 离开。

仅靠审核无法解决问题。正确的做法是确保 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)"]
  }
}

这样可以防止代理在探索过程中误打开文件。这些规则不仅适用于文件工具,Claude Code 还会将其用于检查 Bash 中能够识别的文件命令,例如 cat 和 head,以及重定向的目标,例如 < .env。但这些规则无法阻止注入的指令在项目目录中运行 grep -r KEY .,也无法阻止短小的 Python 脚本自行打开 .env,因为权限文档说明,拒绝规则不适用于未明确指定文件名的文件读取命令,也不适用于自行打开文件的子进程。对于这些情况,文档建议使用 Claude Code 的沙箱,由操作系统层强制执行阻止规则。此类命令运行前是否需要征得确认,取决于会话的权限模式;而 自动模式现在是交互式 Claude Code 会话的内置起始模式,因此您未监控的服务器会在没有提示的情况下运行更多此类命令。同样的限制也适用于任何影响代理行为习惯、而非权限的机制:要求代理只执行能奏效的最小改动的技能可以避免一次运行随意访问本不应打开的文件,但这仍然只是模型可以被说服放弃的建议。应将配置视为护栏,将文件系统权限视为围墙。同样的区分也适用于容器内部:Docker Compose 中的 env 文件和机密介绍了这一问题在下一层中的对应情况。

为每个代理分配独立的非特权用户

如果代理以您的身份运行,它会继承您的 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 而失败。如果它输出密钥,说明代理用户可以访问该文件。按名称打开文件时,需要对文件路径上每一级目录拥有搜索(x)权限,而不是读取权限。因此,即使主目录的权限模式为 711,其他用户无法列出其中内容,只要密钥文件本身可读,仍然可以访问该密钥。chmod 700 ~ 会阻断这条路径,chmod 600 ~/.ssh/id_ed25519 会修复密钥权限。不要将代理用户加入 sudo,也不要为它配置范围超过实际所需单条命令的 NOPASSWD 规则。VPS 上的最小权限用户详细介绍了组和 sudoers 配置。运行多个会话后,请始终保持这种隔离,因为一个 Claude Code 会话可以直接向另一个会话发送文本,第一个会话持有的任何内容都可能通过该通道在一条消息中传出。

在云 VPS 上,还应增加一道边界。实例元数据服务在固定的链路本地地址上提供响应,并且通常会向任何发起请求的对象提供角色凭据。端点不会检查请求来自哪个本地用户:在 EC2 上,AWS 文档说明 IMDSv1 请求不需要令牌;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 链末尾规则会接受所有新的 TCP 连接,因此追加的规则永远不会被处理。iptables 规则也会在重启后消失,除非将其持久化。如果已启用元数据服务的 IPv6 端点,请使用 [fd00:ec2::254],并通过 ip6tables 添加相同的规则。

从代理侧进行检查。sudo -u agent-review curl -s --max-time 3 http://169.254.169.254/ 应无输出并以非零状态退出,因为数据包在离开主机前就已被拒绝。如果它输出目录列表,说明该规则未被处理。

在边界注入凭据

真正有效的模式是注入凭据。agent 从不持有真实密钥。它通过本地网关发送请求,网关在请求发出时将占位符替换为真实密钥。密钥存储在网关的存储中,由不同的进程管理,并归不同的用户所有。

OneCLI 是这一模式的一个开源实现。除 ee/ 目录外,它采用 Apache-2.0 许可证;这些目录使用单独的企业许可证,生产环境使用需要订阅。它以容器形式运行在 agent 旁边。截至 October 2026,项目文档提供了以下 Docker Compose 配置。该配置从 compose 文件旁的 .env 文件中读取 3 个生成的密钥:

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 会导致其中存储的所有密钥失去关联。仪表板监听端口 10254,网关监听端口 10255;默认情况下,两者都绑定到 127.0.0.1。你只需存储一次真实凭据,然后为每个 agent 提供一个占位值,用它替代密钥,并提供该 agent 自己的受限访问令牌。agent 会将令牌放在 Proxy-Authorization 标头中发送。网关根据主机和路径匹配出站请求,解密对应的凭据并替换占位值。agent 的环境中不保存任何值得窃取的内容。

这里的价值不在于加密本身,而在于“这个 agent 使用了什么,以及何时使用”变成了一个日志查询。你只需读取一条审计记录,而不必猜测 6 个环境中的哪个环境保存了密钥副本。

将密钥交给进程,而不是交给环境

如果您在 systemd 下运行代理,则完全不需要环境变量。LoadCredential= 会将密钥放在只有服务用户可以读取的私有目录中;在 unit 文件中,该目录显示为 %d,在进程内部显示为 $CREDENTIALS_DIRECTORY。此限制按用户生效,而不是按服务生效。因此,应为每个代理服务分配独立用户:任何以相同用户身份运行的其他进程都可以打开该目录,除非该 unit 同时使用文件系统命名空间,例如 PrivateMounts=yes,将该目录对其他服务隐藏。该值不会出现在 /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。这证明加密文件可以在此主机上解密。然后在 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-worker

代理代码需要该值时,会打开 $AGENT_KEY_FILE。读取文件只发生在需要时。环境变量会在整个进程生命周期内存在,并会传递给该进程创建的每个子进程。

优先使用短期令牌,而不是长期密钥

永不过期的密钥在数月后出现在日志或会话记录中时,仍然有效。服务提供会话令牌时,应使用会话令牌,并将有效期设置为工作允许的最短时间。

aws sts assume-role \
  --role-arn arn:aws:iam::123456789012:role/agent-readonly \
  --role-session-name agent-review \
  --duration-seconds 900

AWS 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,但这只能说明目录无法列出;前文的 cat 检查才证明目录中的文件无法读取。第三项会显示 agent 的网络路径对外呈现的身份,这正是网关模式要解决的问题:如果返回 401,表示请求到达 GitHub 时没有携带 GitHub 接受的凭据,可能完全没有凭据,也可能凭据无效或已过期;如果返回 200,表示请求携带了有效凭据,因此您应确认使用的是哪个令牌。如果您以无人值守方式运行 agent,控制 VPS 上的 AI agent 成本介绍了与这些访问限制配套的预算限制。

FAQ

我可以直接相信模型不会泄露我的密钥吗?

不可以,因为在这个威胁模型中,模型不是攻击者。代理会读取网页、代码仓库和 issue 跟踪器中的文本,而这些文本可能包含指令。模型没有可靠的方法区分您的指令和它获取的文本。任何依赖模型做出正确选择的控制措施,都会在某次注入指令具有足够说服力时失效。因此,控制措施必须放在操作系统或网络层,而不能依赖模型。

环境变量真的不适合存放代理密钥吗?

它们有一个具体问题:会被继承。代理启动的每个子进程都会获得一份副本,包括构建脚本、测试运行器和任何软件包安装钩子。同一用户还可以通过 /proc/<pid>/environ 读取这些变量,因此代理运行的任何程序都能读取它们,而无需代理主动传递。通过 LoadCredential= 或网关在使用时读取文件,可以将暴露范围限制在使用时刻。

将密钥放入保管库后,问题就能自动解决吗?

只能解决一部分。保管库解决的是存储问题。如果您自行托管该保管库,还需要单独进行加固,因为 Vaultwarden 服务器通常是通过其管理令牌或备份文件被攻破的,而不是通过其中保存的加密条目被攻破。它无法解决最后一步:某个程序从保管库取出密钥,再将其作为环境变量交给代理。这会让您回到原点。关键在于由谁执行替换。如果代理获取密钥,代理就持有密钥。如果网关或 init 系统在代理进程之外执行替换,代理就不会持有密钥。

如何判断代理是否已经泄露了某些内容?

通常事后无法确定,这正是使用网关的理由。没有网关时,证据会分散在 shell 历史记录、代理的对话记录,以及您很可能没有保留的出站连接日志中。使用凭据网关后,每次使用凭据都会记录为一行,其中包含代理身份和时间戳。如果怀疑发生泄露,应先轮换密钥,再进行调查。轮换成本低,而确定性并不容易获得。

我今天至少应该做什么?

将所有 .env 文件移出代理工作目录,并为每个代理创建一个无特权用户。这两项更改大约需要十分钟,可以阻断最常见的路径:代理读取本不该与代码放在一起的凭据文件。网关和短期令牌是下一步措施,不是第一步。相同的起点适用于任何代理运行时,包括 在 VPS 上安全运行自主代理。

这篇文章对您有帮助吗?