# 面向 AI 代理的 SSH 证书与本地凭据保管

SSH 证书和本地凭据保管需要配合使用。证书向 SSH 服务器提供一份带有效期限的声明，说明某个公钥可以被接受。本地保管则让对应的私密凭据远离 AI 代理的上下文、文件、子进程和日志。把其中任何一个当成另一个的替代品，都会留下安全缺口，并在最糟糕的时候暴露出来。

这种设计最常见的说法是，15 分钟有效的证书就能让一切安全。事实并非如此。如果代理可以读取私钥，它就能一直使用该密钥，直到证书过期；如果它能连接签发服务，还可能申请新的证书；它也可能把密钥复制到运行结束后仍然存在的工作区。较短的有效期只能缩短服务器接受某张证书的时间，不能消除持有秘密的进程所拥有的能力。

对于能够修改基础设施的代理，应使用证书缩短服务器接受凭据的时间，同时用本地保管防止私密凭据泄露。然后把续期、中断和撤销设计成一个完整的运维系统。图上标出的证书有效期并不是重点，真正重要的是这些细节。

## 证书限制的是接受时间，不是持有时间

OpenSSH 用户证书是经过签名的公钥，以及一组约束。SSH 服务器会验证 CA 签名，并检查证书类型、有效时间范围、主体列表、关键选项和扩展等字段。客户端仍然必须证明自己控制着与该公钥对应的私钥。

人们经常混淆这两点。证书属于公开材料。你可以把 `id_ed25519-cert.pub` 放在私钥旁边，将它复制到客户端，或随意检查它。秘密仍然是 `id_ed25519`。如果代理收到了这个私钥文件，较短的证书有效期只能限制某一张已签发证书的可用时间。代理仍然持有可以重复使用的签名凭据。

OpenSSH 在 PROTOCOL.certkeys 中记录了证书格式，并在 `ssh-keygen` 手册中提供了常用的创建选项。签名命令使用 `-s` 指定 CA 私钥，使用 `-I` 指定身份字符串，使用 `-n` 指定允许的主体，使用 `-V` 指定有效期。这些都是授权输入，不是装饰性元数据。服务器必须配置为信任该 CA，并在这些字段影响访问之前正确解释主体。

可以比较两种设计：

- 第一种设计中，代理收到 `id_ed25519`、证书和 CA 端点。代理可以自行签署 SSH 挑战，也可能能够发起未来的证书请求。
- 第二种设计中，本地执行器将 `id_ed25519` 保存在受保护的存储中。代理请求一项明确命名的 SSH 操作，由执行器处理 SSH 身份验证，并且不返回任何私密材料。

两种设计都可以使用同一张证书。只有第二种设计能阻止代理提示注入、被入侵的插件或过于好奇的子进程复制私密凭据。

也不要把证书的身份字段当作保管凭据的证明。`-I` 值可以帮助人们关联签发记录和访问记录，但任何能够申请证书的调用方都可以选择一个看起来很正式的身份。如果签发服务接受任意公钥和任意身份字符串，那么在压力之下，审计记录会变成虚构故事。

## 本地保管改变了你需要规划的故障类型

将凭据留在本地，会把秘密外泄事件转变成操作授权事件。这是更好的问题，但仍然需要处理。代理可能会请求执行破坏性命令、定位到错误的主机，或在操作员原本只打算允许短时间使用的情况下继续使用已批准的会话。

本地组件应持有 SSH 私钥，并自行完成协议操作。代理可以收到命令输出、退出状态和有限的诊断信息，但不应收到私钥文本、临时文件路径、环境变量、可以转发的 SSH agent 套接字，也不应收到某个其他工具能够解析的伪装脱敏值。

一种常见的错误妥协方案，是将秘密目录以只读方式挂载到代理环境中。只读可以防止修改，不能防止读取。另一种错误方案，是把私钥放进 SSH agent，并让每个子进程都能访问 `SSH_AUTH_SOCK`。对于严格控制的交互式 shell，这种做法或许可以接受。但自主编程代理会启动工具、测试运行器、软件包钩子和辅助进程，每一个进程都会扩大能够要求 agent 签名的范围。

本地保管还提供了一个有用的人为控制点。人员可以审批新进程发起的第一次操作，要求每次使用敏感凭据时都获得批准，或直接锁定整个保险库。这些控制不能替代服务器授权，它们决定的是某个进程是否可以尝试执行操作。

