SSH免密登录失效?从sshd崩溃到RPM底层文件手工修复
2026/9/9 14:39:17 网站建设 项目流程

上周三下午,我正在处理另一台机器的日志切割任务,群里的消息突然就炸了。

“SSH 连不上了,所有服务器都试了一遍,就这台不行。”

发的是一台国产化 Linux 服务器,银河麒麟 ARM 架构,之前我花了不少时间给它配好了 SSH 密钥免密登录,连 root 带普通管理员账号一共五个,全部走公钥认证。结果现在,开发同事说连不上,而且不只是密钥不行,连密码登录都被拒,22 端口始终没响应。

我当时第一反应是“密钥权限被改了”,第二反应是“sshd_config 被谁动过”,结果排查到最后,发现真正的问题出在 RPM 包底层文件上,连 rpm 命令都直接报“没找到”。整个排障过程持续了将近三个小时,从 SSH 服务一路挖到系统的包管理文件,最后用 rpm2cpio 和 cpio 手工把底层二进制文件从 RPM 包里抠出来,重新塞回去才救回来。

这篇文章我把完整的排查链路、认证原理、修复步骤和踩过的坑都整理出来,希望对搞运维、搞国产化系统、或者遇到过不明原因 SSH 挂掉的朋友有帮助。

1. 故障现场:密钥免密登录配置“翻车”了?

1.1 出事之前:这台机器上我都做了什么

先交代一下背景。这批机器是几个月前上线的,系统是银河麒麟,基于 RPM 包管理,内核和桌面环境都做了定制,底层命令和 CentOS/RHEL 高度兼容。上线的第二周我做了 SSH 密钥免密登录的配置,目的很简单:让自动化运维脚本和开发同学不用每天输密码,也减少密码被爆破的风险。

当时做的事情其实就三件:

第一步,生成密钥对。我在本地跳板机上执行了ssh-keygen -t ed25519 -C "deploy@internal",得到一对密钥,私钥留在本地,公钥分发到服务器上。选择 ed25519 而不是 RSA,主要是因为它密钥短、生成快、安全性不输 RSA 4096,而且新版 OpenSSH 对它的支持非常好。

第二步,部署公钥。把公钥追加到服务器的~/.ssh/authorized_keys文件里。需要注意的是~是哪个用户的家目录,我当时给 root 和 deploy 用户都配了,所以分别在/root/.ssh/authorized_keys/home/deploy/.ssh/authorized_keys各追加了一次。权限也顺手设了,.ssh目录是 700,authorized_keys是 600,属主归属对应用户。

第三步,修改/etc/ssh/sshd_config开启公钥认证。主要参数是PubkeyAuthentication yes,然后测试无密码登录成功,确认没问题之后才关闭了密钥登录的后备选项,当然密码登录我还是留着的(PasswordAuthentication yes),因为总有人会忘带私钥。

配置完之后,免密登录一直用得挺顺。无论是ssh root@server还是scp传文件,都是秒连,从来没有出过问题。

1.2 故障症状:密钥被拒、密码被拒、端口没反应

开发同学反馈的问题描述是这样的:

  • 密钥登录:本地执行ssh deploy@server后,终端卡在Authenticated to server之前,迟迟不出来,最后报Permission denied (publickey,password)
  • 密码登录:ssh root@server后输入正确密码,同样被拒绝
  • 端口状态:在本地telnet server 22,显示Connection refused

这个组合很反常。如果只是密钥没配对,密码认证应该还能兜底;如果只是密码策略变了,密钥认证应该能通。现在两者同时挂了,最直接的解释就是 sshd 服务根本没在监听,或者监听进程是异常状态。

我从带外管理口(那台机器有 BMC 远程控制台)登录进系统,先确认网络层面,ping网关正常,说明服务器本身还活着。接着看了 sshd 服务状态:

systemctl status sshd

输出显示Active: failed,也就是 sshd 服务已经崩溃退出。

再尝试手动启动看看报错:

systemctl start sshd

结果瞬间失败,什么守护进程都没起来。这时候我就明白,这已经不只是配置层面的问题了,大概率是 sshd 依赖的底层文件出了问题。

2. 顺着登录链路深挖:密钥认证的本质与定位转折

2.1 密钥免密登录到底是怎么工作的

在讲后面的修复过程之前,我先把密钥免密登录的认证机制完整梳理一遍,因为这次排障里一个关键教训就是:很多人对密钥认证的理解停留在“把公钥放上去就行”,这会导致故障时根本不知道该往哪个方向查。

