# 通过操作网关轮换凭据，并保留可信证据

替换即将到期的 API 令牌或 SSH 身份，本来应该是一件平淡无奇的事。但当每个代理、shell 配置文件、仓库秘密和本地工具都各自保存一份副本时，事情就会变得危险。在这种情况下，你轮换的不是一个凭据，而是在寻找数量未知的副本，希望一个不漏地找全，同时还要面对没人测试过的工作流程突然中断。

操作网关改变了这件事的处理方式。网关负责保存凭据、执行对外请求，再把结果返回给代理。这样，轮换只需要处理一个保存秘密的位置、一次受控切换，以及能将代理会话与所用身份区分开的证据。这种区分在事件调查期间尤其重要，因为人们很容易把“这个进程发送了请求”和“这个进程持有令牌”混为一谈。

我见过一些团队因为秘密管理器里出现了新值，就认为轮换已经完成。后来他们发现，旧的部署密钥仍留在账户的 authorized_keys 文件中，或者某个被遗忘的本地环境变量还在让已过期的 API 令牌继续生效。只有当替换后的身份已经证明预期的操作路径可用，并且旧身份再也无法完成身份验证时，轮换才算完成。

## 轮换是身份变更，不是替换字符串

凭据轮换会用一个新的身份替代原来的身份，并证明访问权限已经按预期转移。把新令牌粘贴到某个字段里，只是这次变更中的一个操作。

在记录和工具中，把下面四件事分开：

- 外部身份：提供商接受的 API 令牌、客户端秘密、SSH 密钥对或服务账户凭据。
- 凭据记录：保存身份及其使用信息的加密条目。
- 授权目标：接受该身份的 API 账户、仓库、机器账户、主机或网络端点。
- 代理会话：发起某项操作的具体进程。

人们经常把第一项和第四项混在一起。这会产生错误的事件报告。代理会话可能通过网关调用了某个端点，但它不需要读取 bearer token 才能完成调用。反过来，泄露的令牌也可能被一个从未出现在代理日志中的进程使用。这是两类不同的调查，需要采取不同的遏制措施。

过期问题也一样。提供商可能会在指定时间使令牌失效，但你的网关记录作为存储数据仍然完全正常。记录不会自动过期。你必须在提供商拒绝旧身份之前替换它，验证新身份，然后停用旧身份。

NIST Special Publication 800-57 Part 1 对加密周期的讨论，不把它当成单纯的日历提醒。它把加密周期与暴露程度、使用情况和泄露风险联系起来。API 和 SSH 凭据也应该遵循这个思路。拥有广泛写入权限、使用频繁或归属不明确的令牌，其计划使用期限应该短于只执行一项只读任务、权限范围很窄的身份。不要把轮换变成每周五机械地更换字符串，却继续保留过大的权限范围。

一条清晰的轮换说明应该这样写：“构建代理的会话使用凭据记录 deploy-api-prod 发起了这次请求。该记录使用的提供商令牌，从 ID 末尾为 4K2 的令牌切换为 ID 末尾为 P9M 的令牌。新记录完成预期操作后，旧令牌已被撤销。”其中不应包含令牌本身。

## 在到期日前把秘密集中到一个位置

只要凭据仍然存在于代理提示、仓库文件、shell 变量和复制的配置中，你就无法进行受控轮换。即使到期日已经临近，也应先完成集中管理。

先根据使用情况建立清单，而不只是盘点存储位置。询问每个团队，代理可以发起哪些对外调用，又能连接哪些 SSH 目标。对每项调用记录提供商账户或主机账户、凭据类型、负责人、预期操作、权限范围、过期方式，以及凭据是否出现在其他位置。最后一项往往是最麻烦的地方。

扫描仓库可以发现明显的问题，但无法证明某处不存在秘密。查找环境变量名称、配置键、部署模板、复制的私钥路径，以及指导人们把令牌粘贴进代理配置的文档。还要检查代理的工具定义。如果工具接受 `token`、`api_key`、`authorization` 或原始私钥内容作为参数，即使另一个系统保存了副本，代理仍可能携带秘密。

