刚接触服务器运维那会儿,我最头疼的就是 SSH 登录安全。密码简单一点怕被暴力破解,密码复杂一点自己都记不住,后来索性把 SSH 端口改掉,但心里其实清楚这只能防住一部分扫描器。真正让我下定决心做双因素认证的,是有一次在一台公网服务器上看到 auth.log 里密密麻麻的认证失败记录,其中一个 IP 在短短一个小时内尝试了上千次密码组合。密码这东西,只要用户有权限访问服务器,就总有被猜中的风险,而动态验证码能把风险窗口压缩到几十秒。这篇文章我会完整拆解如何在 SSH 登录中集成 Google Authenticator 的 PAM 模块,从安装、配置、验证到排障,把这套双因素方案落地。
这套方案适合谁?正在管理 Linux 服务器的运维工程师、自己搭了 VPS 但又担心账户安全的个人开发者,还有那些被公司安全合规要求压着需要补强登录认证的团队。我会尽量把原理说清楚,把每一步操作背后的逻辑讲透,同时附上我自己踩过的坑,让你照着做就能上线。
1. 为什么要给 SSH 加一道锁
1.1 密码登录的痛点
很多人觉得只要把密码设得够复杂就安全了,这个思路在 SSH 场景下其实有很大的问题。你可以登录服务器看一下日志,公网上的扫描器从来不会因为你的密码有大小写加特殊字符就放弃尝试,它们用字典、用历史泄露的密码库、用各种组合规则,一天跑几万次都是家常便饭。更麻烦的是,团队里总有人喜欢用简单密码,或者把密码写在某个共享文档里,一旦泄露,你的服务器就等于门户大开。
密钥登录比密码强一些,因为私钥的熵值远超任何人类记忆的密码,但这也不是万无一失。你没法保证每个开发者的私钥都保存在足够安全的环境里,笔记本被拿走、私钥误传到公共仓库这类事,我在行业里见过不止一次。所以单纯依赖某一种认证因素,本质上都是把鸡蛋放在一个篮子里。
双因素认证的意思,就是让用户证明自己同时拥有"我知道的东西"(密码或者私钥)和"我拥有的东西"(手机),两条独立的认证链路都会被验证。即便密码泄露了,入侵者没有手机上的动态验证码,还是进不去。
1.2 PAM 方案的优势
Linux 下做双因素认证,主流的做法就是在 PAM(Pluggable Authentication Modules)框架里挂一个认证模块。PAM 是 Linux 登录认证的通用接口,无论是本地登录、SSH 登录还是 su/sudo,最终都会经过 PAM 层的调度。挂了 google-authenticator 这个模块后,系统在响应 SSH 认证时就会额外要求用户输入动态验证码。
相比直接在应用层面改代码,这个方案有三个明显的好处。一是接入成本低,不需要改 SSH 服务端源码,也不用给客户端装额外软件,用户手机上的 Google Authenticator、Authy、LastPass Authenticator 这些标准 TOTP 应用都能用。二是覆盖面广,只要 PAM 参与认证的入口都可以复用这个模块,比如还可以结合 sudo 做二次认证,一套配置多处受益。三是可回滚,把配置文件里的那一行注释掉就恢复到原来的认证方式,排查问题时非常方便。
这里补充一句,我配置的是 PAM 体系中最常用的一套流程,不同发行版在 PAM 文件的组织方式上会有差异,但核心概念完全一致,你理解了之后切到任何系统都不会慌。
2. 环境准备与工具安装
2.1 环境要求与安装命令
我这边用的测试环境是主流 Debian 系的 Linux 发行版,装的是libpam-google-authenticator这个包。如果你是 RHEL/CentOS 系列,包名通常叫google-authenticator-libpam,一般可以从 EPEL 源里装上。不管哪个发行版,底层那个程序都是同一个:google-authenticator。
安装命令非常简单:
# Debian/Ubuntu 系 apt update && apt install -y libpam-google-authenticator # RHEL/CentOS 系(需要先配置 EPEL 源) yum install -y google-authenticator-libpam装完后你会得到两个核心组件:一个是命令行工具google-authenticator,用来为用户生成密钥和应急码;另一个是 PAM 模块文件pam_google_authenticator.so,位于系统的安全模块目录下。安装完成后可以顺手确认一下模块文件在不在:
find /lib /usr/lib -name "pam_google_authenticator.so" 2>/dev/null有一点需要提前说明:同一台服务器上的每个系统用户(比如 root、普通用户)都需要单独执行一次google-authenticator来生成属于自己的密钥文件,这个文件默认存在用户主目录下,叫.google_authenticator。所以这个方案不是"装完包所有人自动就有验证码",而是每个用户都要主动绑定一次手机。
2.2 TOTP 原理速览
要去配置它,最好先理解它背后那个算法。Google Authenticator 生成的是标准的 TOTP(Time-based One-Time Password,基于时间的一次性密码)。核心逻辑是这样的:客户端和服务端预先共享一个密钥(也就是二维码里编码的那个 secret),然后以当前 Unix 时间戳整除一个时间步长(默认 30 秒)得到一个计数器值,再对这个值做 HMAC-SHA1 运算,最终截取一段给用户显示。
理解了这一点,你就知道配置里最关键的参数是什么了——时间同步。因为双方都是拿当前时间在算验证码,如果服务器时间和手机时间偏差超过一个时间窗口,验证码就会频繁失效。所以生产环境务必开启 NTP 时间同步,这是很多"验证码明明输对却一直报错"案例的真正根源。
TOTP 还支持一个"时间窗口容差"参数,PAM 模块默认允许前后一个时间窗口的误差。所以偶尔偏差几秒不影响使用,但偏差超过 30 秒就会开始出问题。这个容差在配置中可以通过参数调整,不过除非测试环境,我一般不建议随便放大窗口,窗口越大,暴力猜验证码的成功概率就越高。
3. 核心配置:从生成密钥到生效
3.1 生成用户密钥:google-authenticator 交互详解
首先要切到你想启用双因素认证的用户身份去执行生成命令。比如要给普通用户dev配置,就执行:
su - dev google-authenticator如果是给 root 配置,直接以 root 身份执行即可。交互过程中它会连续问几个问题,我逐一说明该怎么回答以及为什么这么选。
第一个问题问你是否要使用基于时间的一次性密码,也就是 TOTP。这里选y。另一个选项是 HMAC-based(基于计数器的方式),需要客户端和服务器端各自维护一个计数器,使用体验不如时间戳方案,绝大多数场景都选 TOTP。
接着终端会输出一个巨大的二维码图案,下面还会显示一段密钥字符串和一个验证码。用手机上的 Authenticator 应用扫码或者手动输入那段密钥,手机上就会出现每 30 秒刷新一次的六位数字。
随后它会更新配置文件.google_authenticator,然后问是否要更新此文件,选y。紧接着问你是否禁止同一验证码多次使用,这里我也建议选y。这个选项的作用是防止"重放攻击"——即使验证码被截获了,也不能再次用来登录,代价是服务器需要在文件里记录最近使用过的时间戳。我见过有人为了省这么点磁盘读写而选n,实在没必要,安全收益是实打实的。
再往下它会问是否要允许时间偏移(允许前后一个验证码窗口的缓冲),默认是y,我建议保留默认。如果你服务器的 NTP 偶尔有偏差,这个缓冲能救命。最后它会为你生成 5 个一次性应急验证码,这些码是打印在终端里的,每张只能用一次,请务必抄下来放在安全的地方。真到了手机丢了的那一天,这 5 个码能让你还能进服务器。
整个过程走完后,用户的~/.google_authenticator文件就写好了,权限默认是 600。你可以cat查看一下文件内容,里面第一行是密钥,后面几行是刚才那些应急码的哈希值。配置完整无误。
3.2 修改 PAM 配置
这一步是整个方案的核心,目的是把pam_google_authenticator.so挂到 PAM 认证链里。不同发行版的配置位置不一样,Debian 系通常在/etc/pam.d/sshd,RHEL 系你可能还需要额外检查/etc/pam.d/sshd里的auth段是 include 了哪个公共文件。
我的建议是直接编辑/etc/pam.d/sshd,在其中添加一行。在 Debian 系的默认配置里,文件顶部通常有一段被注释掉的@include common-auth相关说明。最稳妥的方式是找到auth开头的行,把下面的内容加到合适的位置:
auth required pam_google_authenticator.so nullok注意我加了nullok参数。它的意思是,如果某个用户还没有生成.google_authenticator密钥文件,就跳过这一步,不阻断登录。这个参数在调试阶段、以及给多用户团队逐步分批启用的时候非常重要。如果没有nullok,任何没绑定过验证码的用户都会被直接拒绝登录,在团队里很容易造成"怎么突然谁都登不上了"的恐慌。
等到你想强制所有人都启用双因素时,把nullok去掉就行,这样没有密钥文件的用户将无法登录。
有一点要特别提醒:PAM 行的顺序是有讲究的。SSH 登录时,PAM 会按顺序逐条处理认证规则。如果你把pam_google_authenticator.so放在密码认证之前,系统会先问验证码再问密码;放在后面则先验证密码。我一般放在 sshd 文件的auth区段中相对靠前的位置,或者放在@include common-auth后面,保证密码和验证码都会被验证。不同发行版默认的 PAM 文件内容差异很大,如果你看到文件里是@include common-auth这种写法,也可以直接在@include common-auth之后追加那行配置,效果一样的。
3.3 修改 sshd_config
PAM 配置加好了,但这还不够,因为 SSH 服务默认不一定启用了键盘交互式认证(KbdInteractiveAuthentication),而 PAM 回调恰恰依赖这个机制。你需要编辑/etc/ssh/sshd_config:
KbdInteractiveAuthentication yes ChallengeResponseAuthentication yesKbdInteractiveAuthentication是新的 OpenSSH 版本中的正式名称,老版本里叫ChallengeResponseAuthentication,为了保险,我在新旧两种格式里都写上。有些发行版默认就是yes,但还是建议检查一下。
开启之后,建议直接设置强制双因素认证的组合策略。在生产环境里,我强烈推荐使用AuthenticationMethods来显式规定认证顺序,而不是仅依赖 PAM 顺序。例如,要求公钥和验证码双重验证:
AuthenticationMethods publickey,keyboard-interactive:pam这个语法的含义是:用户必须先完成公钥验证,再完成 PAM 层的键盘交互认证(也就是输入 TOTP 验证码),两者都通过才算认证成功。这样即使有人偷到了私钥,没有你的手机验证码还是进不来。
如果你的团队仍然在用密码而非公钥登录,那可以把publickey换成password,写成password,keyboard-interactive:pam。不过最稳妥的组合还是公钥 + 验证码,因为密码本身有被猜测的风险,公钥没有。
改完 sshd_config 后,在修改生效前一定要做两件事。第一,用sshd -t检查配置文件语法,语法错误会直接在控制台报出来。第二,不要贸然重启 SSH 服务,而是先一个新窗口保持一个旧会话不退出,再重启,这样即使配置有误导致新的连接全部失败,你也还保留着一个活动会话可以回滚修改。我这里说的不是危言耸听,很多人是在远程服务器上配 SSH 被锁在门外的。
sshd -t systemctl reload sshd我用的是reload而不是restart,前者只重载配置、不断开已有连接,风险小得多。
4. 验证登录与多因素组合
4.1 完整的登录流程
配置完毕后,新开一个终端窗口,正常发起 SSH 连接。如果你配置的是publickey,keyboard-interactive:pam,连接过程会是这样:
- SSH 客户端先用自己的私钥完成公钥验证。
- 服务端返回一个键盘交互式认证请求,提示输入验证码。
- 你打开手机,输入当前六位验证码,回车。
- 进入系统。
如果你用 Xshell 或者 mobaXterm 这类终端工具,在键盘交互阶段界面会直接弹出一个输入验证码的框,体验跟密码登录差不多。如果用标准 OpenSSH 客户端,提示通常会以Verification code:的形式出现,直接输入就行。
在服务端,你可以通过查看日志确认这次登录是否走了双因素:
grep sshd /var/log/auth.log | tail -20正常的话会看到Accepted publickey以及后续的键盘交互认证成功的记录。如果验证码输入错误,日志里会出现pam_google_authenticator相关的失败信息。我在配置完成后的第一件事,就是故意输错一次验证码,确认失败日志确实产生了,再正确输入一次,确认成功。这一步能帮你快速判断模块是否真的挂在认证链上,而不是配置了个寂寞。
4.2 更严格的组合策略
AuthenticationMethods publickey,keyboard-interactive:pam只是一道组合,实际上这个字段支持的组合方式很灵活,可以按需收紧或者放宽。我个人比较推荐的一套组合是为普通用户和特权用户分别设置不同的认证策略。
例如,对于普通用户,允许公钥加验证码;对于 root 用户,直接禁止 SSH 远程登录,要求先登录普通用户再 su 或 sudo 切换到 root。这种做法的好处是把最重要的管理入口从网络层直接封死,即便双因素被绕过,攻击者也没有直接获得 root 的方式。
如果你正在管理一批服务器,不想手工逐个修改配置,建议把 sshd_config 和 PAM 的这几处改动放进配置管理工具里,用模板统一分发。这样新增服务器时,一次性就能把双因素策略铺下去。对于团队来说,还要准备好"用户忘记手机怎么办"的预案:临时关闭某台机器的认证模块、或者使用用户那 5 个应急验证码都是可行方案,关键是预案要提前定好,别等出事了再开会。
另外还有一点值得提:你可以再让这个模块保护sudo命令。做法是编辑/etc/pam.d/sudo,同样加上那一行auth required pam_google_authenticator.so。这样即使有人已经进入了你的服务器,执行 sudo 时还得再验证一次动态密码。这个方案我在某些安全性要求较高的环境中用过,虽然确实会增加操作成本,但对于有合规要求的场景,非常有用。
5. 常见问题与排查实录
5.1 排查速查表
我在实际配置过程中遇到过不少问题,也帮团队排查过一些类似情况,整理成一张表,你可以对照排查。
| 症状 | 常见原因 | 解决办法 |
|---|---|---|
| 验证码明明正确却提示失败 | 服务器时间与手机时间偏差过大 | 检查 NTP 状态,timedatectl status,同步时间后重试 |
| 连接时根本没有输入验证码的环节 | sshd_config 未启用 KbdInteractiveAuthentication | 确认该参数为 yes,并 reload sshd |
| 输完密码直接报错,验证码都没问 | PAM 行序不正确,auth 链提前失败 | 检查/etc/pam.d/sshd,把模块行放在正确位置 |
| 未配置的用户被拒绝登录 | 加了模块行但忘了nullok参数 | 添加nullok,允许未绑定用户通过 |
| 扫码后手机验证码一直错误 | secret 录入不完整或粘贴多了空格 | 重新执行 google-authenticator,重新扫码绑定 |
| 重启 sshd 后所有连接都失败 | sshd_config 语法错误 | 用sshd -t检查,通过已有会话回滚配置 |
| 修改 PAM 后本地登录也要求验证码 | PAM 模块被挂到了全局认证链 | 确认@include common-auth是否有被修改,还原 |
重点说一下第一行的 NTP 问题。我的经验是,一台刚开通的服务器如果不装 NTP 相关服务,时间偏差可能超过几分钟,这种情况下 TOTP 必然失效。修时间是所有镜像安装完毕后的第一件事:
timedatectl set-ntp true timedatectl status确认System clock synchronized: yes,再去做二次验证就会稳定很多。
5.2 几个让我印象深刻的坑
第一个坑是 OpenSSH 版本差异。早期版本用的是ChallengeResponseAuthentication,新版改成了KbdInteractiveAuthentication,很多人照着老教程配好之后怎么都不生效,问题就出在这个参数名上。我现在的习惯是干脆把两个参数都写进配置,为了兼容老版本,这样不管系统用的是哪个版本的 OpenSSH,都能覆盖。
第二个坑是 PAM 的required与sufficient的区别。如果我用了sufficient,那么一旦验证码被判定通过,后续的 PAM 规则就不会再执行了,这可能导致密码验证被跳过。所以这里必须用required,保证每个认证因素都要过,不能"一票通过"。
第三个坑是测试时的会话隔离。我第一次在生产环境上加模块时,直接在唯一的 SSH 会话里改配置然后systemctl restart sshd,结果因为配置文件有个拼写错误,新连接全部被拒,我只能靠带外控制台去恢复。那次之后我立了个规矩:凡是改 SSH 或 PAM 配置,必须先开一个不关的"备份会话",再用reload而不是restart,最后在新窗口验证成功后才关闭备份会话。这个习惯救过我至少两次。
第四个坑是应急码的保存。生成器输出的那 5 个应急码,如果你没有抄下来就关掉了终端,它们就永远无法再次显示了。google-authenticator不会在下次运行的时候重新显示旧码,而是会重新生成一个新密钥,也就是说你要重新扫码绑定手机。所以第一个教训就是:认真抄好应急码,或者干脆把终端输出重定向到一个文件再备份到安全的地方。
在实际使用中,我还发现一个体验上的小问题:当 SSH 的公钥验证和键盘交互验证都启用时,有些终端工具(尤其是老版本)对键盘交互的支持不太好,验证码输入框可能一闪而过。碰见这种情况,优先升级终端工具,或者改用支持完整 KbdInteractive 流程的客户端。绝大多数现代终端工具都不存在这个问题,但如果碰上了,也不要怀疑是服务器配置错了,先查客户端。
关于重放防护,再补充一句:PAM 模块默认会用文件的修改时间和其中的记录来防止验证码重放,如果你对.google_authenticator文件做了手工修改或者恢复备份,可能会导致后续验证码全部失败。遇到这种情况,最简单的处理是重新执行一次google-authenticator,重新绑定手机,顺便拿一份新的应急码。
以我个人的实践经验来说,这个方案上线后真正的收益并不只是"多输一个六位数"这么简单,而是把账号安全管理从"依赖每个人守好密码"变成了"系统层面强制两道验证"。就算某一天有人把开发者的私钥泄露出去,只要手机不在攻击者手里,服务器仍然安全。而且这套方案的可维护性确实很好,回滚就是把那几行配置删掉,几分钟的事,完全可以在周一早上做完验证后直接推广给整个团队。
最后分享一个小技巧:如果你管理的服务器很多,可以考虑把绑定步骤也标准化——让每个用户在自己的机器上跑一遍google-authenticator,然后统一把密钥文件备份到加密的密码管理器里。这样万一手机丢了,重新生成密钥也比逐台排查要快得多。当然,前提是密钥备份的保管足够安全,毕竟它就是那扇门的另一把钥匙。