保持这条边界足够狭窄。持有私钥的组件不应因为代理提供了任意 shell 文本，就盲目接受并执行。如果它要执行一个操作请求，就应明确知道目标主机、目标账户和操作内容，并在执行前或执行时记录这些事实。如果无法还原哪个进程请求了 `ssh deploy@host`，本地保管就变成了一个缺乏问责能力的秘密包装层。

实际结论很直接：证书续期负责刷新公共授权，本地保管继续负责控制私钥签名。除非有独立的轮换理由，否则不要每次证书过期都重新生成私钥。

## 证书有效期应当匹配中断容忍度

应根据停止新工作后还能容忍已签发凭据存在多久来选择有效期。只读诊断、部署流程，以及能够修改访问控制的代理，答案都不同。

先选择一个能为真实工作留出余量的时长。团队往往低估证书续期失败的频率，因为笔记本会休眠、VPN 路由会变化、CA 可能短暂不可用、长命令会保持连接，或者机器时钟会漂移。过短的有效期会把普通中断变成持续的重试逻辑，并诱使团队绕过安全控制。

对于许多自主任务，15 到 60 分钟通常是合理的初始范围。账户能够影响生产系统时，使用较短的一端。只有在任务确实需要更长时间，并且你能明确说明原因时，才使用更长的窗口。一个只运行 10 分钟的任务，却使用数小时有效的证书，通常只是把便利伪装成了运维需要。

不要把证书有效期和 SSH 连接寿命混为一谈。SSH 通常在建立连接时进行身份验证。证书到期时，服务器通常不会把已经通过身份验证的会话踢出去。连接复用会让这个问题更明显：后续命令可能会复用已经通过身份验证的主连接，而不是重新检查证书。

这会改变设计方式：

1. 用证书有效期限制新的身份验证。
2. 在本地执行器中单独限制命令或会话时长。
3. 不要把可复用的多路复用控制套接字交给代理。
4. 如果必须立即停止操作，就在紧急情况下终止活动会话。

OpenSSH 的 `ssh_config` 选项 `ControlMaster` 可以提高交互速度，但会破坏每条命令都重新进行身份验证这一简单假设。对于自主工作，应在敏感目标上禁用多路复用，或由执行器负责创建并关闭连接。不要让代理继承一个在审批或证书窗口结束后仍保持身份验证状态的控制路径。

时钟同步也属于这一部分。证书拥有绝对的有效时间范围。如果签发主机和目标服务器的时间存在明显差异，新生成的证书可能看起来已经过期，或者尚未生效。应监控两端的时间同步，并在服务器拒绝有效期时安全失败。不要通过签发超长证书来绕过时钟故障。

## 续期必须证明同一条保管边界仍然存在

续期服务只有在能够将请求关联到本地持有的凭据、当前代理会话和请求的授权范围后，才应签发新证书。仅仅在 HTTP 请求中接受一个公钥，并不能证明请求方控制着对应的私钥。

最清晰的流程是，让本地执行器在续期时证明自己持有已登记的公钥对应的私钥。签发方验证该证明，检查活动会话和请求的主体，签署公钥，然后只返回公共证书。执行器将这张证书附加到内部持有的私钥上，用于下一次 SSH 连接。

签发方上的证书创建命令可能如下：

```
ssh-keygen -s agent_user_ca -I run-4821 -n deploy-prod -V +30m agent-run-4821.pub
```

这条命令使用 CA 私钥为 `agent-run-4821.pub` 签名，并创建一张配套的公共证书，通常命名为 `agent-run-4821-cert.pub`。`run-4821` 字符串帮助关联记录，`deploy-prod` 是允许的主体，`+30m` 请求一个 30 分钟的有效时间范围。身份字符串应由签发方自行生成，不要信任代理提供的标签。

在让自动化依赖每张新证书之前，都应检查它：

```
ssh-keygen -L -f agent-run-4821-cert.pub
```

输出会列出证书类型、签发 CA 指纹、身份、序列号、有效范围、主体、关键选项和扩展。应把这项检查纳入签发方的测试。否则，缺少主体、有效期从未来开始，或出现意外扩展，都可能在部署失败时才暴露，随后有人会试图拿出长期有效的备用密钥。

应在工作需要建立新的 SSH 连接之前续期，而不是等到到期的准确秒数。执行器可以在有效期末尾附近请求替换证书，此时现有证书仍然可用。如果剩余有效期不足以覆盖命令允许的运行时长，也应拒绝启动该命令。这样可以防止任务在下一次身份验证即将失败前，刚好开始一项重要的写操作。