密钥免密登录的本质,是服务端验证“你确实持有与已登记公钥配对的私钥”。完整个流程是这样:

第一步,客户端发起连接,告诉服务器自己想用哪个用户登录,并列出自己支持的认证方法。服务器端如果开启了PubkeyAuthentication,就会回复允许公钥认证。

第二步,服务器在用户的authorized_keys文件里找到客户端声称持有的公钥,生成一个随机挑战值,用这个公钥加密后发给客户端。

第三步,客户端用本地私钥解密挑战值,把结果返回给服务器。服务器验算通过,就确认“持有私钥的人”是合法用户。

这个过程绕过了密码传输,私钥全程不出本地机器,安全性和便捷性都比密码登录高不少。但这里面有个容易忽略的细节,sshd服务在读取authorized_keys文件之前,会先检查文件的权限和属主。如果.ssh目录权限是 777,或者authorized_keys文件其他用户也能写,服务端会直接拒绝加载公钥。这就是StrictModes参数在做的事,默认是开启的。

我举一个生活化的例子:公钥就像你所在写字楼前台的访客名单,写着“持有工牌 9527 的人可以进入 12 层”。私钥就是那张工牌,每次进门时,你刷一下工牌,前台把编号和名单一比对,符合就放行。但如果你把访客名单放在人来人往的公共桌上,前台会认为名单不可信,直接拒绝所有人进去。

所以如果你发现自己明明配好了密钥却依然要输密码,别急着怀疑密钥对不对,先去看权限。这是我在过往项目中踩过最多的坑,没有之一。但这一次,连authorized_keys都还没来得及看,服务就起不来了,肯定还有更底层的问题。

2.2 关键转折:sshd 崩溃背后的依赖分裂

从带外控制台登录系统后,我先执行了最常见的三连排查:

sshd -t journalctl -u sshd --no-pager -n 50 ls -l /etc/ssh/sshd_config

sshd -t只校验配置语法,很快就通过了,说明sshd_config本身没问题。journalctl里只看到服务启动失败的记录,没有更详细的报错。第三次检查配置文件也没有异常。

这时候我意识到一个问题:既然配置没问题,那 sshd 为什么连启动都启动不了?我决定直接尝试手动执行 sshd 二进制文件,看真实输出:

/usr/sbin/sshd -D

终端立刻输出了一行关键信息:

/usr/sbin/sshd: error while loading shared libraries: libcrypto.so.10: cannot open shared object file: No such file or directory

这句话才是真正的问题:sshd启动时依赖的libcrypto.so.10动态库文件丢失了。

libcrypto.so.10是 OpenSSL 的核心动态库,SSH 在做加密握手、密钥交换、证书认证时都离不开它。如果这个库缺失,sshd 整个进程连初始化都做不到。

我赶紧用ldd命令检查/usr/sbin/sshd的完整依赖关系:

ldd /usr/sbin/sshd

输出的后半段有一堆not found,不只是libcrypto.so.10,还有libssl.so.10,以及好几个 PAM 相关的库文件。这说明并非偶发性的单个文件损坏,而是多个 RPM 包的文件同时缺失,很像是有人对系统文件做过整体误删,或者某个不安分的安装脚本把库文件给覆盖没了。

紧接着我准备用 RPM 查询哪个包应该拥有这些文件:

rpm -qf /usr/lib64/libcrypto.so.10

结果系统直接回了一句:

bash: rpm: command not found

在这个时刻,我的排障思路彻底转折了——这台机器不仅 SSH 挂了,连 RPM 包管理工具本身也丢了。一个连包管理机制都瘫痪的系统,用常规手段根本无法修复。

2.3 为什么 rpm 命令会突然消失:常见原因盘点

后来故障恢复之后,我做了一次彻底复盘。结合现场查到的线索,这台机器的rpm命令丢失以及底层库文件缺失,最可能是下面几个原因之一:

第一,被误卸载。如果有人执行了rpm -e或者第三方安装脚本里写了卸载操作,恰好把包含rpm命令的rpm包,或 OpenSSL 相关的openssl-libs包给卸掉了,就会同时引发命令丢失和库文件缺失。

第二,文件被恶意清空。这类情况在国产化系统上也不是没发生过,某些安全软件或可疑脚本会因为“误判”而清空一些核心动态库文件。毕竟libcryptolibssl这两个文件的名字在某些检测规则里很容易被当成“可疑加密工具”处理,但实际上它们是系统运行必不可少的底层组件。

