适用对象:从 GPU 云的 pod、CI runner、一次性 VM 这类临时环境通过 SSH 连到生产服务器的人。这里不讲攻击手法,只讲把密钥做成最小权限、让泄露时的损害更小。密钥分离的基础,请参考 安全底线清单。
本站的看法:把临时环境当作不可信任的环境
用完即弃的计算环境往往省掉加固,是可能被入侵的地方。在那里放上生产的 root 密钥,那个环境被入侵就等于生产被入侵。本站采用每台主机使用不同的密钥,限定每把密钥的用途,不再使用就立刻删除(→ 分开密钥把损害降到最小)。不把密钥当作「方便的工具」,而是当作「一旦泄露就能访问所有服务器的最重要资产」,最终是最安全的做法。
✗ 临时环境放 root 密钥
临时环境被入侵 → 用那把密钥以 root 进入生产 → 文件、机密、其他服务全部都能够到。
○ command 限制的非 root 密钥
同一把密钥即使泄露,也只能执行指定的那一条命令。没有 shell,也不能转发,损害只限于一个操作。
为什么临时环境的 root 密钥危险
临时环境常因「反正马上就删」而省掉加固,容易被攻击,也容易被当作攻击生产的中转点。一旦那里有生产的 root 密钥,损害就会达到最大。
举个例子,你为了计算临时启动了一个 pod,为了从那里操作生产,把密钥登记进 root 的 authorized_keys。方便是方便,但那个 pod 一旦被入侵,攻击者就能用那把密钥以 root 权限进入生产。无论攻击始于 RCE 还是密钥泄露,最终结果都一样:生产被人用盗来的密钥夺走(相关:RCE 导致密钥被盗、被恶意扣费的故事)。
把密钥做成最小权限的 3 个步骤
从「图方便就交出全部权限」改成「把能做的事收窄到最小」。
删除临时环境的 root 密钥
~/.ssh/authorized_keys 里删掉。处于闲置状态的话,删掉也没有影响。首先,不要把不用的密钥留在那里。再次需要也不要回到 root
把密钥限定为一个操作
command="..." 和 restrict。即使登录成功,也只能执行指定的那一条命令,端口转发和 shell 都被禁用。泄露时损害只限于那一个操作。# Register a key restricted to ONE operation in a non-root user's authorized_keys.
# Logging in with it can run only the given command; forwarding and PTY are off.
command="/usr/local/bin/deploy-pull",restrict ssh-ed25519 AAAA...rest-of-public-key deploy@cirestrict 会把转发、agent 转发、PTY、X11 等一并禁用,command="..." 则把可执行的操作固定为一个。基本做法是按用途分别创建限定为单一目的的密钥,例如只允许拉取部署、只允许运行某个脚本。
常见的结构(危险)
- 从 pod/CI 把密钥登记到生产的 root
- 用完的密钥放着不删
- 一把密钥在多台主机上复用
- 到处散布能拿到完整 shell 的密钥
最小权限的结构
- 只以非 root 用户连到生产
- 密钥限定为一个操作(command 限制)
- 按主机和用途分开密钥
- 不再使用的密钥立刻从 authorized_keys 删除
把复用的密钥当作最重要的资产
把密钥当成「方便的工具」,就会复用、忘记删除。应该把它当作一旦泄露就能访问所有服务器的最重要资产。按主机和用途分开,不在临时环境放生产的密钥,用完后一定删除。仅此就能防止一把密钥的泄露扩散到全部。
可以加在密钥行上的主要限制
在 OpenSSH 中,写在 authorized_keys 密钥行开头的选项可以收窄这把密钥能做的事。常用的有:
| 选项 | 限制内容 | 使用场景 |
|---|---|---|
command="..." | 用这把密钥登录时只执行指定的命令,客户端发送的命令被忽略 | 只做一件固定工作的密钥,例如拉取部署或做备份 |
restrict | 一并禁用端口转发、agent 转发、X11 转发、PTY 分配以及 ~/.ssh/rc 的执行(OpenSSH 7.2+) | 几乎所有自动化用的密钥;只把需要的功能重新打开,例如 pty |
from="..." | 除了密钥之外,客户端的主机名或 IP 地址也必须与列表匹配 | 来源 IP 固定的 CI 或作业服务器 |
expiry-time="..." | 超过指定日期时间后不再接受这把密钥(OpenSSH 7.7+) | 只用于临时作业的密钥 |
PermitRootLogin(在 sshd_config 中) | 是否允许直接以 root 登录;forced-commands-only 只允许带 command= 的密钥以 root 登录 | 除非确实需要直接 root 访问,否则设为 no |
OpenSSH 的发布说明把 restrict 描述为包含「所有当前和未来的密钥限制」。与其逐个列出 no-pty、no-port-forwarding 等,不如加上 restrict 后只重新打开需要的功能,这样以后出现新功能时也不会有遗漏。
# Also narrowing the source and the lifetime (the IP is a documentation address)
from="192.0.2.10",expiry-time="20261231",command="/usr/local/bin/deploy-pull",restrict ssh-ed25519 AAAA...rest-of-public-key deploy@ci-2026-10如果要在大量密钥上管理有期限的访问,OpenSSH 证书也是一个选择(用 ssh-keygen -s 签名,用 -V 设定有效期,用 force-command 或 source-address 加以限制)。规模较小时,从密钥行上的选项开始就足够了。
如何检查自己的环境
下面的检查只读取你自己管理的服务器的设置和日志。修改设置时,保持一个 SSH 会话不关,从另一个终端测试新连接,确认不会被锁在外面之后再关闭旧会话。
1. 列出已登记的密钥
# Show the fingerprint and comment of each key registered for root and each user
sudo ssh-keygen -lf /root/.ssh/authorized_keys
sudo ssh-keygen -lf /home/deploy/.ssh/authorized_keys每把密钥输出一行,例如 256 SHA256:... deploy@ci (ED25519)。根据注释(末尾的 deploy@ci 部分),挑出来源不明的密钥、已经用完的临时环境的密钥,以及登记给 root 的密钥。旧文件名 authorized_keys2 默认也会被读取,如果存在,也用同样方法检查。
2. 确认 sshd 实际使用的配置
# Check the syntax, then print the settings actually in effect
sudo sshd -t
sudo sshd -T | grep -iE 'permitrootlogin|authorizedkeysfile|pubkeyauthentication|passwordauthentication'sshd -T 会输出读取配置文件之后的实际生效值。如果 permitrootlogin 是 yes 或 prohibit-password,有密钥的人就能直接以 root 登录。如果 authorizedkeysfile 指向默认位置以外的地方,要把那个位置也加到第 1 步里。
3. 查看哪些密钥实际在用
用密钥登录成功后,sshd 通常会记录这样一行:Accepted publickey for deploy from 192.0.2.10 port 50022 ssh2: ED25519 SHA256:...。在 Debian 和 Ubuntu 上查看 /var/log/auth.log,在 Red Hat 系统上查看 /var/log/secure,使用 systemd 的主机则用 journalctl -u ssh(Red Hat 系统为 -u sshd)。
把这些指纹与第 1 步的列表对照,就能知道哪些密钥没有被使用。NIST 关于 SSH 密钥管理的报告(NISTIR 7966)建议在删除未使用的密钥之前观察日志大约 6 到 12 个月,以免误删很少使用的密钥。已经确定用完的密钥,例如临时环境的密钥,可以立刻删除。
4. 检查临时环境一侧
- CI 的 secrets 或 GPU pod 的
~/.ssh里,是否放着能连到生产的私钥? - 是否在连到生产的途中用
ssh -A(agent 转发)进入临时环境? - 是否有一把密钥被多个 CI 系统或服务器共用?
常见错误及修正方法
- 只删除临时环境一侧的私钥就结束:如果私钥已经被复制,只要服务器上还留着公钥,它就仍然有效。一定要从生产的
authorized_keys中删除对应的行。 - 在
command=调用的脚本里直接执行客户端发来的命令(SSH_ORIGINAL_COMMAND环境变量):这等于没有限制。只有当这个值与一组固定选项中的某一个完全一致时才使用它。 - 改用非 root 用户,却允许不受限制的 sudo:如果这个用户无需密码就能 sudo 任何命令,实际上就等于 root。如果需要 sudo,在 sudoers 中把它限定为一条命令。
- 用
ssh -A进入临时环境:在那里拥有足够权限的人,可以通过转发的连接使用你本地 agent 中加载的密钥(OpenSSH 手册对此有警告)。如果需要经由某台主机连接,使用ssh -J(经由跳板主机连接)。 - 密钥不加注释:检查时就无法判断它从哪里来。创建时写上用途和日期,例如
ssh-keygen -C "deploy@ci-2026-10"。 - 多人或多个环境共用一把密钥:NISTIR 7966 也要求禁止共享私钥或把私钥复制到其他系统。这样无法追踪谁在哪里使用了它,一次泄露就要在所有地方更换。
真实案例:被篡改的 CI 工具收集的东西里包括 SSH 密钥
2026 年 3 月,开源漏洞扫描器 Trivy 出现了恶意版本,其 GitHub Actions 的标签也被改写。据开发方 Aqua Security 称,恶意代码从 CI runner 收集凭据并发送出去,目标包括 ~/.ssh 中的私钥、authorized_keys、known_hosts 以及 SSH 主机密钥。Aqua 要求可能运行过受影响版本的用户,把该流水线能读取的所有机密(包括 SSH 密钥)都当作已泄露,并立即更换(详情:Trivy 供应链入侵事件(2026 年 3 月))。
在 2021 年的 Codecov 事件中,CI 中运行的脚本也在源头被篡改,大约两个月间,客户 CI 环境中的机密被发送出去(→ Codecov 供应链攻击)。
这两个案例都说明,即使自己的代码没有问题,CI 里的密钥也可能被拿走。如果被拿走的密钥登记在生产的 root 上,生产就在那一刻被交了出去。如果使用专用的非 root 用户、command= 加 restrict,并用 from= 收窄来源,同一把泄露的密钥就只能做你定义的那一件事,来自其他地方的连接也会被拒绝。
本站是怎么做的
本站每台服务器使用专用的密钥,不复用密钥。部署落在非 root 的专用用户上,生产采用「只接收、不主动外出拉取」的单向结构(→ 生产只负责接收)。不会从临时计算环境向生产留下常驻的密钥,只在需要时以最小权限连接。理由正如本文所述:预先让每把密钥能做的事尽量少,使一处被入侵不会扩散到全部。
接下来读
- 新漏洞:OpenSSH 10.6p2 修复了无 PAM 的 musl Linux 上 sshd 的密码认证绕过问题(CVE-2026-123158)
- 基础:安全底线清单(最小权限、保护管理账户)
- 运营:分开密钥把损害降到最小/生产只负责接收 · 漏洞应对的实务
- 案例:Trivy 供应链入侵(CI 中的 SSH 密钥成为目标) · Codecov 供应链攻击
- 事故:RCE 导致密钥被盗、被恶意扣费的故事 · Benesse 个人信息泄露事件(过度权限与内部作案)
参考资料
- OpenBSD 手册「sshd(8)」AUTHORIZED_KEYS FILE FORMAT(command、restrict、from、expiry-time、SSH_ORIGINAL_COMMAND):man.openbsd.org
- OpenBSD 手册「sshd_config(5)」PermitRootLogin 与 AuthorizedKeysFile:man.openbsd.org
- OpenBSD 手册「ssh(1)」-A(agent 转发的注意事项)与 -J:man.openbsd.org
- OpenBSD 手册「ssh-keygen(1)」-l、-C 与证书选项:man.openbsd.org
- OpenSSH 7.2 发布说明(新增 restrict):openssh.org
- OpenSSH 7.7 发布说明(新增 expiry-time):openssh.org
- NIST「NISTIR 7966: Security of Interactive and Automated Access Management Using Secure Shell (SSH)」(2015 年 10 月):csrc.nist.gov
- Wikibooks「OpenSSH/Logging and Troubleshooting」(登录成功日志中的密钥指纹):en.wikibooks.org
- Aqua Security「Trivy Supply Chain Attack: What You Need to Know」(2026 年):aquasec.com
- Codecov「Bash Uploader Security Update」(2021 年):about.codecov.io