# 代理身份与凭据身份：别再隐藏真正的调用者

持有生产凭据的 AI 代理会同时制造两个身份问题。代理可以以账户身份执行操作，而团队之后只能假装账户名称说明了是谁执行的。事实并非如此。一个使用 `deploy-bot` 身份验证的 API 请求，只能说明某人持有 `deploy-bot` 的凭据。它无法说明调用者是经过批准的编程代理、复制出来的 shell 脚本、恶意依赖，还是测试结束后仍在运行的进程。

团队需要把请求操作的进程，与获准执行该操作的账户分开识别。这不是审计日志的装饰问题。它决定了你能否批准某次具体的代理运行，在不影响无关工作的情况下停止它，调查一次错误部署，并在凭据泄露时准确遏制风险，而不是靠猜测。

## 凭据标识的是权限，不是调用者

凭据说明服务会授予什么权限，代理身份则说明哪个进程请求了这些权限。把二者混在一起，会让之后的每个安全决策都过于粗糙。

假设有一个删除云存储对象的请求。接收请求的 API 可能看到一个 bearer token，并将它映射到名为 `release-publisher` 的服务账户。这个账户回答的是授权问题：该账户能否删除对象？它没有回答归因问题：哪个本地进程使用了令牌，谁启动了它，运行了什么代码，以及是否有人批准了这次运行？

在平稳时期，这些问题听起来相近，因为预期进程和账户往往由同一个人负责。压力一来，它们就会分开。开发者把令牌复制到本地环境，代码代理继承这个环境，代理启动的插件读取令牌。每个请求仍然显示为 `release-publisher`。授权层完全按照配置工作，调查却失去了真正的调用者。

凭据身份通常生命周期很长，用途也很广。它可能是 API token、SSH 私钥凭据、云服务账户或客户端证书主体。进程身份应该描述范围窄得多的对象：哪个可执行文件、由谁签名或来自哪个镜像、在什么上下文中启动、属于哪次运行。

这个区别带来直接的实际影响：

- 某次代理运行变得不可信时，撤销进程会话。
- 机密可能泄露时，禁用或轮换凭据。
- 权限本身过大时，修改账户权限。
- 需要了解实际发生了什么时，调查进程记录。

如果一个字段必须承担这四项工作，它就不会有一项做得好。把凭据命名为 `agent-prod-42` 并不能解决问题。更好的标签不会把 bearer token 变成 bearer 的证据。

## 一次代理请求中会出现四种身份

一次生产操作通常至少涉及四种身份。审计记录应保留它们，而不是把它们压扁成一个账户名称。

第一种是发起工作的人员或系统所有者。它可能是终端前的开发者、CI 任务触发器或定时任务。所有者信息回答谁要求代理运行，以及谁应该收到警报。

第二种是代理进程。对于本地编程代理，有用的证据可能包括可执行文件路径、签名机构、进程 ID、父进程、启动时间，以及启动时生成的随机会话标识符。在容器环境中，对应信息可能包括镜像摘要、工作负载身份文档、编排器工作负载名称，以及 pod 或任务实例。这种身份回答哪个运行中的程序请求执行操作。

第三种是凭据身份，也就是目标系统接受的服务账户、SSH 主体、OAuth 客户端或 API token 记录。它回答目标系统允许做什么。

第四种是目标身份，例如受请求影响的主机名、代码仓库、云项目、数据库或 API audience。团队经常只记录通用端点，之后才发现操作越过了本来不打算授予的环境边界。

清晰的记录可以写成这样：

```json
{
  "event": "http.request",
  "session_id": "ses_7zQ2...",
  "process": {
    "pid": 8421,
    "signing_authority": "Example Developer ID",
    "parent_pid": 8190,
    "launch": "interactive-terminal"
  },
  "credential_ref": "cred_release_publisher",
  "credential_subject": "release-publisher",
  "target": "api.example.internal",
  "operation": "POST /releases",
  "decision": "approved",
  "result": 201
}
```

`credential_ref` 指向保险库记录，而不是机密本身。进程区块提供了凭据主体无法提供的证据。审核人员现在可以看到，一次经过批准的会话使用 `release-publisher` 访问了一个目标，也能把这次运行与之后使用同一账户的另一个进程区分开。