第三,rpm 数据库损坏。命令还在,但执行时读取/var/lib/rpm下的数据库报错,表现上也像是“没有这个命令”。这种情况稍微好办一点,因为rpm --rebuilddb通常能解决。

第四,磁盘写满导致文件截断。文件明明存在,但内容写到一半因磁盘满被截断,系统启动后加载动态库失败。我当时检查了磁盘空间,没有满,所以基本排除。

无论如何,当务之急是找到正确的 RPM 包,从中提取底层文件,手工恢复。这就要用到我们今天标题里的另一个关键词——RPM 包底层文件修复。

3. 修复实操:不依赖 rpm 命令也能手工恢复底层文件

3.1 第一阶段准备:确认系统版本、架构和拿到正确 RPM 包

在动手之前,我反复告诉自己一个原则:绝不要在一台系统已经处于异常状态的机器上,随手找一个版本相似的文件就覆盖过去。系统版本、包版本、CPU 架构必须完全对上,否则可能引起连锁的兼容性问题,到时候连系统都起不来。

首先确认当前系统的基础信息。我用cat /etc/os-release查看系统发行版,确认是银河麒麟 V10 SP1 版本,内核基于 4.19。再执行uname -m确认 CPU 架构是aarch64,也就是 ARM 64 位。这一步很重要,因为 ARM 架构和 x86_64 的 RPM 包完全不通用,硬装会导致二进制格式不兼容。

接着我需要找到这台机器对应的原始安装源。幸运的是机房里有这台机器当时的 ISO 安装镜像,我把Kylin-Desktop-V10-SP1-Release-aarch64.iso挂载到另一台同架构的正常机器上,进入Packages目录后,找到了几个必要的 RPM 包:

  • openssh-server-*.aarch64.rpm
  • openssh-clients-*.aarch64.rpm
  • openssh-*.aarch64.rpm
  • openssl-libs-*.aarch64.rpm
  • pam-*.aarch64.rpm

这里面openssh-server提供/usr/sbin/sshd主进程,openssh提供公共配置文件,openssl-libs就是丢失的libcrypto.so.10libssl.so.10的所属包,pam包里则有许多 SSH 登录依赖的 PAM 模块。

如果你没有原始 ISO,也不要慌,只要网络能通且系统配置了 Yum/DNF 源,可以用yumdownloader直接下载对应包,或者在有图形界面的机器上从官方镜像站手动下载相同版本的 RPM 包。但注意一点,下载时一定要匹配系统版本和 CPU 架构,千万别下载成x86_64的包塞到aarch64系统里。

3.2 核心操作:利用 rpm2cpio 和 cpio 从 RPM 包里提取文件

RPM 包这东西,本质是一个带元数据的 cpio 归档。平时我们安装时,RPM 工具会负责校验依赖、执行脚本、把文件放到正确位置。但现在 rpm 命令本身已经“失联”了,没法走正常的安装通道,所以我决定绕开 rpm,直接用底层的rpm2cpiocpio组合把文件解出来。

把 RPM 包传到目标机器(通过带外控制台挂载虚拟光驱,或者用 scp 从别的机器传),然后在任意临时目录下执行:

mkdir /tmp/openssh-fix cd /tmp/openssh-fix rpm2cpio openssh-server-8.2p1-4.ky3.aarch64.rpm | cpio -idmv

这条命令中,rpm2cpio负责把 RPM 包转换成 cpio 格式流,竖线管道符再把这个流直接交给cpio-i表示解包,-d表示自动创建所需目录,-m表示保留文件时间戳,-v是显示解出的文件明细。

执行完毕后,临时目录里会出现一个usr/目录,里面的结构就是 RPM 包装载到系统后的文件布局,比如usr/sbin/sshdusr/etc/ssh/sshd_config(具体路径取决于包内定义)。这一步相当于我们用手把压缩包里的东西“倒”了出来,不需要发行版的包管理器参与。

有了这个基础之后,修复就变成了一个“文件搬运”的活儿。但搬运之前还有一步必做的工作:先看包里有哪些文件、对应的路径是什么。这一步尤其重要,因为很多 RPM 包里的文件路径和系统实际路径不完全一致,例如某些配置文件在包内叫usr/etc/xxx,但装到系统后会放到/etc/xxx。要对照cpio -t的输出,或者直接看解出来的目录结构判断。

find /tmp/openssh-fix -type f | sort

确认文件清单后,再用ldd检查一下sshd解包后所有依赖的库文件是否都已经解出来了。如果还缺哪个库,继续从对应 RPM 包里解包提取。这一步是确保我们“搬”过去的文件能够真正运行起来的关键。

