使用 YubiKey OpenPGP 和 SSH Agent Forwarding 做远程 sudo 认证¶
这篇文章记录一种我比较喜欢的远程 Linux 管理方式:
- 普通 SSH 登录继续使用原来的 SSH key、密码管理器或证书;
- sudo 认证使用本地 YubiKey 上的 OpenPGP authentication 子钥;
- 远端主机只验证转发过来的 SSH agent,不保存私钥;
- 原 sudo 密码仍然作为 fallback 保留。
这不是把工作站上所有本地 sudo 都替换成 YubiKey。它只是一个远程维护模式:当你已经登录到可信服务器之后,再通过触摸本地 YubiKey 来批准远端的 sudo。
为什么要分开 SSH 登录和 sudo¶
SSH 登录和 sudo 解决的是两个不同问题。
SSH 登录问的是:
这个用户能不能进入服务器会话?
sudo 问的是:
这个已经登录的用户,现在能不能提权?
把两层分开有一个很实用的好处:你可以保持现有 SSH 登录方式不变,只给管理操作增加一个硬件确认步骤。
例如:
- SSH 登录继续使用现有 SSH agent、密码管理器、证书或普通 key。
- sudo 通过转发的
gpg-agent要求 YubiKey 做签名。 - 如果 YubiKey 暂时不可用,原 sudo 密码 fallback 仍然可用。
架构¶
核心思路是:让 gpg-agent 以 SSH agent 的形式暴露 YubiKey 上的 OpenPGP authentication 子钥。
本地工作站
YubiKey OpenPGP auth 子钥
gpg-agent SSH socket
|
| 只对本次会话开启 SSH agent forwarding
v
远端 Linux 服务器
sudo PAM 规则
验证转发过来的 agent 是否能用允许的公钥完成签名
|
v
成功:sudo 认证通过
失败:继续走原来的密码认证
私钥留在 YubiKey 上。远端服务器只保存允许用于 sudo 的公钥,或者保存它的引用。
本地配置¶
先安装 GnuPG,并开启 gpg-agent 的 SSH 支持。
macOS + Homebrew:
配置 ~/.gnupg/gpg-agent.conf:
重启 agent:
查看 SSH socket:
用这个 socket 检查 agent 暴露出来的 SSH key:
导出准备允许 sudo 使用的公钥:
真实使用时,只保留明确要授权给 sudo 的那一把公钥。不要把 agent 当前暴露的所有 key 都无脑授权。
远端路线一:pam_ssh_agent_auth¶
有些发行版直接打包了 pam_ssh_agent_auth。如果能从发行版仓库安装,这是最清爽的路线。
安装:
把允许用于 sudo 的公钥放到服务器上:
sudo install -o root -g root -m 0600 yubikey-sudo-authorized_keys \
/etc/security/authorized_keys_sudo_yubikey
允许 sudo 保留转发过来的 agent socket:
sudo tee /etc/sudoers.d/10-yubikey-agent-auth >/dev/null <<'EOF'
Defaults env_keep += "SSH_AUTH_SOCK"
EOF
sudo chmod 0440 /etc/sudoers.d/10-yubikey-agent-auth
sudo visudo -cf /etc/sudoers.d/10-yubikey-agent-auth
然后在 /etc/pam.d/sudo 的常规密码认证行前面加入一行 sufficient:
auth sufficient pam_ssh_agent_auth.so file=/etc/security/authorized_keys_sudo_yubikey
auth include system-auth
sufficient 很关键。YubiKey 认证成功时,sudo 直接通过;如果失败,PAM 会继续走原来的密码认证。
远端路线二:pam_exec 和 ssh-add -T¶
在一些较新或较精简的系统上,仓库里可能没有 pam_ssh_agent_auth。这时可以用 pam_exec 加 ssh-add -T 作为替代。
ssh-add -T <public-key-file> 会让 SSH agent 对测试数据签名,并用文件里的公钥验证签名。如果转发过来的 agent 能使用 YubiKey 上的对应 key,测试就会成功。
创建一个 root 拥有的小检查脚本:
sudo tee /usr/local/sbin/yubikey-sudo-check >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
key_file="/etc/security/authorized_keys_sudo_yubikey"
if [[ -z "${SSH_AUTH_SOCK:-}" || ! -S "${SSH_AUTH_SOCK}" ]]; then
exit 1
fi
if [[ ! -r "$key_file" ]]; then
exit 1
fi
exec /usr/bin/ssh-add -T "$key_file"
EOF
sudo chown root:root /usr/local/sbin/yubikey-sudo-check
sudo chmod 0755 /usr/local/sbin/yubikey-sudo-check
公钥和 sudoers 环境变量规则仍然按第一条路线配置:
sudo install -o root -g root -m 0600 yubikey-sudo-authorized_keys \
/etc/security/authorized_keys_sudo_yubikey
sudo tee /etc/sudoers.d/10-yubikey-agent-auth >/dev/null <<'EOF'
Defaults env_keep += "SSH_AUTH_SOCK"
EOF
sudo chmod 0440 /etc/sudoers.d/10-yubikey-agent-auth
sudo visudo -cf /etc/sudoers.d/10-yubikey-agent-auth
在原密码认证行前加入:
这不如 pam_ssh_agent_auth 那样专用,但运维形态一致:agent 验证成功就通过,失败则继续走密码认证。
SSH 命令模式¶
不要给所有主机开启全局 agent forwarding。
只在可信的远端 sudo 会话中显式转发 YubiKey agent:
ssh -tt \
-o ForwardAgent="$(gpgconf --list-dirs agent-ssh-socket)" \
admin@example-host \
'sudo -k -v'
普通 SSH 登录保持原配置。例如你已经在使用密码管理器或另一个 SSH agent 登录,就继续保持原样。
两类 agent 的职责不同:
- 登录 agent:打开 SSH 会话;
- YubiKey
gpg-agent:批准远端 sudo。
验证¶
验证硬件 sudo 路径:
ssh -tt \
-o ForwardAgent="$(gpgconf --list-dirs agent-ssh-socket)" \
admin@example-host \
'sudo -k -v && echo YUBIKEY_SUDO_OK || echo YUBIKEY_SUDO_FAILED'
验证密码 fallback:
ssh -tt \
-o ForwardAgent=no \
admin@example-host \
'sudo -k -v && echo PASSWORD_FALLBACK_OK || echo PASSWORD_FALLBACK_FAILED'
第二条命令应该要求输入普通 sudo 密码。如果没有提示密码,需要停下来检查是不是不小心做成了免密 sudo。
安全注意事项¶
Agent forwarding 是强能力。可信远端的 root 进程在会话打开期间,可以请求转发的 agent 做签名操作。私钥仍然不会复制到服务器,但被转发的 socket 本身就是一种能力。
我的实践规则是:
- 只把 YubiKey agent 转发到可信主机;
- 按命令或按主机开启,不做全局开启;
- 在 break-glass 路径验证前保留密码 fallback;
- 为 sudo 使用专用的允许公钥文件;
- 首次上线时 PAM 使用
sufficient; - 修改 PAM 或 sudoers 时保留当前 SSH session;
- 关闭维护窗口前同时验证硬件认证和密码 fallback。
回滚¶
如果行为不符合预期:
- 恢复之前备份的
/etc/pam.d/sudo; - 删除或禁用 sudoers 环境变量片段;
- 保持普通密码 sudo 可用;
- 新开 SSH session 测试通过后,再关闭旧 session。
PAM 错误可能导致无法提权。把它当作生产认证变更处理:小步推进,一次一台主机,并且准备一条无聊但可靠的回滚路径。