# 分离 SSH 凭据，让生产环境访问更安全

一个能够访问开发、暂存和生产环境的 SSH 凭据，会把小小的便利变成广泛的远程权限。它也会让审批失去意义：一个人可能批准了看似无害的暂存环境任务，但同一个凭据几分钟后就能打开生产环境会话。

应按系统类别和风险拆分 SSH 凭据。然后在客户端配置中明确体现这种拆分，在服务器端强制执行，并像测试允许的路径一样认真地测试被拒绝的路径。我见过一些团队把这称为访问控制，但他们只是在 `~/.ssh/config` 中给同一个密钥起了三个名字，这不构成边界。

## 一个凭据就会产生一个故障域

跨环境共享凭据，会把这些环境的全部权限合并到每个持有者手中。如果工程师的工作站、自动化任务或审批路径可以使用它，那么其中任一条路径被攻破，都可能触及接受该公钥的最高权限环境。

常见的辩解是，同一批人负责管理所有环境。这没有抓住重点。人员可能重合，但使用场景并不相同。开发工作经常涉及未经审核的脚本、测试数据、临时机器和广泛试验。生产工作应允许更少的流程，经过更谨慎的审核，并清楚记录谁在什么原因下建立了连接。

这也是为什么给同一个凭据设置不同主机别名没有实际作用。下面的配置看起来很有条理：

```sshconfig
Host dev-db
  HostName dev-db.internal

Host prod-db
  HostName prod-db.internal
```

但如果两台主机都接受同一个 SSH 公钥，两个别名都可以用同一个私钥完成认证。自动化变量中的一个拼写错误，或一条被复制的命令，都可能绕过边界，而不触发任何新的授权决定。

不同凭据会改变结果。被盗的开发凭据应在暂存和生产环境中认证失败。获准使用暂存凭据的进程，不应拥有任何私密材料或签名能力，使它能够认证到生产环境。这是两种不同的控制。前者限制入侵造成的影响，后者防止一个合法但权限过宽的进程执行错误操作。

不要把这和密码轮换混为一谈。轮换是在一段时间后替换凭据，隔离则决定凭据究竟能在哪里使用。两者都需要，但轮换无法修复一个被有意设置为到处接受的凭据。

## 环境名称不足以构成边界

只有当开发、暂存和生产对应不同的信任决策时，它们才是有用的标签。名为 `staging` 的主机可能包含类似生产环境的数据，发送真实邮件，保存签名密钥，或连接到在线支付端点。反过来，生产监控主机所需的权限可能低于暂存数据库管理员。

应根据会话产生的影响来分类 SSH 访问，而不是根据主机名前缀分类。我通常先问四个问题：

- 这个账户能读取客户数据或受监管数据吗？
- 它能修改在线服务、部署、防火墙或 DNS 记录吗？
- 它能取得另一个凭据，或冒充其他服务吗？
- 它能跳转到权限更高的系统吗？

如果这些问题的答案在不同主机之间有所变化，凭据范围也应随之变化。这通常会产生比熟悉的三环境模型更实用的分组：应用开发、包含敏感数据的测试系统、发布主机、生产环境只读诊断，以及生产环境管理。

独立的 Unix 账户通常也应纳入设计。名为 `deploy` 的账户可以拥有发布目录，并接受受限命令。名为 `ops-read` 的账户可以查看日志，但不能编辑服务单元。名为 `admin` 的账户可以在更高审批标准下执行维护。不同账户让服务器有地方附加权限，也让日志中的主体更有意义。

不要因为账户名称不同，就接受一个共享凭据。如果同一个公钥出现在不同机器的 `dev`、`deploy` 和 `admin` 账户下，私钥仍然是跨环境凭据。不同账户和不同凭据解决的是问题的不同部分。

## 为每个人和每个进程提供独立身份

每个操作人员和每个自动化进程，都需要在获准的环境类别中拥有独立的 SSH 身份。团队共享的私钥会让事件响应变慢，因为撤销一个人的访问权限意味着要为所有人替换凭据。它也会让日志几乎失去作用，因为多个人使用同一个账户和同一个公钥认证时，日志无法区分他们。

对于人工操作人员，应为允许的不同范围创建不同私钥。名称应说明范围，不要使用 `id_ed25519_new` 或 `server-key-final` 这类含义模糊的名称。