对于 HTTP，理想的结构很简单：代理提供操作和普通请求数据，网关选择已存储的凭据，并只在发送请求时注入授权材料。代理工具调用在概念上可以包含以下内容：

```json
{
  "credential": "deploy-api-prod",
  "method": "POST",
  "url": "https://api.example.internal/releases",
  "headers": {"Content-Type": "application/json"},
  "body": {"revision": "a81c2f"}
}
```

其中绝不能包含 bearer token，即使字段名称看起来很友好也不行。在工具结果中返回授权头，也是反向发生的同一种错误。应该在操作边界处进行遮盖，而不是等到聊天记录中再处理。

对于 SSH，代理应该通过指定的凭据记录和目标请求连接，而不应收到私钥内容、临时私钥文件，也不应被要求搜索 `~/.ssh`。代理工作目录中的私钥会残留在缓存、编辑器历史、归档文件中，有时还会进入提交记录。我清理过太多这类文件，因此可以确定，“临时”这个词本身没有任何安全含义。

Sallyport 会在 macOS 应用内部的加密保险库中保存 API 密钥和 SSH 密钥，然后执行 HTTP 和 SSH 操作，不向代理暴露秘密。这样你只需要修改一条记录，但仍然必须找出并删除迁移前已经存在的旧副本。

## 为每个凭据指定一个负责人和一个用途

一个凭据可以有多个合法使用者，但仍然需要一名明确负责的人和一个写明的用途。所谓共同负责，通常只是没人检查到期时间、权限范围或停用情况的委婉说法。

命名记录时，应让操作人员无需看到秘密材料，就能识别授权边界。`billing-write-prod` 比 `token-final-2` 表达得更清楚。`github-deploy-repo-a` 也比 `automation-key` 更有信息量。如果环境会影响目标，就把环境写进名称；如果凭据属于某项服务职能，就不要把个人姓名嵌入名称。人员会离开，但服务用途应该始终清楚。

不要因为减少记录数量，就让一个共享令牌覆盖不相关的目标。这个捷径很受欢迎，因为看起来只需要续期一次，但它会带来三个问题：

1. 你无法判断到底哪个目标触发了轮换。
2. 某个用途需要扩大权限时，其他用途也会一起获得更大权限。
3. 事件期间撤销该身份，会中断无关工作。

当操作范围和负责人确实相同，一个记录可以支持针对同一提供商账户的一组密切相关的操作。但这应是明确的决定，而不是默认做法。如果构建发布任务和支持导出任务需要不同权限，即使它们调用同一个 API，也应该使用不同身份。

SSH 也需要同样的纪律。绑定到某组主机上的部署账户的 SSH 密钥，不应因为生产数据库也使用 SSH，就兼作生产数据库访问密钥。尽可能在服务器端设置限制：专用账户、受限命令、网络支持时的来源限制，以及清晰的 authorized key 注释。注释不会强制执行访问控制，但在压力下检查 `authorized_keys` 时，它能让仍然存在的旧密钥更容易被发现。

OpenSSH 的 `authorized_keys` 手册记录了 `command=`、`restrict` 和 `from=` 等选项。只有在测试过自动化所需的完整命令路径后，这些选项才真正有用。我见过有人出于好意添加 `restrict`，却意外破坏了部署任务悄悄依赖的端口转发。计划轮换期间发现这种问题，总比半夜才发现好。不要盲目添加所有可用限制，只添加与已命名操作相匹配的限制。

## 使用有明确结束时间的重叠窗口

只在验证替换结果和应对切换失败所需的时间内，让旧凭据和新凭据同时有效。重叠是一种安全措施，不应成为永久运行模式。

有些提供商允许多个 API 令牌或多个客户端凭据同时有效。先创建替换凭据，记录一个可以安全保留的标识符，再将它加载到网关记录中。然后通过代理日常工作所使用的同一路径执行一次无害操作。只要该操作能检查所需权限范围和目标账户，就可以使用读取端点、在测试项目中创建草稿，或请求返回调用者身份。

如果代理平时会发布版本或修改工单，不要只依赖通用的“令牌有效”端点。令牌可能有效，却没有写入权限，指向沙盒账户，或者因为网关注入了错误的请求头类型而失败。应使用无害对象，测试真实的方法、URL 范围、账户和请求体结构。

