# 面向 AI 智能体的秘密管理器：存储不等于操作控制

面向 AI 智能体的秘密管理器很有必要，但它并没有解决最危险的部分：当智能体请求凭据后会发生什么。如果管理器把明文返回给智能体进程，那么秘密已经越过了你原本想保护的边界。

只有当智能体遵循了恶意指令、调用了意料之外的工具、写入诊断文件，或联系了错误的主机时，这个区别才会显得格外重要。保险库可以让令牌加密保存多年，却可能在一次检索调用中让之前的大部分保护失效。对于智能体，安全得多的设计是把凭据留在可信的执行器中，让智能体请求执行经过身份验证的操作。

## 保险库保护的是存储，不是凭据的使用

传统秘密管理器回答的是一个存储问题：谁可以检索这个值？自主智能体带来了另一个使用问题：此时此刻，这个进程是否可以使用这个值，针对这个目标执行这项具体操作？

大多数团队都是从合理的习惯开始的。他们把令牌移出代码仓库，进行静态加密，在意外提交后轮换令牌，并把令牌注入构建环境。这些习惯很适合普通的应用部署。构建任务通常有固定脚本、有限的生命周期和明确的端点。它的环境可能仍然过于宽泛，但任务启动前，命令通常已经由人编写好了。

智能体不同。它会生成命令、选择工具、遵循工单文字、读取其他人写入的文件，还可能修改自己的计划。把 `DEPLOY_TOKEN` 放进环境变量，意味着它启动的每个子进程都能读取这个令牌。打印环境变量的 shell 命令、调试器、软件包安装钩子，或者智能体因为某份文档要求而选择的不可信工具，也可能读取它。

这种情况下，秘密管理器并没有失败。它只是完成了配置要求的工作：把秘密交给了一个已获授权的工作负载。问题在于，对于能够自行选择下一步操作的行为主体来说，这套授权模型过于粗糙。

请把下面两种权限分开：

- 通过稳定引用识别某个凭据的权限。
- 使用该凭据执行一项明确描述的出站操作的权限。

第一种权限可以安全地授予智能体。第二种权限需要与操作相匹配的限制。`POST https://deploy.example.internal/releases` 这样的请求，审核者可以评估。`read production token` 这样的请求则会交出一个资产，而在检索时无法评估它未来会被怎样使用。

这也能纠正一个常见但没有帮助的说法：“智能体已经拥有代码执行能力，所以隐藏秘密没有任何作用。”开发者机器上的代码执行已经够危险了。从这台机器复制出去的 Bearer 令牌还会带来第二个问题：它可以离开这台机器，在会话结束后继续存在，并从完全不同的计算机上使用。让秘密留在进程外并不会消除所有风险，但它能移除攻击者最看重的一种可携带、持久化访问权限。

RFC 6750 对 OAuth Bearer 令牌说明得很清楚：任何持有 Bearer 令牌的一方都可以使用它。协议并不关心持有者是获准的智能体、恶意插件，还是发现日志文件的人。把 Bearer 令牌当作普通工具输出，就忽略了它最根本的属性。

## 读取权限会把指令错误变成凭据泄露

拥有明文秘密访问权限的智能体，即使没有主动进行数据外传，也可能泄露凭据。危险路径在最后一步之前往往看起来很普通。

假设有人要求智能体诊断部署 API 为什么拒绝发布。智能体读取了仓库中的一份文档，文档要求收集支持包。支持包脚本运行 `env`，复制配置文件，然后把输出打包。智能体照做，创建压缩包，并把它附加到工单或上传到聊天系统。令牌从未显示在终端中，却已经经过了环境变量、压缩包、工单，以及与该工单相关的所有备份或通知系统。

这不只是提示注入。提示注入是引发错误操作的一条路径，但复制出的凭据在原始指令消失后仍会产生自己的影响范围。之后查看工单的人可能下载它，自动扫描器可能将其编入索引，收件人可能在智能体运行结束很久之后再次使用它。

同样的失败也可能以更隐蔽的方式出现：

- 详细模式的 HTTP 客户端在重试时打印 `Authorization` 标头。
- Shell 历史文件记录了通过命令行传入的令牌。
- 测试固件记录请求标头，随后被提交到代码仓库。
- 子进程继承了它根本不需要的环境变量。
- 由于秘密出现在工具输出中，模型在解释中包含了这个秘密。

常见的答案是脱敏。脱敏能在错误发生后提供帮助，但无法证明每条输出路径、每种压缩格式、每个跟踪系统、每个子进程和每项远程服务都正确处理了这个值。对于格式未知的秘密，以及拆分在多个字段中的令牌，脱敏也会失效。对于智能体本来就不应该读取的凭据，不要把脱敏当作主要防线。

