安全指南

SSH 密钥的最小权限:别把生产的 root 密钥放进可能被入侵的临时环境

从 GPU 云 pod、CI runner 等可能被入侵的临时环境向生产服务器登记 root 的 SSH 密钥,一旦泄露就等于交出生产 root 权限。讲解用非 root 用户与 command 限制密钥实现最小权限的做法。

发布于 2026-06-11 更新于 2026-10-06 5 分钟阅读
本页目录

适用对象:从 GPU 云的 pod、CI runner、一次性 VM 这类临时环境通过 SSH 连到生产服务器的人。这里不讲攻击手法,只讲把密钥做成最小权限、让泄露时的损害更小。密钥分离的基础,请参考 安全底线清单。

本站的看法:把临时环境当作不可信任的环境

用完即弃的计算环境往往省掉加固,是可能被入侵的地方。在那里放上生产的 root 密钥,那个环境被入侵就等于生产被入侵。本站采用每台主机使用不同的密钥,限定每把密钥的用途,不再使用就立刻删除(→ 分开密钥把损害降到最小)。不把密钥当作「方便的工具」,而是当作「一旦泄露就能访问所有服务器的最重要资产」,最终是最安全的做法。

✗ 临时环境放 root 密钥

临时环境被入侵 → 用那把密钥以 root 进入生产 → 文件、机密、其他服务全部都能够到。

○ command 限制的非 root 密钥

同一把密钥即使泄露,也只能执行指定的那一条命令。没有 shell,也不能转发,损害只限于一个操作。

从一开始就让密钥能做的事尽量少。泄露时的损害会大不相同。

为什么临时环境的 root 密钥危险

临时环境常因「反正马上就删」而省掉加固,容易被攻击,也容易被当作攻击生产的中转点。一旦那里有生产的 root 密钥,损害就会达到最大。

root→全部
泄露的密钥能完全控制生产
用完即弃
临时环境省掉加固
复用
一把泄露暴露全部主机
最小权限
把密钥限定为一个操作,损害也随之受限

举个例子,你为了计算临时启动了一个 pod,为了从那里操作生产,把密钥登记进 root 的 authorized_keys。方便是方便,但那个 pod 一旦被入侵,攻击者就能用那把密钥以 root 权限进入生产。无论攻击始于 RCE 还是密钥泄露,最终结果都一样:生产被人用盗来的密钥夺走(相关:RCE 导致密钥被盗、被恶意扣费的故事)。

把密钥做成最小权限的 3 个步骤

从「图方便就交出全部权限」改成「把能做的事收窄到最小」。

1

删除临时环境的 root 密钥

把不再使用的密钥从生产的 ~/.ssh/authorized_keys 里删掉。处于闲置状态的话,删掉也没有影响。首先,不要把不用的密钥留在那里。
2

再次需要也不要回到 root

为生产作业重新登记时,新建一个专用的非 root 用户,把密钥登记给它。即使泄露,影响范围也仅限于该用户的权限。
3

把密钥限定为一个操作

在 authorized_keys 的密钥行上加 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@ci

restrict 会把转发、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 的专用用户上,生产采用「只接收、不主动外出拉取」的单向结构(→ 生产只负责接收)。不会从临时计算环境向生产留下常驻的密钥,只在需要时以最小权限连接。理由正如本文所述:预先让每把密钥能做的事尽量少,使一处被入侵不会扩散到全部。

接下来读

参考资料

  • 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

FAQ

Q为什么把 root 密钥放进临时环境很危险?
A

GPU 云的 pod、CI runner 这类临时环境用完即弃,加固往往被省略,所以可能被入侵。在那里放上通往生产的 root 密钥,那个环境一旦被入侵,攻击者就会连同密钥一起拿到生产的 root 权限。要把临时环境当作不可信任的环境,不在其中放生产的 root 密钥。

Q如果还是需要从临时环境连到生产呢?
A

不要回到 root。新建一个专用的非 root 用户,给它登记一把把能做的事限定为单一操作的密钥。在 authorized_keys 里加上 command 限制和 restrict 标志后,即使登录成功,也只能执行那一条命令。泄露时损害也只限于那一个操作。

Q密钥管理里最重要的原则是什么?
A

把复用的密钥当作最重要的资产,绝不搭出漏一把就暴露全部的结构。按主机、按用途分开密钥,不再需要的密钥就从 authorized_keys 里删掉。这样一把密钥的泄露就不会扩散到全部。

Q怎样确认服务器上登记了哪些 SSH 密钥?
A

对每个用户的 ~/.ssh/authorized_keys 运行 ssh-keygen -lf,就能列出所有已登记密钥的指纹(SHA256:...)和注释。包括 root 在内的所有账户都要检查,挑出说不清来源的密钥和已不再使用的临时环境的密钥。想知道哪些密钥实际在用,就把这些指纹与 sshd 写入日志的登录成功记录进行对照。

QPermitRootLogin 应该怎么设置?
A

OpenSSH 的默认值是 prohibit-password:root 不能用密码登录,但有密钥的人可以直接以 root 登录。如果不需要直接以 root 登录,就设为 no,从非 root 用户开始工作。如果备份等用途确实需要 root 密钥,forced-commands-only 只允许带有 command="..." 选项的密钥以 root 登录。

Q怎样安全地从 CI 或 GitHub Actions 通过 SSH 部署?
A

CI runner 是典型的临时环境,现实中也发生过 CI 中使用的工具被篡改、包括 SSH 密钥在内的机密被收集的案例。把部署密钥登记给专用的非 root 用户,并用 command="..." 和 restrict 把它限定为拉取/部署这一个操作。如果来源 IP 固定,再用 from="..." 收窄。每条流水线、每个环境使用不同的密钥;一旦有入侵公告,先删除旧密钥,再登记新密钥。