在测试前就设定旧凭据的撤销时间。如果你无法说出这个时间，就没有重叠窗口，而是又创建了一个永久凭据。

实际步骤可以是：

1. 创建权限范围符合要求、且过期策略明确的新提供商凭据。
2. 更新唯一的网关记录，只在声明的重叠期内保留旧凭据。
3. 通过已授权的代理会话或由操作人员控制的测试会话，执行一次范围狭窄的操作。
4. 在提供商处验证结果，并检查操作日志中是否出现预期会话和目标。
5. 撤销或删除旧提供商凭据，然后再次执行一次范围狭窄的操作。

撤销后的最终测试并非形式主义。它能发现一个尴尬问题：测试可能因为环境变量、代理配置或另一条凭据记录优先级更高，实际上悄悄使用了旧令牌。这个问题发生得比人们承认的更频繁。

对于只允许一个活跃 API 凭据的提供商，你无法建立真正的重叠期。应安排变更窗口，在切换前记录一次基线操作，替换网关中的秘密，立即执行范围狭窄的测试，并安排账户负责人待命，以便提供商拒绝新凭据时及时创建替代凭据。不要因为无法重叠，就提前把新令牌放进代理配置中。

## 验证代理实际经过的路径

轮换测试必须经过网关、凭据选择、请求构造、远程授权和结果处理。只单独测试每个组件，会在凭据错误最容易隐藏的地方留下空白。

对于 HTTP，应让测试请求足够具体，这样你才能在提供商日志中识别它。如果 API 支持幂等令牌，就使用一个幂等令牌；否则创建一个标记清楚、可以随时删除的临时对象。检查提供商报告的账户或主体是否正确，然后确认操作日志中有匹配的调用、目标、结果和会话引用，并且没有打印凭据。

通用 shell 测试有助于隔离提供商行为，但它能证明的事情没有人们想象的那么多：

```sh
curl -sS -D /tmp/headers.txt \\
  -H "Authorization: Bearer $NEW_TOKEN" \\
  https://api.example.internal/v1/whoami
```

预期输出通常会在 `/tmp/headers.txt` 中显示 `200` 状态，并在响应体中标识服务账户。这说明提供商接受该令牌，但不能证明操作网关注入了相同的请求头、选择了正确记录，或阻止代理看到 `$NEW_TOKEN`。只能在操作人员控制下使用此方法，完成后从 shell 中删除变量。绝不要把真实令牌粘贴到工单或保存的终端录屏中。

对于 SSH，应使用自动化实际需要的同一主机名、用户和命令结构进行测试。下面的公钥检查可以帮助诊断服务器授权，同时不会在输出中暴露私密材料：

```sh
ssh -o BatchMode=yes -o IdentitiesOnly=yes \\
  deploy@host.example.internal 'id \u0026\u0026 test -w /srv/releases \u0026\u0026 echo write-ok'
```

`BatchMode=yes` 会让身份验证失败直接返回，而不是等待交互式密码。`IdentitiesOnly=yes` 可以阻止 SSH 客户端尝试代理中加载的其他无关密钥。不要因为 `ssh deploy@host` 登录成功，就认为部署操作一定可用。远程账户可能允许 shell 登录，却拒绝任务所需的命令、目录或强制命令限制。

OpenSSH 的 `ssh` 手册说明，`IdentitiesOnly` 会限制客户端提供的身份。这个选项可以发现开发者电脑上常见的假阳性结果：本地身份验证代理中的个人密钥登录成功，但新的自动化密钥在生产环境中会失败。在网关设计中，应通过网关的 SSH 路径执行等价检查，这样选中的存储密钥就不会产生歧义。

成功和失败都要测试。故意尝试使用未授权的方法发送请求，或执行超出目标账户权限范围的 SSH 命令。你希望远程系统明确拒绝，并生成准确的日志记录。如果一个本应范围很窄的凭据能够完成无关操作，就停止轮换，在停用旧身份前先缩小权限范围。

## SSH 轮换会因服务器上残留旧公钥而失败