在请求层面保持续期幂等。响应丢失后，客户端可能重试，即使签发方已经创建了有效证书。记录请求标识、会话标识、公钥指纹、主体和到期时间。重试时，如果输入相同，就返回之前签发的证书。如果输入不同，应拒绝请求，不要猜测代理指的是哪一次请求。

## 主体和服务器策略决定证书能做什么

证书说明谁可以进行身份验证。服务器仍然决定哪个本地账户接受该身份，以及该账户能够执行什么操作。如果允许代理证书以权限广泛的共享管理员账户进行身份验证，较短的有效期也无法挽救宽松的授权决策。

OpenSSH 可以通过 `TrustedUserCAKeys` 信任用户 CA。随后可以使用 `AuthorizedPrincipalsFile` 或 `AuthorizedPrincipalsCommand` 映射接受的主体。后者适合需要集中管理账户映射的服务器，但会为登录增加可用性依赖。静态主体文件灵活性较低，对于规模较小的服务器群通常更容易理解和维护。

受约束的服务器配置可以如下：

```
TrustedUserCAKeys /etc/ssh/agent_user_ca.pub
AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u
RevokedHostKeys /etc/ssh/revoked_agent_credentials.krl
```

对于名为 `deploy` 的本地账户，`/etc/ssh/auth_principals/deploy` 可能只包含：

```
deploy-prod
```

这意味着，服务器只有在证书来自指定 CA、包含 `deploy-prod` 主体，并且没有被配置的 KRL 撤销时，才会接受它。应在服务器上使用的确切 OpenSSH 版本上进行测试。配置指令和证书行为在原理上较为稳定，但发行版打包方式和旧版本可能会影响你能够依赖的功能。

证书的关键选项和扩展还可以进一步缩小使用范围。OpenSSH 支持 `force-command` 和 `source-address` 等关键选项，也能识别 `permit-pty`、`permit-port-forwarding` 和 `permit-agent-forwarding` 等扩展。如果目标流程范围明确，就应使用这些限制。一个只运行部署辅助程序的代理，不应意外获得通用交互式 shell。

不要因为将来可能需要调试，就给代理证书添加通用的 `permit-pty` 能力。需要调试时，应单独签发一张经过明确批准的诊断凭据。为特殊任务提供的便利扩展，往往会在任务结束后长期存在。

## 紧急撤销不能只靠等待过期

短期有效期适合日常遏制。如果停止签发，而证书将在 30 分钟后过期，那么新的 SSH 身份验证会在这个窗口结束后停止。当你在任务接触敏感目标之前发现异常时，这或许足够。但如果私钥可能已经泄露，或代理已经开始造成危害，仅靠它就不够了。

第一步应是停止为相关会话、凭据或 CA 签发新证书。接着，在紧急程度要求的情况下，阻止已经签发的凭据继续进行身份验证。在 OpenSSH 中，密钥撤销列表，也就是 KRL，可以让服务器拒绝特定证书、公钥或签发 CA。

操作员可以使用类似下面的命令，将已签发证书加入 KRL：

```
ssh-keygen -k -f revoked_agent_credentials.krl -s agent_user_ca.pub agent-run-4821-cert.pub
```

`-s` 参数标识签署该证书的 CA。将生成的 KRL 分发到目标服务器，确保路径与 `RevokedHostKeys` 保持一致，并按照操作流程重新加载 sshd。应提前测试完整路径：创建证书，成功进行身份验证，将证书加入 KRL，分发文件，重新加载 sshd，然后确认新的身份验证会失败。

KRL 不是中央式的终止开关。每台 SSH 服务器都必须收到并读取该文件。如果某台主机不可访问、处于离线状态，或由另一套配置机制管理，它可能会一直接受该证书，直到证书过期。因此，即使你运行撤销列表，较短的有效期仍然有用。

如果怀疑 CA 私钥本身已经泄露，撤销单张证书的规模就不对了。应在受影响的服务器上移除或替换对该 CA 的信任，签发新的 CA，然后只为仍然信任的凭据重新签证。为代理访问维护独立的 CA，这样处理时不会锁死人工应急访问。保留经过测试的紧急访问路径，但让它远离代理，并记录每次使用。

最后，要终止活动会话。KRL 的分发会阻止未来的身份验证，但不一定会终止服务器已经接受的 SSH 连接。使用主机上的会话控制、任务监管器或网络控制来停止正在运行的工作。随后还要检查已经执行的命令，因为撤销凭据无法撤回部署，也无法恢复已删除的文件。

## 证书签发方必须在不确定时安全失败