```sh
mkdir -p ~/.ssh/identities
chmod 700 ~/.ssh/identities

ssh-keygen -t ed25519 \\
  -f ~/.ssh/identities/id_ed25519_dev_alex \\
  -C dev-alex

ssh-keygen -t ed25519 \\
  -f ~/.ssh/identities/id_ed25519_stage_alex \\
  -C stage-alex

ssh-keygen -t ed25519 \\
  -f ~/.ssh/identities/id_ed25519_prod_alex \\
  -C prod-alex
```

注释有助于人们查看公钥列表，但它不会强制执行任何规则。服务器根据公钥、账户和授权规则决定是否允许访问。把注释当作供操作人员识别的标签，而不是安全元数据。

自动化也需要同样的纪律。发布任务应使用发放给该任务和该环境的凭据，而不是把工程师的生产身份复制到密钥存储中。如果多个任务需要访问，除非它们拥有相同的负责人、目标集合和命令权限，否则应为它们提供不同身份。事后复盘时，一个凭据应该能够回答一个简单的问题：是谁使用了它？

硬件保护的凭据可以提高窃取私钥文件的难度，但无法修复宽泛的授权设计。一个被所有环境接受的硬件保护凭据，仍然可以访问所有环境。先限定范围，再决定如何保护每个私钥。

## 客户端配置必须防止身份外溢

OpenSSH 会尝试配置中的身份，除非加以限制，也会尝试 SSH 代理提供的身份。只要代理中同时保存了生产身份，而本来要连接暂存环境的连接又用它成功认证，这种便利就会变成问题。

OpenSSH 的 `ssh_config` 手册将 `IdentitiesOnly` 描述为一个控制项。它会将公钥认证所使用的身份限制为配置的身份文件和证书，即使代理中保存着其他身份。应为每个有范围的主机别名设置该选项。给每个别名配置一个明确的身份文件，不要依赖代理碰巧提供密钥的顺序。

```sshconfig
Host dev-*
  User devops
  IdentityFile ~/.ssh/identities/id_ed25519_dev_alex
  IdentitiesOnly yes
  ForwardAgent no

Host stage-*
  User release
  IdentityFile ~/.ssh/identities/id_ed25519_stage_alex
  IdentitiesOnly yes
  ForwardAgent no

Host prod-*
  User admin
  IdentityFile ~/.ssh/identities/id_ed25519_prod_alex
  IdentitiesOnly yes
  ForwardAgent no
```

使用能让环境在终端历史中一目了然的别名。例如，当开发和生产环境都有名称相近的 API 主机时，`prod-api-01` 就比 `api-01` 更好。不要使用 `server` 这样的通用别名隐藏目标。

检查 OpenSSH 最终会采用的配置。这可以发现通配符冲突、被包含的文件，以及遗忘的全局设置。

```sh
ssh -G prod-api-01 | grep -E '^(hostname|user|identityfile|identitiesonly|forwardagent) '
```

输出应接近下面的形式：

```text
hostname prod-api-01.internal
user admin
identitiesonly yes
forwardagent no
identityfile ~/.ssh/identities/id_ed25519_prod_alex
```

路径在你的机器上可能会展开成不同形式。关键是生产别名只能解析到生产身份，并且 `identitiesonly` 显示为 `yes`。

有人为了方便运行 `ssh-add` 后，经常会出现一种故障。此时代理中已经保存了多个身份。没有 `IdentitiesOnly yes` 时，客户端会逐个向主机尝试这些身份，直到其中一个成功。服务器通常会限制认证尝试次数，因此可能造成令人困惑的失败。更糟的是，范围过宽的凭据可能悄悄让连接成功，操作人员甚至不会意识到自己使用了错误范围的身份。

## 服务器授权必须与拆分保持一致

只有在每台服务器接受匹配的公钥并拒绝其他公钥时，不同私钥才真正有效。将开发公钥放在开发主机上，将暂存公钥放在暂存主机上，只在确实需要生产访问的地方放置生产公钥。

这条规则听起来很明显，但在紧急设置期间，糟糕的做法很常见：有人把完整的 `authorized_keys` 文件复制到新主机。该文件中往往包含多年来积累的过期身份、前承包商身份、部署密钥和通用管理员密钥。新生产主机因此继承了没人审核过的访问决策。

应根据账户的工作内容建立授权列表。对于部署账户，使用适合非交互式传输或部署操作的 OpenSSH 限制。OpenSSH 的 `authorized_keys` 手册说明了 `restrict` 选项，它会关闭端口转发、代理转发、X11 转发和 PTY 分配，除非其他选项重新允许这些功能。受限的部署条目可以如下所示：

```text
restrict,from="198.51.100.0/24" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... prod-release-job
```