更好的安排是缩小智能体的输入范围。智能体提交不包含凭据材料的意图，其中包括 HTTP 方法、允许访问的 URL、请求正文和凭据引用。可信组件检查请求，在内部添加身份验证材料，发送请求，然后删除敏感标头后返回响应。

这道边界能让事件响应更容易回答问题。如果智能体行为异常，可以撤销它的会话或拒绝后续操作。如果运行结束后才发现恶意指令，也不必先假设每份会话记录、临时文件和远程产物中都已经包含生产令牌。

## 操作执行与秘密检索是两种不同的接口

执行边界应该接收一项操作，而不是把一个不透明的值交给智能体，再希望它谨慎使用。这正是团队最容易混淆的区别。混淆之后，保险库就会变成凭据自动售货机。

对于 HTTP，智能体可能需要表达这样的请求：

```json
{
  "credential": "release-api",
  "method": "POST",
  "url": "https://deploy.example.internal/releases",
  "headers": {
    "Content-Type": "application/json"
  },
  "body": {
    "version": "2025.03.8",
    "environment": "staging"
  }
}
```

执行器在受保护的保险库中解析 `release-api`，注入匹配的身份验证方案，然后发出请求。智能体可能收到如下结果：

```json
{
  "status": 201,
  "headers": {
    "content-type": "application/json"
  },
  "body": {
    "release_id": "rel_4821",
    "state": "queued"
  }
}
```

响应不应包含被注入的 `Authorization` 标头、复制出的凭据值，或会暴露凭据的传输诊断信息。这听起来很明显，但当开发人员把请求和响应日志视为无害的基础设施时，边界设计就会失败。

对于 SSH，智能体应该提交主机、账户、命令，也可以提交凭据引用。执行器在 SSH 身份验证交换过程中使用私钥，并返回标准输出、标准错误和退出状态。智能体永远不会收到 PEM 内容，也不会收到可以在别处复用的代理套接字。

不要把这和通用代理混为一谈。代理会转发任意流量，并可能检查或修改流量。操作执行器的职责更窄：持有凭据，执行命名的 HTTP 和 SSH 操作，记录决策，并返回有边界的结果。这种狭窄本身就是优势。每增加一种协议或一个通用绕过机制，智能体就多了一种把权限变成无法审核的请求的方式。

请求接口仍然需要严格设计。仅有凭据引用还不够。如果 `release-api` 能向许多主机进行身份验证，或能接受任意 URL，智能体就可以把有效凭据发往攻击者控制的端点，或安全性较低的内部服务。应将凭据绑定到预期的身份验证方案和目标。拒绝意外的用户信息段、重定向到新主机，或仅仅看起来与获准主机相似的主机等 URL 欺骗手段。

执行器还需要决定返回哪些内容。完整的 API 响应可能包含用户数据、下游服务签发的访问令牌，或不应重新进入模型上下文的配置细节。只返回下一步操作所需的字段，通常既能提高安全性，也能让智能体更可靠。

## SSH 需要命令控制，而不只是保护私钥

隐藏 SSH 私钥有帮助，但并不能让任意远程命令变得安全。身份验证只告诉服务器谁连接了。Shell 启动后，身份验证并不会限制这个账户能做什么。

RFC 4252 将 SSH 公钥身份验证描述为对交换数据进行签名。私钥在交换过程中始终保持私密，这也是人们常认为 SSH 比把 API 令牌塞进环境变量更安全的原因。在一个狭窄的意义上，这确实如此：客户端证明自己持有私钥，而不是通过网络发送私钥。但能够请求 SSH 代理签名的进程，仍然可以访问信任该密钥的系统。

SSH 代理转发需要直截了当地看待。它允许远程服务器在连接期间使用本地身份验证代理。远程服务器不会收到私钥文件，但可以通过转发的套接字请求签名。只有当你信任远程机器，并且接受它在转发通道存在期间实际拥有以你的身份进行身份验证的能力时，这种做法才合适。对于能够自行选择连接目标和设置命令的自主智能体来说，这不是一道清晰的边界。

更安全的 SSH 操作接口应明确指定目标和命令，并同时记录两者，因为 `ssh deploy@host "./deploy staging"` 和 `ssh deploy@host "cat /etc/shadow"` 触发的是同一次身份验证，但后果完全不同。

