跳转至

使用 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:

brew install gnupg pinentry-mac yubikey-manager

配置 ~/.gnupg/gpg-agent.conf

enable-ssh-support
pinentry-program /opt/homebrew/bin/pinentry-mac

重启 agent:

gpgconf --kill gpg-agent
gpgconf --launch gpg-agent

查看 SSH socket:

gpgconf --list-dirs agent-ssh-socket

用这个 socket 检查 agent 暴露出来的 SSH key:

SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" ssh-add -l -E sha256

导出准备允许 sudo 使用的公钥:

SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" ssh-add -L > yubikey-sudo-authorized_keys

真实使用时,只保留明确要授权给 sudo 的那一把公钥。不要把 agent 当前暴露的所有 key 都无脑授权。

远端路线一:pam_ssh_agent_auth

有些发行版直接打包了 pam_ssh_agent_auth。如果能从发行版仓库安装,这是最清爽的路线。

安装:

sudo dnf install 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_execssh-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

在原密码认证行前加入:

auth       sufficient  pam_exec.so quiet /usr/local/sbin/yubikey-sudo-check
auth       include     system-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。

回滚

如果行为不符合预期:

  1. 恢复之前备份的 /etc/pam.d/sudo
  2. 删除或禁用 sudoers 环境变量片段;
  3. 保持普通密码 sudo 可用;
  4. 新开 SSH session 测试通过后,再关闭旧 session。

PAM 错误可能导致无法提权。把它当作生产认证变更处理:小步推进,一次一台主机,并且准备一条无聊但可靠的回滚路径。

参考