在保险库中替换 SSH 私钥，并不会撤销旧密钥。只要旧公钥仍存在于任何信任它的授权源中，服务器就会继续接受旧身份。

SSH 的清单管理很复杂，因为公钥可能出现在多个位置：`~/.ssh/authorized_keys`、身份管理服务、云实例元数据设置、配置管理模板，或提供商的部署密钥界面。先找到权威来源，再进行修改。如果配置管理会重新生成 `authorized_keys`，紧急手动删除可能会在下一次运行时恢复。

使用批准的工具生成替换身份，并且只在网关边界保存私钥部分。将新的公钥与旧公钥并列安装。为每个条目添加能说明用途和轮换日期的注释，然后通过预期路径进行测试。确认可用后，从事实来源中删除旧公钥条目，并确认服务器会拒绝它。

你可以在不暴露私钥的情况下查看公钥指纹：

```sh
ssh-keygen -lf deploy-release-ed25519.pub
# 256 SHA256:exampleFingerprint deploy-release-2025 (ED25519)
```

输出的结构比示例指纹本身更重要：位数、指纹、注释和密钥类型。将实际指纹写入变更记录。不要把公钥周围的授权选项放进含糊的截图中。将服务器端的完整规则复制到经过审查的配置中，这样其他操作人员就能看到它是否包含强制命令或来源限制。

然后，在销毁最后一份受控旧密钥之前，使用旧密钥执行否定测试。删除旧密钥后，服务器应该拒绝它。如果你已经不再拥有旧私钥，无法执行该测试，就检查权威授权密钥来源和提供商审计记录，并在记录中说明这一限制。假装测试过撤销，比记录一次不完整的检查更糟糕。

不要通过覆盖同一路径上的 SSH 密钥文件，再重启一个情况不明的客户端来完成轮换。长期运行的进程可能保持连接，SSH 代理可能继续提供旧身份，辅助程序也可能缓存文件描述符。无状态操作辅助程序可以减少这类歧义，因为每次连接都会从一个已知选择开始。重要的是可观察的行为，而不是某种偏好的实现语言。

## 将授权与轮换分开

批准代理执行某项操作，与批准它每次调用都使用某个特定凭据，是两件不同的事。把两者视为同一个控制，要么造成审批疲劳，要么让敏感身份缺少约束。

会话控制回答的是：“这个刚启动的代理进程可以在本次运行期间执行操作吗？”它适合在新进程获得对外访问权限之前进行拦截。单次调用控制回答的是：“现在可以使用这个特定凭据吗？”它适用于支付操作、生产发布，或权限范围足以让每次使用都值得人工确认的凭据。

不要因为凭据轮换让所有人感到紧张，就要求人工批准每一次无害的状态读取。人们会在不阅读的情况下批准重复出现的相同提示。对于每次操作都需要判断的身份，应频繁要求批准；其他情况则使用边界明确的会话批准。轮换不应训练操作人员机械点击警告。

Sallyport 的决策阶梯会始终保留保险库这一绝对边界，然后默认授权新的代理进程在本次会话中运行，同时允许对选定凭据的每次使用单独要求批准。轮换期间，操作人员可以允许测试会话运行，同时在切换完成前，要求每次使用新的生产凭据都进行明确确认。

保险库边界有独立作用。保险库锁定后，无论代理会话之前获得了什么权限，都必须拒绝操作。这让操作人员在怀疑泄露或变更行为异常时拥有明确的停止点。但它不会撤销提供商凭据。如果有人可能已经将凭据复制到网关之外，锁定访问后仍要撤销或停用外部身份。

简要记录谁批准了例外的生产测试，以及批准原因。不要把审批说明写成日记。会话身份、时间、凭据记录、目标和变更引用，就足以将批准与轮换事件关联起来。

## 审计证据必须回答两个不同的问题

一份有用的轮换记录应同时回答“代理做了什么？”和“我们能相信这份记录吗？”普通应用日志在文件被修改、某行被删除，或日常调用与事件调用混在一起后，通常无法很好地回答这两个问题。