远程端的限制仍然不可缺少。为不同职责使用不同账户。让部署账户只能访问部署目录，而不是拥有管理员权限。在服务器支持时，为自动化凭据配置强制命令或受限命令包装器。不要因为工作站上已经有一把共享的个人管理员密钥，就把它用于智能体任务。

一个实际的 SSH 请求可以简单到这样：

```json
{
  "credential": "staging-deployer",
  "host": "staging-runner.internal",
  "user": "deploy",
  "command": "./release apply 2025.03.8",
  "timeout_seconds": 120
}
```

这个请求给审核者提供了明确的审批对象，也给日后的审计提供了具体记录。如果智能体需要交互式 Shell，请先停下来问问原因。交互式 Shell 适合人类修复系统，但不应成为智能体的默认方式，因为它会把有边界的操作变成开放式会话，而之后的每条命令都会继承相同权限。

主机验证同样重要。执行器应使用已知主机验证，而不是因为模型被要求连接，就接受任意提供的主机密钥。如果智能体可以关闭主机验证，网络攻击者就可能接收命令、查看输出，甚至捕获智能体登录后发送的数据。

## 审批应同时确认进程和操作范围

如果审批按钮只告诉你“某个智能体”请求了访问权限，它就很脆弱。你需要知道是哪一个本地进程发起请求、什么签名权限启动了它，以及审批适用于本次运行还是未来每次运行。

每次调用都弹出提示看起来最安全，因此团队往往从这里开始。随后智能体会连续执行大量无害的读取、状态检查和后续调用。负责审批的人看到一大堆几乎相同的对话框，最后开始凭节奏点击批准。这种反应可以理解，却会破坏原本想实现的控制。

对于普通工作，把一次审批绑定到一个智能体进程的生命周期通常效果更好。审批者在查看身份后批准某一次运行。智能体可以在退出前完成预期流程。新的进程必须重新获得决定。这样可以限制重启或被替换的进程所产生的影响，即使它使用的是同一个项目目录。

对于使用后果不可逆或代价高昂的凭据，再保留逐次调用审批。生产发布凭据、删除账户的令牌，或能够访问敏感系统的 SSH 身份都属于这一类。只读状态检查通常不需要。

审批范围应当用简单语言回答四个问题：

1. 哪个代码签名机构或可执行文件发起了这次请求？
2. 它将使用哪个凭据引用？
3. 操作将发往哪个目标或主机？
4. 决定是否在进程退出时失效，还是每次调用都需要单独确认？

除非你有专门人员维护策略，并且有测试证明策略的实际效果，否则不要使用复杂的策略语言。策略引擎吸引工程师，因为它承诺能为每种情况给出精确答案。但实际上，一堆规则会变成无人维护的授权程序，团队遇到阻碍时就会添加宽泛例外。少量、清晰可见的控制更容易检查，也更难被意外削弱。

Sallyport 使用固定的决策梯：保险库锁定时拒绝所有操作；新的智能体进程默认需要会话授权；选定的凭据可以要求每次使用都审批。这个模型有意保持简单，因为智能体凭据边界应该让审批状态一目了然，而不是让管理员调试授权语法。

## 审计记录必须在运行结束后解决问题

你需要从两个角度查看智能体活动：一个用于整个运行，另一个用于每次使用凭据的调用。会话记录回答谁运行了智能体，以及它的权限何时结束。操作记录回答哪个目标、方法或命令、凭据引用、决策和结果实际发生了什么。

团队经常只记录终端会话，这不够。会话记录展示的是智能体打印了什么，不一定是执行器发送了什么。它可能遗漏后台调用，包含被修改的输出，或者在智能体可以访问秘密时泄露秘密。反过来，原始数据包或请求日志又可能记录过多敏感数据，难以安全保存。

记录决策点，而不是每一个字节。对于 HTTP，记录规范化的方法、主机、路径、凭据引用、响应状态、时间戳、会话身份和审批结果。如果需要关联请求，可以记录请求正文摘要或有限的概要，而不保留客户数据。对于 SSH，记录经过验证的主机、远程用户、命令、退出状态，以及相同的会话和审批字段。

一条记录应该能回答：“哪个获准进程使用部署凭据执行了什么操作，这次使用是否经过人工批准？”如果回答不了，它或许能帮助调试，但在事件发生后提供的帮助会非常有限。

防篡改能力会改变记录的价值。保存在同一台机器上的普通日志文件，可能被拥有足够本地权限的进程编辑。哈希链会把每个事件与前一个事件连接起来，因此删除或改写早期记录会破坏后续验证。加密保护内容，而链式结构能证明记录序列是否发生变化。