将示例网络替换为你实际控制的地址范围。不要仅仅因为 `from=` 看起来严格就添加它。如果任务来自不断变化的地址或托管运行器池，错误的源地址限制会导致中断，也会让人在压力下删除所有限制。

强制命令只应用于定义明确的自动化。强制命令可以阻止部署凭据获取 Shell，但它也会变成一份维护契约。命令必须验证输入，安全选择路径，并记录请求。不要将强制命令用于管理员账户后，就认为账户已经安全。人们迟早需要 Shell，并会设法绕过限制。

对于人工生产管理，单独的账户加单独的公钥，通常比复杂的 `authorized_keys` 选项更清晰。通过正常的主机权限限制账户，在适用的情况下记录其 sudo 活动，并在人员不再需要访问时删除公钥。

## 只有在声明范围狭窄时，SSH 证书才有帮助

OpenSSH 证书可以让短期访问更容易签发和管理撤销。证书颁发机构为用户公钥签名，服务器信任该机构，而不必保存每个个人公钥。这可以减少更新大规模主机集群的工作量。

它们并不会消除分离不同授权机构的需要。带有生产主体的证书，不应仅仅因为某个操作人员同时负责三个环境，就同时授予开发和暂存环境的访问权限。应签发不同证书，使用不同主体，或在管理和风险边界确有必要时使用不同的证书颁发机构。

OpenSSH 证书协议区分用户证书、主体、有效期和关键选项。只有服务器检查主体，并且签发规则保持狭窄时，这种结构才有用。一个对所有主机上的所有账户都有效的证书，只是把私钥变成了带有过期时间、被广泛信任的持有者身份。

较短的证书有效期有助于应对设备丢失和员工离职，但不能代替主动事件中的撤销。你仍然需要一种能够停止接受受损身份，或及时移除其授权的方法。在签发证书前规划好这套机制，并在真实主机上进行测试。

证书也不能解决代理误用问题。如果自动化进程每次请求都能获得生产证书，那么签发服务就是生产环境的边界。应像保护生产私钥一样保护它。

## 代理转发会跨越你看不见的边界

SSH 代理转发允许远程主机请求本地代理为其他主机的认证质询进行签名。远程机器不会收到私钥，但只要会话保持打开，运行在远程账户下的进程就可以使用被转发的代理。

因此，对于首先落到跳板机上的生产会话，代理转发尤其危险。如果跳板机遭到入侵，或不受信任的进程以该账户运行，它就可以请求你转发的代理所提供的每个身份进行签名。结果可能是横向访问，而你的原始连接计划从未提到这种可能性。

在客户端配置中保持以下默认值：

```sshconfig
Host *
  ForwardAgent no
```

只有在受维护的工作流程确实需要时，才创建一个明确的例外。在添加例外前，先问问自己，是否可以使用 ProxyJump、专用堡垒机凭据，或在本地执行操作来消除这个需求。ProxyJump 会让 SSH 连接经过中间主机传输，但不会像代理转发那样把本地代理暴露给中间主机。

不要接受「私钥仍然留在笔记本电脑上，所以转发是安全的」这种说法。一个签名请求入口就足以让攻击者在其他地方完成认证。这个区别在系统遭到入侵时很重要：保护文件不等于限制谁可以使用凭据。

## 审批范围必须匹配凭据范围

批准一个新进程使用 SSH 凭据时，审批应标明凭据所属的环境以及提出请求的进程。只显示 `ssh command requested` 的提示，会让审批者从过少的信息中推断过多内容。

团队经常在这里混淆两个控制。凭据范围回答的是某个身份可以在哪里认证，审批范围回答的是哪个运行中的进程可以使用该身份。两者都需要。不同的生产凭据可以防止开发进程意外触及生产环境，进程审批则可以防止未知的本地进程使用当前可用的生产凭据。

按命令审批听起来更安全，经历过不安事件后，人们往往会提出这种要求。但在日常维护中，它通常会失败，因为反复出现的提示会训练操作人员不阅读内容就批准。只有当凭据本身的使用需要人工决定时，才应保留每次使用确认，例如高影响力的生产环境管理身份。普通工作可以使用在进程退出时结束的会话授权，同时保持凭据在服务器端的范围狭窄。

Sallyport 将 SSH 凭据保存在加密保险库中，可以要求批准新的代理进程，也可以针对选定凭据在每次使用时审批。这不能取代服务器端的独立授权，但能在不把凭据交给代理的情况下，明确划分进程边界。