人类所有者字段也不要承担过多含义。要求代理更新文档的人，不一定就是后来代理尝试进行生产部署时的批准者。要把发起者、进程、凭据和目标分开记录。令人不舒服的缺口会显现出来，这正是目的。

## Bearer token 会抹掉最需要的证据

Bearer token 之所以被接受，是因为调用者持有它，因此它无法证明究竟哪个进程提交了它。这是机制本身的属性，不是 API 提供商日志做得不够好。

RFC 6750 直接定义了 bearer token 的使用方式：任何持有令牌的一方都可以使用它。这让 bearer token 易于部署，但也意味着除非增加独立控制，否则 API 无法区分经过批准的代理和复制出来的进程。常见回应是“我们把令牌放进了机密管理器”。这解决的是静态存储问题，并没有解决进程收到令牌之后会发生什么。

OAuth 2.0 Security Best Current Practice，也就是 RFC 9700，在适用时建议使用发送者约束的访问令牌，同时采用较短的有效期和 audience 限制。发送者约束可以通过要求资源服务器验证预期客户端的证明来降低令牌重放风险。但它仍不能替代对控制该客户端证明材料的进程进行识别。如果代理进程可以使用私有证明材料，控制该进程的攻击者仍然可以冒充客户端行动。

这对 AI 代理尤其重要，因为它们的运行环境通常很容易穿透。它们会读取仓库、执行命令、调用工具、检查环境变量，有时还会根据文件或 issue 评论中的指令行动。即使团队原本只打算让某个工具调用使用令牌，令牌一旦暴露给代理，也不会因为意图如此就继续留在一个干净的信任边界内。

不要把“给每个代理一个专用长期令牌”当成解决方案。这个建议流行，是因为它能产生整齐的账户名称和简单的仪表板。但如果代理能读取令牌、其他进程能读取同一个环境，或账户在代理运行结束后仍然存在，它就会失效。专用凭据有助于计算影响范围，却不能证明进程来源。

你需要两项控制：阻止代理接触可重复使用的机密材料，并单独保留请求每个操作的代理进程记录。前者限制盗窃，后者让批准、撤销和调查足够具体，真正发挥作用。

## 进程身份需要经得住提示词的证据

进程身份应来自操作系统或执行环境可以报告的事实，而不是代理在工具请求中自行提供的字符串。代理声明 `name: trusted-release-agent` 只是提出了一个说法，并没有提供身份证据。

在受管理的 Mac 上，代码签名机构是一个有用的起点。操作系统可以告诉监管应用哪个已签名的可执行文件被启动，以及哪个机构对它进行了签名。这不代表每个已签名程序都值得批准，而是意味着批准界面可以显示一个比进程自行提供的标签更难伪造的事实。

在服务器上，要寻找同等证据，例如运行时签发的工作负载身份、经过验证的镜像摘要、双向认证通道，或进程监管器的证明。SPIFFE 通过 SPIFFE Verifiable Identity Document 和 X.509 SVID 记录了这种模型：工作负载从环境中获得身份，再将身份展示给另一个工作负载。由于身份限定在具体工作负载上，这种模型比共享环境变量更强。

不要把静态镜像摘要误认为完整的进程身份。它能识别代码内容，但一次有用的操作决策仍需要运行实例和启动上下文。同一个镜像可能在预期的生产任务、开发者的实验环境或攻击者控制的任务中执行。目标和会话仍然属于决策的一部分。

对于本地代理工作，在创建会话时收集证据，然后把后续操作绑定到该会话。最小会话记录可以包括：

```text
session_id: ses_7zQ2...
executable: /Applications/Agent.app/Contents/MacOS/agent
signing_authority: Example Developer ID
parent_process: /Applications/Terminal.app
started_at: 2025-03-08T14:03:19Z
owner: developer@example.invalid
```

具体字段会因操作系统而异，但原则不变：批准操作的组件应自行收集进程事实。不要在代理已经接收不可信输入之后，再相信代理会准确描述自己。

进程身份也有边界。代码签名不能证明已签名代理遵循了安全指令，镜像摘要也不能证明配置安全。身份能告诉你批准了哪个调用者，并提供撤销句柄，但它本身不能让危险的请求变得安全。

## 批准应绑定到一次运行，而不是账户名称

人工批准应授权一个特定的进程会话，并限定有效时间，而不是永久认可之后使用该账户的任何进程。否则，批准就会变成变相的长期访问授权。