当签发方无法验证本地执行器身份、会话状态、请求主体，或该范围所需的授权时，应拒绝续期。可用性压力会让团队把这些检查降级成警告，但这样会把临时故障变成无限制的凭据签发。

在部署前，先规划普通故障场景。如果签发方不可访问，可以让已有证书继续工作到正常过期，但不要悄悄替换为静态私钥。如果本地保险库已锁定，就拒绝操作。如果需要人工审批但无人响应，就让任务过期，不要让代理无限期保留审批结果。

有用的续期记录应包括 CA 指纹、证书序列号、签发时间、到期时间、公钥指纹、请求主体、代理进程身份、会话标识、目标类别和审批结果。证书本身包含其中一部分信息，但它不会告诉你请求是否经过了预期的本地控制路径。

签发方还应阻止续期过程中的范围漂移。一个获准使用 `deploy-prod` 的会话，不应仅仅因为代理修改了计划，就续期为 `root-prod` 或其他更宽泛的主体。不同账户、目标组或命令类别都应要求新的授权边界。许多长期运行的自主任务正是在这里变得不安全：会话一开始范围很窄，后来通过无人审查的续期逻辑不断累积例外。

将签发记录和执行记录分开保存。签发记录证明 CA 授权了某张证书。SSH 服务器日志和执行器活动记录则证明凭据在哪里使用，以及运行了什么命令。只有两者结合，操作员才能判断证书是被错误签发，还是签发正确却被滥用。

## 把撤销当作有时间要求的运维演练

只写着「撤销证书」的操作手册并不完整。它还必须写明涉及哪些系统和文件、需要什么访问权限、预期失败表现是什么，以及签发方不可用时谁可以行动。应在非生产目标上演练，使用真实证书和生产环境采用的同一分发机制。

按以下顺序操作：

1. 签发一张序列号已知、有效期较短且有明确记录的证书。
2. 完成一次身份验证，并记录服务器端的接受事件。
3. 禁用关联代理会话的续期。
4. 将证书加入 KRL，分发 KRL，重新加载目标 sshd 实例。
5. 尝试建立新的 SSH 连接，确认服务器拒绝该连接；如果存在原有活动会话，再终止它。

测量从发起遏制请求到每个服务器组拒绝连接的实际时间。不要发布从未实际观察过的目标数字。这个时间取决于配置系统分发 KRL 的速度，以及重新加载 sshd 的可靠程度。

还要测试那些令人不舒服的场景：某台目标主机无法访问时撤销证书；存在多路复用连接时撤销；本地机器休眠、续期无法完成时撤销；操作员已经批准会话，但第一条命令尚未执行时撤销。每个结果都会告诉你控制措施阻止的是签发、新的身份验证，还是正在执行的操作。这些是不同的控制，把它们统称为撤销会制造危险的误解。

保存演练证据。保留已签发证书、KRL 更新、执行器记录和目标服务器的拒绝日志。真实事件发生时，这些材料能为响应人员提供经过验证的操作路径，而不是一份已经过时、只能寄托希望的文档。

## 让审批、审计和 SSH 访问保持关联

只有当人工审批能够映射到实际使用它的进程时，审批才有价值。审批一个未命名的后台进程，就等于邀请自己批准错误的对象。审核人员应看到是谁签署了该进程、它要启动哪个会话，以及请求的 SSH 凭据属于普通范围还是敏感范围。

Sallyport 将 SSH 凭据保存在加密的本地保险库中，并通过内置 helper 执行 SSH，因此支持 MCP 的代理不需要在自身环境中持有 SSH 私钥。它的会话日志和活动日志可以将人工审批、代理进程和后续操作记录关联起来。

审计记录既需要便于查看，也需要具备防篡改证据。Sallyport 将日志投影到加密的哈希链审计日志中，`sp audit verify` 允许操作员在离线状态下验证这条链，无需访问保险库。在事件复盘和导出证据时都应执行验证。仅有一份可读的活动列表，并不能证明没有人删除不利事件。

不要让审计系统承担它并未执行的访问策略。SSH 服务器仍然必须信任正确的 CA，只接受预期主体，读取最新 KRL，并限制目标账户。本地执行器仍然必须保护私密凭据，并按设计要求发起审批。日志提供证据和运维反馈，但无法修复权限过大的账户。

最有效的起点不是单纯缩短证书有效期。先盘点哪些代理操作需要 SSH，为每项操作分配范围狭窄的主体和账户，将私钥置于本地保管之下，然后演练停止签发、分发撤销信息和终止活动会话的完整流程。确认系统确实能这样工作后，再选择实际续期系统能够稳定支持的最短证书有效期。