Sallyport 会把会话日志和操作日志写入加密、带哈希链的审计日志，`sp audit verify` 可以在没有保险库密钥的情况下，对密文离线检查哈希链。这种分离在调查期间很重要：审核者可以验证日志完整性，而无需获得保险库持有的凭据或请求内容。

哈希链不会凭空让已被入侵的端点变得可信。控制正在运行的应用程序的攻击者仍可能在你响应前发起操作，而在日志条目写入前就已取得控制权的攻击者，也可能影响记录内容。哈希链能为已保存的历史提供有力证据。若需要更完整的事件记录，还应配合及时撤销、受保护的本地存储和远程端日志。

## 失败始于智能体获得方便的绕过方式

最危险的设计通常始于一个看似合理的例外。有人说网关对某项集成限制太多，于是给智能体一个带有环境令牌的通用 Shell 命令。或者部署工具需要 SSH，团队便启用转发，而不是定义具体的 SSH 操作。又或者工程师添加了一个返回标头的调试端点，因为这样测试更方便。

每个例外都解决了一个局部问题，却又以另一种名义重新开放了秘密检索。

看看一个熟悉的设置。编程智能体的环境中有服务令牌，因此它能运行发布命令。智能体读取了一条拉取请求评论，评论要求调查一次失败的发布。仓库中的辅助脚本调用诊断命令。该命令把环境变量导出到支持压缩包中。由于评论要求创建工单，智能体把压缩包上传到第三方工单系统。

攻击者不需要提前知道令牌值。他只需要影响智能体视为指令的文字，再把智能体引向一个会暴露继承状态的工具路径。发现问题后撤销智能体会话，并不能收回压缩包。轮换令牌变成必需操作，团队还必须找出压缩包经过的每一个地方。

受控执行器会改变这个流程。辅助脚本仍然可以运行，智能体也仍然可以通过获准的 API 操作收集发布状态。但它无法从环境中读取令牌，因为令牌从未被放入环境。如果脚本尝试发起新的凭据调用，执行器会记录该调用，并应用会话级或逐次调用的决定。支持压缩包中可被窃取的内容也会减少。

不要因为智能体声称需要某个工具完成任务，就授予绕过权限。应询问工具真正需要的最小操作。如果它只需要调用一个 API 端点，就暴露这个操作。如果它只需要一条部署命令，就暴露这条命令。如果它确实需要广泛权限，就把请求视为广泛授权，并要求人工通过独立且有意为之的流程来处理。

## 围绕真正重要的操作建立边界

从丢失后会迫使团队紧急轮换，或允许执行重大生产变更的凭据开始。你不必第一天就迁移所有开发令牌。围绕最危险凭据建立一部分边界，也胜过启动一场最终无人完成的全面迁移。

首先盘点智能体目前如何获得权限。搜索智能体启动脚本、Shell 配置、CI 交接文件、工具配置、本地 `.env` 文件和命令包装器。标记每个向智能体提供令牌、密码、私钥、云会话或转发身份验证套接字的位置。这份盘点经常会发现，智能体通过继承的开发者设置获得了远超预期的权限。

然后用操作请求替代明文传递。按照用途定义凭据引用，不要按照人员或模糊的环境标签定义。`staging-deployer` 比 `shared-key-2` 表达得更清楚。当两种用途需要不同的审批要求或目标时，应创建不同的引用。

在自动化之前，为每个引用决定以下事项：

- 哪些 HTTP 主机、方法和路径，或哪些 SSH 主机、用户和命令符合它的用途？
- 新的智能体进程是否需要明确批准后才能使用它？
- 每次使用是否都需要确认，因为操作具有破坏性或难以逆转？
- 智能体需要获得哪些结果字段才能继续工作？
- 哪些记录能让你日后解释这次操作，同时不保留秘密材料？

和测试正常流程一样认真地测试拒绝路径。启动一个未经批准的智能体进程，确认它无法执行操作。尝试使用相似的主机名、重定向到其他主机、执行预期部署路径之外的命令，以及把凭据引用当作值来打印。确认被撤销的会话无法继续发起请求。

也要测试人工操作的一面。如果审批请求频繁到让你不再阅读，说明范围设计错了。如果对话框无法告诉你哪个进程发出了请求，说明进程身份太弱。如果智能体无法完成正常工作，却反复要求一个通用绕过机制，说明操作接口需要增加另一个经过仔细限制的操作，而不是倾倒秘密。

成功标准很简单：智能体能够完成获准的工作，但会话记录、子进程、插件或恶意指令无法把这项工作变成对可复用凭据的占有。在下一次紧急部署让方便的例外看起来无害之前，先朝这个标准建立边界。