批准疲劳通常源于批准单位选错。团队要么要求人员批准每个无害请求，导致人们习惯性点击通过，要么一次批准服务账户，从此再也看不到有意义的提示。两种方式都无法识别当前进程。

在代理开始运行时使用会话批准。展示操作人员能够评估的进程证据：可执行文件或工作负载名称、签名机构或镜像摘要、父进程或启动器、预期通道和会话标识符。批准后，允许该会话执行操作，直到进程退出或操作人员撤销会话。即使新进程打算使用同一个凭据，也必须重新申请会话。

对于影响足够大的凭据，再使用逐操作批准。生产数据库管理员凭据、能够访问主机群的 SSH 身份，或可以发布公开产物的账户，可能需要每次都由人工决定。普通读取权限则不需要。批准机制应明确体现这种差异，而不是把它埋在没人审核的大型策略文件中。

这种设计提供了共享访问无法提供的遏制选项。代理开始异常时，撤销它的会话，凭据仍可供另一个已批准的发布进程使用。如果怀疑机密泄露，再禁用凭据，并检查引用过它的所有会话。

Sallyport 直接采用了这种分离方式：新代理进程默认获得按会话授予的授权，凭据保存在加密保险库中，操作在代理进程之外执行。会话提示首先展示进程的代码签名机构，这是批准者在允许运行前需要看到的证据。

批准记录应同时包含进程会话和之后使用的凭据引用。提示中不要显示凭据值，也不要在操作结果中返回凭据值。系统即使在界面中隐藏了令牌，但如果把令牌放进代理工具响应，仍然等于把令牌交给了代理。

## 凭据注入让机密离开代理的触手可及范围

代理应通过指定凭据引用和目标来请求操作，由可信执行器只在发出 HTTP 或 SSH 调用的瞬间注入机密。代理得到的是结果，而不是可重复使用的凭据。

这比把令牌放进 shell 环境，再要求代理不要打印它，边界清晰得多。环境变量会泄露到子进程、调试输出、崩溃报告、命令历史包装器和工具调用中。事后脱敏有助于避免意外显示，但无法从已经读取机密的进程中取回它。

安全的操作请求应包含足够的信息，以便授权并执行调用，但不能包含凭据本身：

```json
{
  "session_id": "ses_7zQ2...",
  "credential_ref": "cred_release_publisher",
  "channel": "http",
  "request": {
    "method": "POST",
    "url": "https://api.example.internal/releases",
    "headers": {"Content-Type": "application/json"},
    "body": {"version": "1.4.2"}
  }
}
```

执行器会检查会话是否仍获批准，确认保险库可用，读取 `cred_release_publisher`，将它注入所需的授权标头，执行请求，并记录决策和结果。它应返回代理完成任务所需的响应数据，同时按通常要求处理敏感响应内容。

SSH 也遵循同一原则。代理可以通过 SSH 通道请求 `git fetch` 或远程命令，但不应获得 PEM 格式的私钥，也不应获得任何子进程都能在无人监管的情况下使用的 agent socket。执行器可以使用私钥建立连接，并把事件绑定到代理会话。

Sallyport 目前支持 HTTP 凭据注入，以及通过内置 helper 执行 SSH 操作，因此支持 MCP 的代理可以请求工作，而不持有 API 或 SSH 机密。这种设计不会让每个操作自动安全，但它保留了判断某个进程是否应获准请求该操作所需的分离。

## 共享部署令牌会把失败隐藏到真正需要它时

共享凭据会把一次直接的事件变成重建过程，因为每一条相关日志都显示同一个账户名称。问题往往从一个看似合理的捷径开始。

假设团队把 `PUBLISH_TOKEN` 放在 CI 机密存储中。部署任务读取它，这是预期行为。开发者为了重现发布问题，也在本地导出该令牌。之后，他们在这个 shell 中启动编程代理，因为代理需要检查部署脚本。现在代理可以通过环境调用发布 API。

仓库文件中隐藏的一条指令要求代理执行命令上传草稿发布。命令成功了。API 审计记录显示 `release-publisher created release 1.4.2`，CI 日志中却没有匹配任务。开发者说自己只要求代理检查文件。大家都能同意这些事实，但没有任何一项能识别发出调用的进程。