AI 编程代理比交互式终端需要更严格的隔离，因为它可以快速执行大量命令，也可能遵循错误指令。默认只给它开发环境访问权限。如果它需要暂存环境，就使用仅限暂存环境的身份，并运行一个单独获批的任务。把生产访问当作独立操作，指定明确目标和狭窄用途。

## 故障通常从一个看似无害的例外开始

假设某团队有一把 `ops` SSH 密钥，开发、暂存和生产主机都接受它。工程师为了进行生产维护，把这把密钥加载进 SSH 代理。后来，一个本地构建助手连接暂存主机来收集日志。

构建助手没有明确的 `IdentityFile` 设置，`IdentitiesOnly` 也没有配置。OpenSSH 尝试代理中的身份。共享的 `ops` 密钥认证成功，因为暂存环境接受它。此时，构建助手已经使用一个同时适用于生产环境的身份建立了暂存会话。

第二个错误随之发生。暂存主机启用了代理转发，因为上个月有人需要进行一次临时连接。该主机上的进程可以通过被转发的代理请求签名。它使用同一个 `ops` 身份访问生产主机。原工程师批准的是一次生产维护会话，但无关的构建助手和暂存主机继承了它的权限。

在这个过程中，不需要泄露私钥也能完成攻击。设计允许使用范围过宽的身份，客户端机会式地选择了它，代理转发又扩大了它的触及范围。日志可能一路显示有效认证，这也是团队会把问题误认为用户操作错误，而不是修复访问模型的原因。

拆分后的设计会在多个位置切断这条链。构建助手只能使用暂存凭据。生产环境拒绝该凭据。代理转发保持关闭。生产身份只能提供给明确获批的生产进程。上述任何一个控制都有帮助，结合起来则能让错误连接尽早失败。

## 轮换和紧急撤销需要经过演练的顺序

轮换的正确方式是先添加替代凭据，再移除旧身份，确认实际路径，最后撤销旧凭据。生产轮换之所以失败，往往是因为有人只从自己的笔记本电脑测试，而真正的部署器使用的是不同账户、网络或自动化运行器。

正常轮换可以按以下顺序进行：

1. 创建一个与旧凭据范围同样狭窄的替代凭据。
2. 将其公钥或证书授权添加到指定账户和主机。
3. 从真实进程路径执行真实命令进行测试，包括任何跳板机。
4. 移除旧授权，并确认旧凭据现在确实失败。
5. 在访问记录中记下替代凭据的指纹、负责人、范围和移除日期。

保留一条紧急访问路径，但不要把它变成复制到每台工作站上的第二个永久管理员凭据。将它单独保存，限制能够启用它的人，并在受控条件下进行测试。没人实际使用过的紧急访问流程，在中断期间只会变成慌乱的猜测。

紧急撤销与普通轮换不同。如果生产凭据可能已经暴露，应立即删除其公钥，或停止接受其证书，然后再进行替换。不要等到计划中的轮换窗口，因为攻击者不会遵守你的日程。代价是运营中断，这也正是狭窄凭据有价值的原因：撤销生产部署身份不应阻止开发工作。

## 测试被拒绝的访问，并查看证据

只有当错误凭据以一种你能够解释的方式失败时，访问隔离才算完成。每次重大变更后都进行一次有意的负面测试：使用开发身份访问生产主机，确认公钥认证失败。然后使用预期凭据测试，并检查连接记录中的账户名称和目标。

测试时使用详细的客户端输出，不要把它当作永久习惯：

```sh
ssh -vvv -o IdentitiesOnly=yes \\
  -i ~/.ssh/identities/id_ed25519_dev_alex \\
  admin@prod-api-01.internal
```

你应该看到客户端提供开发公钥，而服务器拒绝该公钥。不要未经检查就把这段输出粘贴到工单中，因为详细的 SSH 日志可能暴露主机名、用户名和认证信息。

服务器日志应该能够回答：谁完成了认证，使用了哪个账户，服务器接受了哪个公钥指纹，以及连接从哪里发起。如果多个人或进程共享一个身份，日志无法事后补回缺失的归属信息。

检查边界是否发生漂移：开发密钥被添加到生产账户，旧部署凭据仍然被接受，生产密钥被加载到通用代理中，或者通配符客户端规则覆盖了 `IdentitiesOnly`。这些变化经常以临时修复的形式出现。临时 SSH 访问往往会在制造它的紧急情况结束很久之后仍然存在。

先盘点生产主机接受的每个公钥，并为每个公钥标明负责人、进程、用途和范围。任何无法用这四项信息说明清楚的条目，都不应继续获得授权。