第一个问题需要操作字段：会话身份、可用时的代码签名机构或进程身份、时间、操作类型、目标、凭据记录名称、结果，以及提供商侧的请求引用。日志不应保留 bearer token、密码、私钥或包含秘密的完整请求头。包含凭据的日志，会把每个日志读取者都变成另一个凭据持有者。

第二个问题涉及完整性。“只追加”这个说法本身不够，因为管理员仍可能改写昨天的日志。哈希链让每条记录都依赖前一条记录，验证者检查链时，就能发现删除或修改。它不能证明某个操作从未发生，也不能让虚假输入变成真实记录，但能显著增加悄悄修改记录并冒充原始记录的难度。

这就是分开保存会话日志和活动日志的价值。会话日志回答哪个代理进程获得了权限，以及是否有人撤销了这次运行。活动日志回答其中执行了哪些具体的 HTTP 或 SSH 操作。如果 API 令牌在 14:00 轮换，你可以查看切换前后使用该记录的调用，而不必把每次会话批准都当成每个请求的证据。

Sallyport 会从加密、不可写的审计日志中生成这两种视图，并可以使用 `sp audit verify` 在离线状态下验证日志链，无需保险库密钥。在敏感轮换前后都执行验证，并将结果与变更记录一起保存。验证成功说明记录链在内部保持一致，但不能替代对实际目标和结果的检查。

对于高风险轮换，可以在一条简洁记录中保存以下证据：

- 凭据为何发生变化，以及谁负责授权目标。
- 旧、新提供商凭据的安全标识符，以及计划的重叠结束时间。
- 范围狭窄的验证操作及其提供商侧结果。
- 验证过程中涉及的代理会话和活动引用。
- 旧身份撤销的证明，或已记录的限制。

这份记录能让后续调查人员区分正常的计划变更和无法解释的新凭据，也能在数月过去之前暴露遗漏的撤销检查。

## 将疑似泄露先当作遏制事件处理，再进行替换

如果怀疑代理看到了秘密或将秘密导出，应先停止访问。只创建替换令牌，却不禁用已经暴露的令牌，会把旧路径继续留给任何复制过它的人。

如果需要立即进行本地遏制，就锁定操作网关；撤销不应继续运行的活跃代理会话；并禁用或撤销提供商凭据。在清理任务删除上下文之前，保存相关的会话和活动证据。然后创建一个权限范围只覆盖必要工作的全新身份。

不要等到完全证明秘密已经泄露才采取行动。出现在代理记录、shell 历史、仓库提交、构建日志或聊天消息中的令牌，都足以将其视为已暴露。对于 SSH 私钥，应从所有受信任来源中删除匹配的公钥，并搜索自动化写入产物的位置，查找私钥副本。轮换私钥，却让旧公钥继续获得授权，不算遏制泄露。

服务恢复后，要找出秘密是如何越过边界的。常见原因都很普通：工具参数接受原始凭据，调试日志打印了请求头，开发者把本地环境文件复制进代理工作区，或者备用客户端绕过了网关。在宣布事件结束前，先修复这条路径。否则，替换后的凭据只会开始下一次泄露倒计时。

## 把到期工作变成有计划的运行测试

日历提醒应该触发一套可重复的测试、负责人检查和撤销决定，而不是让人惊慌地把新令牌粘贴进上次刚好能用的配置文件。

在提供商到期日前检查每条凭据记录。确认指定负责人仍然负责外部账户，记录中的用途仍然存在，权限范围仍然符合操作要求，并确认需要访问权限的代理会话仍然确实需要它。如果其中任何一项的答案是否定的，就停用身份，不要轮换它。

对于仍有必要保留的身份，应尽早演练范围狭窄的验证操作，以便发现提供商账户变化、新的 SSH 限制或 API 权限变化。将演练记录与实际轮换分开保存，避免有人把过去成功的测试误认为今天的替换凭据已经可用。

最容易发现缺陷的测试往往令人不舒服：撤销旧凭据后，再次执行代理所依赖的完全相同的操作，并确认日志将其归属于预期会话和记录。如果这次测试失败，你面对的是一次有负责人、有证据、有明确回退路径的受控失败。这比在自主部署进行到一半时才发现凭据过期，要好得多。