团队通常会轮换 `PUBLISH_TOKEN`。如果令牌已经泄露，这会阻止重放，但也会中断合法 CI 任务，并迫使团队仓促修复。之后，他们可能在没有证据的情况下指责开发者、代理或仓库指令。账户名称无法帮助他们区分 CI 调用和本地代理操作。

现在改变设计。发布凭据保存在由操作执行器控制的保险库中。代理启动一个会话，记录其签名机构和父终端，然后通过该会话请求上传操作。执行器记录批准决策、凭据引用、目标 API、请求路径和响应状态。

可疑上传发生时，事件记录会立即回答最有用的问题：它来自本地代理会话，该会话从某个终端启动，在特定时间获得授权，并使用发布凭据发出了一次 POST 请求。操作人员撤销该会话。CI 继续运行，因为它单独识别的进程没有被撤销。如果仓库指令可能暴露了更广泛的机密，团队仍可以轮换凭据，但这次决定是由证据支持的，而不是因为没有其他控制手段。

区别不在于能否完美预防。经过批准的代理仍可能请求一个人事后后悔批准的操作。区别在于系统保留了调用者、授权决策和针对性的停止机制。这足以缩短事件处理时间，而不是让调查变成争论。

## 审计记录要连接身份，但不能保存机密

审计轨迹应让你连接进程会话、凭据引用和外部操作，同时把机密材料排除在记录之外。如果这些数据分散在没有共同事件标识符的不同产品中，事件响应就会变成高压下的人工关联。

为会话决策写一条事件，为每个操作再写一条事件。两者使用同一个 `session_id`，每个操作则有自己的事件标识符。如果凭据主体适合保留，可以记录凭据引用和凭据主体，但绝不要记录 bearer token、私钥凭据或原始授权标头。

有用的操作记录应包括规范化目标、方法或远程命令、请求摘要、决策、响应状态和时间戳。当请求体可能包含敏感数据时，使用摘要。SSH 记录主机、远程用户、请求的命令和退出结果。HTTP 记录方法、主机、路径和状态。只有在保留和数据处理规则允许时，才保存原始请求体。

由于执行操作的进程之后可能被指责为隐藏记录，防篡改证据很重要。当调查人员拥有完整序列时，哈希链可以检测删除或重新排列。但它不能让事件本身变得真实。如果收集器接受代理提供的进程名称，那么一条完美的哈希链只会永远保存这个虚假声明。先在监管边界收集身份证据，再保护记录。

简单的验证过程应独立于保险库内容。验证器读取每个加密事件块，检查其中保存的前一个哈希是否与上一事件的哈希匹配，重新计算当前哈希，并报告第一个断裂的位置。输出应指出序列位置，但不能暴露机密：

```text
$ audit verify events.log
verified: 184 events
chain: valid
first_event: evt_01J...
last_event: evt_01K...
```

如果验证失败，在尝试修复前先保留原始文件。缺失记录可能意味着损坏、写入中断、导出处理错误或蓄意修改。哈希链能指出不一致，周围的日志和存储控制才能解释原因。

## 在增加自动化前，先分开身份

不必重建所有 API 集成，就能采用这种模型。先处理那些能够改变生产状态、发布产物、访问客户数据或通过 SSH 连接的凭据。代理进程继承这些凭据时，它们最容易制造严重事件。

第一轮可以这样做：

1. 列出每个凭据主体、它能访问的服务，以及当前能收到其机密的所有进程。
2. 从代理提示词、代理配置和代理进程环境中移除可重复使用的机密值。
3. 定义在操作获准处可以收集哪些进程证据。
4. 为每次代理运行签发会话标识符，并在每个操作旁边记录它和凭据引用。
5. 通过撤销一个会话，同时让另一个获准工作负载继续使用同一账户，测试遏制能力。

最后一项测试能迅速暴露常见错误。如果你只能通过禁用共享账户来停止代理，就没有真正把代理身份和凭据身份分开。你只是给共享风险取了一个好听的名字。

不要一开始就构建庞大的规则引擎。大多数团队只需要几个清晰的判断：保险库是否可用，这个进程会话是否获准，以及该凭据执行这项操作是否需要批准。更复杂的部分是运营纪律：不要再把机密送进代理进程，收集可信的进程证据，也不要把服务账户名称当成人或程序。

下一次出现可疑操作时，要分别回答两个问题：哪个进程请求了它，哪个权限执行了它？如果记录无法同时回答这两个问题，就先修复这一点，再授予另一个代理生产访问权限。