3.3 文件覆盖与权限恢复:先备份、再覆盖、后对齐

文件提取到临时目录后,接下来的操作就是覆盖,但覆盖这一步绝对不能用蛮力,必须遵守三条规则:备份旧文件、按需要保留原配置、严格重置权限。

我先在临时目录里找到了usr/sbin/sshd,确认它是可执行文件,查看版本和依赖:

ldd /tmp/openssh-fix/usr/sbin/sshd

这时发现里面大部分依赖已经能搜到输出,只有libcrypto.so.10libssl.so.10仍然显示not found,因为这两个库文件还没有恢复。于是我用相同方式解包openssl-libs

mkdir /tmp/ossl-fix cd /tmp/ossl-fix rpm2cpio openssl-libs-1.1.1f-3.ky3.aarch64.rpm | cpio -idmv

解出来后找到了usr/lib64/libcrypto.so.10usr/lib64/libssl.so.10,以及一系列符号链接文件。

接下来进入正式恢复环节。我先把原系统上还在的旧文件全部做个备份。虽然很多文件已经缺失,但能备份的还是备份一下,放到/root/backup-ssh-$(date +%F)/目录下。备份命令类似这样:

mkdir -p /root/backup-ssh-2025-01-15 cp /usr/sbin/sshd /root/backup-ssh-2025-01-15/sshd.bak 2>/dev/null || true cp /usr/lib64/libcrypto.so.10 /root/backup-ssh-2025-01-15/libcrypto.so.10.bak 2>/dev/null || true

然后,把解压出来的文件复制回系统标准路径:

cp /tmp/openssh-fix/usr/sbin/sshd /usr/sbin/sshd cp /tmp/ossl-fix/usr/lib64/libcrypto.so.10 /usr/lib64/libcrypto.so.10 cp /tmp/ossl-fix/usr/lib64/libssl.so.10 /usr/lib64/libssl.so.10

覆盖完成后,立刻重置文件权限和属主。这步千万不能省,否则文件虽然存在,但可执行位不对,或者属主不对,依然无法正常调用。

chmod 755 /usr/sbin/sshd chown root:root /usr/sbin/sshd chmod 755 /usr/lib64/libcrypto.so.10 /usr/lib64/libssl.so.10 chown root:root /usr/lib64/libcrypto.so.10 /usr/lib64/libssl.so.10

需要注意一个细节:libcrypto.so.10libssl.so.10通常还带有一串符号链接,比如libcrypto.so.1.1.so结尾的软链。这些软链有时候会因为误删操作一起丢失。我专门检查了一遍/usr/lib64/下这两个库的软链情况,发现缺失后通过ln -s按原样补回。如果你不确定软链的目标,可以用ls -l从另一台同环境正常机器上对照获取。

我用一个通俗类比来解释这一步:这些库文件就像家里水管系统的弯头和接头。弯头缺失,水龙头拧开也只能哗哗漏水;接头型号不对,硬接上去可能把整段水管都崩裂。只有弯头、接头、水管口径完全匹配,水才能顺畅地从主管道流到各个水龙头。我们在修复底层库时也是同理,不仅要文件在,还得确保软链、权限、属主全部对齐。

3.4 验证闭环:服务恢复 + 密钥免密登录重新测试

文件覆盖完成后,我重新执行了依赖检查:

ldd /usr/sbin/sshd | grep "not found"

输出的结果是空的,说明所有依赖库都已经找到了。这时候心里稍微有底了一些。

接下来启动服务:

systemctl daemon-reload systemctl start sshd systemctl status sshd

这一次,sshd状态从failed变成了active (running),22 端口也开始监听。但到这里只能算“SSH 服务能起来了”,还不能算“故障已经解决”。为什么?因为现在这台机器的 RPM 数据库和关键包状态还很混乱,我必须确认密钥免密登录真正恢复了,才敢把问题单关闭。

先在服务器本地测一次完整的密钥认证流程。我从另一台正常客户端机器上执行:

ssh -v deploy@server

-v参数会输出详细认证过程。日志里看到:

debug1: Server accepts key: /home/deploy/.ssh/authorized_keys debug1: Authentication succeeded (publickey).

这一行就说明公钥认证已经成功,免密登录恢复了。

但我还是不甘心,又追加验证了一个密码登录:

ssh root@server

输入密码后,成功登录。这说明 PAM 的登录链路也恢复了,密码认证没有受到影响。

验证过程中还有一个重要动作:查看服务端/var/log/secure日志,确认没有出现权限相关告警或Authentication refused的错误。日志干净之后,整个故障闭环才算真正打通。

4. 故障复盘:这类问题怎么防、怎么自愈

4.1 为什么“免密登录”本身没有背锅

这次故障里最误导人的地方,就在于表面症状是“SSH 连不上”,而且我们刚刚大改过密钥免密登录配置,所以所有人第一反应都是“免密登录配置出问题了”。但事实上,密钥免密登录配置一直是对的,真正问题出在 OpenSSL 动态库和 RPM 工具链文件的缺失上。

这个经验非常重要:当 SSH 出现异常时,先把排查范围从“配置文件”扩大到“服务运行状态”和“底层依赖”,才能真正定位问题。我做了一个简单的心得总结:

  • 如果sshd服务还活着,但认证失败,优先排查密钥权限、authorized_keys内容、StrictModes参数、PAM 配置
  • 如果sshd服务起不来,优先看journalctl -u sshd和手动执行/usr/sbin/sshd -D的报错
  • 如果手动执行报缺失动态库,用ldd检查依赖链,并逐步向上追溯是哪个 RPM 包拥有这个文件
  • 如果rpm命令本身消失,那就准备走本文的rpm2cpio + cpio手工修复路线

把这个排查顺序写进故障手册里,下次遇到类似问题能少走至少半小时弯路。

4.2 故障自愈与应急工具建议

这次经历让我对“自愈”有了更深的体会。所谓自愈,不只是出了问题能快速恢复,更是在问题发生之前就给自己留好退路。我在事后给这批服务器做了几项加固措施,现在分享出来:

第一,建立本地 RPM 包迷你仓库。从官方 ISO 里把opensshopenssl-libspamcoreutils等关键软件包全部拷贝到一个独立目录,做一次基于版本号的索引。以后哪怕没外网,也能快速找到匹配的包。

第二,开启 RPM 数据库定时校验。用rpm -V可以比对包内文件的属性与原始安装状态,及时发现文件缺失、内容改动或权限变化。我写了一个简单的巡检脚本,每周跑一次,输出差异报告到监控平台。这个思路成本很低,但能在文件刚开始被破坏时就发出警告。

第三,配置带外管理通道。这台服务器原本就有 BMC 带外控制台,这次我之所以能在断网情况下完成修复,全靠它。绝不要在生产环境放弃带外通道,那是你在紧急情况下的最后一根救命稻草。

第四,建立应急自愈脚本。我写了一个非常朴素的检查脚本,每隔五分钟检测一次sshd服务状态,如果发现服务为failed,就自动执行一次ldd /usr/sbin/sshd检查,一旦确认是依赖缺失,就把本地镜像仓库里准备好的 RPM 包解包并覆盖恢复。这个脚本没有多复杂,但它真正实现了标题里“故障自愈”四个字——当然,覆盖文件前必须备份,这属于基础素养。

第五,把救援手册打印出来放机柜。手册要写清楚“在什么情况下用哪些命令”,包含从带外登录、备份、解包、覆盖、验收到回滚的每一步。我们平时总觉得这些命令烂熟于心,但真实故障现场,人一紧张连ls都可能打错。手册不是给高手看的,是给紧绷状态下的自己兜底的。

4.3 经验之外的一点小提醒

最后说一个很多人容易忽视的细节:清理临时文件。我在修复结束后,把/tmp/openssh-fix/tmp/ossl-fix目录里的 RPM 解包内容全部清理掉了,因为这些文件如果不清理,会占用临时目录空间,还可能被后续巡检脚本误认为“异常文件”。清理命令很简单:

rm -rf /tmp/openssh-fix /tmp/ossl-fix

此外,这次修复也让我更加确信:在国产化 Linux 环境中,尽量不要用“下载二进制然后直接覆盖”的思路做 Fix,因为国产系统对文件权限、SELinux 上下文、PAM 配置的校验往往比标准发行版更严格,偶尔还会加上自己的安全机制。凡是从 RPM 包原文解出的文件,安全性要远高于网上随便下载的二进制文件。

这个案例最让我感慨的地方在于,一次看似普通的 SSH 免密登录故障,最终却牵出了整个系统的 RPM 包管理链路问题。如果没有 RPM 包底层的手工修复能力,这台机器很可能会走向“重装系统”的命运。而看清底层机制、掌握手工提取和恢复的能力,才是运维人在关键时刻真正救命的本事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询