
SSH 升级这件事只要你动过一次生产环境的 sshd就会明白它跟装个 Nginx 完全不是一个量级的操作。Nginx 挂了用户抱怨一句sshd 挂了你自己都进不去机器。所以当安全扫描报告里那串版本号被标红、合规审计要求把 SSH 提到 9.2 的时候真正考验的不是你会不会敲./configure而是你有没有把整套编译安装—打包成 RPM—灰度切换—回滚兜底的链路想清楚。这篇内容就是把我自己在几轮 SSH 编译安装和 RPM 打包里踩出来的经验摊开讲为什么源码编译和打 RPM 包要分成两条路走、configure里那些开关各自在赌什么、spec 文件里哪几行是保命的、以及升级完之后能连上但登不进这种最烦人的问题到底出在哪个环节。不管你是第一次给 SSH 做版本升级的新手还是已经打过几个包的运维老人应该都能从里面抠出点能直接抄的东西。1. 升级前的判断为什么 SSH 版本升级不能照着普通软件的路子走SSH 是整台服务器的最后一扇门它的特殊性在于——你用来修它的通道本身就是要被修的那个东西。这个闭环特征决定了升级方案的设计逻辑跟别的组件完全不同。大多数软件升级失败最多是服务重启不成功你还能 SSH 进去看日志SSH 升级失败日志就在你手里但你已经没有通道去看它了。所以我每次做这件事都是先按最坏情况会失联来准备再倒推操作步骤。1.1 版本号标红之后先搞清楚到底在升什么扫描器给出一堆漏洞编号很多时候并不代表你的 SSH 真的在你担心的场景下可被利用而是版本号低于策略阈值。但 9.2 这个版本确实带来了一些实质性的东西值得认真对待。从 8.x 到 9.x上游做了几件对生产有直接影响的事一是默认的密钥交换算法、主机密钥算法、加密套件都在往更现代的方向收老的 SHA-1 相关算法彻底退到需要显式开关才能启用的位置二是对ssh-rsa也就是 SSH 协议第一版的 RSA/SHA-1 签名的处理越来越保守很多发行版自带的 9.x 包已经把这条算法从默认列表里挪走了三是一些配置项的名字和语义变了旧配置文件里写的东西可能在新版里被忽略甚至报错。这里我个人的原则是升级动机要落到具体能力上而不是版本号数字更大。比如你的动机可能是扫描器要求关闭 SHA-1 系列算法那升级后必须验证sshd -T输出里kexalgorithms、ciphers、macs三行确实没有弱项如果你的动机是审计要求 SSH 版本号大于等于某个值那升级完只要ssh -V对得上就行。两种动机对应的验证方式完全不同别混着做。我自己会在动手前先跑一遍现状采集把升级前长什么样存档这样出问题时才有对照物# 记录当前版本与编译参数 ssh -V 21 | tee /root/pre_upgrade_version.txt sshd -T /root/pre_upgrade_sshd_T.txt 21 rpm -qa | grep -i openssh /root/pre_upgrade_rpms.txt rpm -qi openssh-server /root/pre_upgrade_rpms.txt # 记录当前算法清单 sshd -T | grep -E ^(kexalgorithms|ciphers|macs|hostkeyalgorithms) # 记录谁在用旧算法重要决定升级后要不要开兼容开关 grep -i kex\|cipher\|mac /var/log/secure* 2/dev/null | tail -50最后那条grep很容易被忽略但它是决定升级后要不要临时保留几个老算法的唯一依据。我见过太多次升级完 SSH服务器本身没事结果某个老旧的备份一体机、几台嵌入式设备、某个运维同事常年不关的终端工具全部连接失败因为对方客户端只支持 SHA-1 系列算法。这类下限客户端的兼容性只有翻日志才能提前发现。提示老客户端兼容性问题是 SSH 升级后最常见的软故障它不会在升级当场暴露而是在第二天某个定时任务跑起来的时候才炸。提前翻日志比事后补开算法开关从容得多。1.2 三条命门控制台、不死会话、旧版本包我把这三样叫做 SSH 升级的三件套缺任何一样我都不会按下回车。第一是带外控制台。不管你是云主机还是物理机务必确认有一个不依赖 sshd 的登录入口——云厂商的控制台、IPMI、串口控制台、甚至机房现场。这一步没有替代方案因为 sshd 起不来的时候任何远程手段都是死路。我通常会在这个阶段顺手验证一下控制台真的可用、账号密码真的对而不是应该能用。第二是不死会话。SSH 有个非常好的特性systemctl restart sshd只会杀掉监听进程已经建立的会话不会被断开因为这些会话的进程挂在独立的会话终端下不对 sshd 主进程构成依赖。这个特性是回滚的第一道保险——重启 sshd 之后先别退出你手上那个已经连上的窗口直接用第二个窗口尝试新连接。新连接成功了再关老窗口新连接失败了老窗口还在你可以就地修复。第三是旧版本包。不管是源码编译还是 RPM 包升级之前一定要保证能一键回到老版本。RPM 场景下这个好办rpm -Uvh装新包、rpm -Uvh --oldpackage回滚旧包就行源码编译场景就要靠提前把老的二进制、配置文件、库文件全部备一份。我通常的备份姿势是BK/root/ssh_rollback_$(date %Y%m%d%H%M) mkdir -p $BK cp -a /usr/sbin/sshd $BK/sshd.bin cp -a /usr/bin/ssh $BK/ssh.bin cp -a /usr/bin/ssh-keygen $BK/ssh-keygen.bin cp -a /etc/ssh $BK/etc_ssh rpm -qa | grep -i ssh $BK/rpms.list ldd /usr/sbin/sshd $BK/ldd_before.txtldd那一条是很多人漏掉的。源码编译的 sshd 依赖 OpenSSL 和 zlib如果升级过程里顺手把系统 OpenSSL 也换了老的 sshd 二进制可能因为库版本对不上而完全跑不起来这时候复制回去根本救不了场。先记下依赖树才能判断回滚路径到底是用二进制覆盖、还是必须用包管理回滚。1.3 源码编译还是打 RPM 包这俩不是替代关系刚接触这块的人经常纠结到底该用哪种方式。我的看法是这两条路线解决的是不同层面的问题实际上是一条流水线上的两个阶段。源码编译解决的是能不能跑起来、跑起来对不对的问题。它的优势是完全可控——你可以指定 OpenSSL 路径、指定安装前缀、控制 PAM 是否启用、调节编译优化遇到问题可以直接改源码重编。它最大的毛病是污染环境make install会直接往系统目录里塞文件包管理器不知道这些文件的存在回头rpm -Va校验会报一堆文件被修改更难受的是如果哪天有人跑了一次yum update openssh你的 9.2 就被系统自带的旧版本悄悄覆盖回去了而且没有任何提示。RPM 包解决的是能不能长期维护、能不能批量复制的问题。它把编译产物纳入包管理体系版本可追溯、可回滚、可批量分发、可以和仓库策略统一也避免了被系统更新意外覆盖。所以我的实践路径是先在一台测试机上用源码编译的方式把依赖链、配置、启停全部跑通摸清所有坑然后再把这套已经验证过的流程固化成 spec 文件打成 RPM用包管理的方式推到生产。前者是探路后者是交付。下面这张表是我对不同场景的取舍总结可以对照自己的环境看一眼场景推荐方式关键理由单台临时验证、做兼容性测试源码编译改参数快失败了大不了删目录生产批量升级、需要审计留痕编译 RPM 包版本可追溯能进仓库统一管理系统 OpenSSL 版本过旧需同时升级先编 OpenSSL 再编 SSH最后一起打包依赖链必须整体打否则回滚会断无法访问外网的内网环境编译 RPM 包 本地仓库一次构建离线分发只为一台机器临时打补丁源码编译到独立前缀不动系统目录风险最小2. 源码编译把依赖链和 configure 参数一个个抠清楚源码编译 SSH 这件事命令本身网上到处都是但真正决定成败的是那些为什么这么写的判断。我这部分不求把每条命令罗列全重点讲清楚哪些地方看起来可以随便填、实际上填错了会在几小时后才报错。2.1 依赖不只是装上就行zlib、OpenSSL、PAM 的版本咬合SSH 的编译依赖主要三块zlib压缩、OpenSSL加密与密钥交换、PAM可插拔认证。zlib 一般不用操心系统自带的头文件版本就够用只要确认zlib-devel装了、/usr/include/zlib.h存在即可。OpenSSL 是真正的雷区。这里有个非常典型的判断分歧用系统自带的 OpenSSL还是自己重新编一个。系统自带的版本受发行版策略约束可能偏旧某些新的加密算法不可用自己编的版本新但会引入一个隐患——如果你的 SSH 链接的是/usr/local/ssl/lib下的新 OpenSSL而系统里其他组件比如 curl、python 的 ssl 模块链接的还是/usr/lib64下的老版本那这台机器上就同时存在两套 OpenSSL一旦有人动了环境变量或者ldconfig的配置就会出现某个程序突然找不到符号的诡异问题。我的处理原则是如果系统自带的 OpenSSL 已经能满足 SSH 9.2 的编译要求就优先用系统的通过--with-ssl-dir显式指向系统 OpenSSL 的根目录减少变量。只有在系统 OpenSSL 实在太旧、编译直接报错的情况下才走自己编 OpenSSL 独立前缀 编译时显式链接的路子并且一定要在链接阶段确认依赖是干净的# 确认 configure 之后 SSH 链接的是哪个 OpenSSL grep -i ssl Makefile | head -20 # 编译安装完成后验证依赖 ldd /usr/local/sbin/sshd | grep -i -E ssl|crypto|z如果ldd输出里出现了/usr/local/ssl/lib这样的路径而你又没打算长期维护这个独立 OpenSSL那就应该回头调整configure参数否则后面打包会连带把一整棵 OpenSSL 依赖树都拖进来。PAM 这一块最容易被想当然。SSH 支持两种认证后端直接读/etc/shadow做密码校验或者通过 PAM 做校验。现代发行版上大量认证策略密码复杂度、失败锁定、双因素、账号过期都挂在 PAM 上如果编译时没开 PAMSSH 就绕过了这些策略看起来能登录实际上审计上直接不合规。所以--with-pam基本是必开的同时必须装pam-devel。开了 PAM 之后还有一个连带义务/etc/pam.d/sshd这个文件必须存在且内容正确否则 sshd 会在认证阶段直接失败日志里给出的信息往往很含糊类似pam unable to dlopen或者干脆什么都不报。编译前我会做一次依赖体检把该装的包一次性配齐# RHEL 系 dnf install -y gcc make autoconf automake zlib-devel pam-devel \ openssl-devel perl-core wget tar # Debian 系 apt-get install -y build-essential zlib1g-dev libpam0g-dev \ libssl-dev libselinux1-devperl-core这条容易被漏掉SSH 的构建过程里有几个脚本依赖 Perl缺了会在make后期才报错前面一路顺畅特别容易让人误判。2.2 configure 参数里每一个开关背后的取舍configure那行长参数是整篇里信息密度最高的地方我逐段拆一下我常用的版本./configure \ --prefix/usr \ --sysconfdir/etc/ssh \ --with-pam \ --with-zlib \ --with-md5-passwords \ --with-privsep-path/var/lib/sshd \ --with-ssl-dir/usr \ --with-selinux--prefix/usr这个选择值得说道。理论上装到/usr/local是最干净的做法不跟包管理器打架但实际上系统里sshd的调用路径、systemd 单元文件里写死的路径、各种管理脚本里引用的路径全都是/usr/sbin/sshd。装到/usr/local之后你会发现要么改 systemd 单元要么做软链接反而增加了维护成本。所以我更倾向于装到/usr然后靠先备份再覆盖的方式来保证回滚同时心里清楚这次操作会让系统自带的 openssh 包进入文件被外部修改的状态。--sysconfdir/etc/ssh决定了配置文件的落点。填/etc/ssh能复用系统原有的配置文件路径和 host key 位置省去大量迁移工作。如果填成/usr/etc/ssh或者默认的/usr/local/etc/ssh那么新编译的 sshd 会去找另一个路径下的配置和主机密钥结果就是服务起来了、端口在听、但连上去直接被拒——因为没有主机密钥或者没有正确的配置。--with-privsep-path/var/lib/sshd是权限分离机制的运行时目录。这个目录的权限要求非常严格所有者 root、权限 755 或者 700不能有任何 group/other 写权限。SSH 在认证前后会降权到独立的低权限用户通常是sshd并在这个目录下做一些操作如果目录权限不对sshd 启动时可能不报错但认证时会失败。我见过最隐蔽的一次故障就是这个目录被备份脚本改成了 777结果所有密码登录全部失败只有密钥登录正常——排查了两个小时。--with-selinux只在启用了 SELinux 的机器上需要。开了这个之后编译出来的 sshd 会带上 SELinux 的相关支持但注意编译时支持 SELinux 不等于运行时就自动有了正确的标签。如果二进制文件被放到了/usr/sbin而 SELinux 的策略库里针对这个路径的上下文是sshd_exec_t通常没问题但如果装到自定义路径就必须手动打标签。这一点在第 4 章还会专门讲。2.3 编译安装前的挪窝先并行部署别直接覆盖这里是我觉得最有价值的一条经验不要直接make install覆盖系统原有的 sshd。原因很简单——一旦覆盖你就同时失去了旧版本和旧配置中间的验证环节就没法做对照了。我的做法是先安装到一个临时前缀把它当成新版本的预演区./configure --prefix/opt/ssh92 \ --sysconfdir/opt/ssh92/etc \ --with-pam \ --with-zlib \ --with-privsep-path/var/lib/sshd make -j$(nproc) make install装到/opt/ssh92之后你得到的是一套完全独立的新版本。接下来直接用它在另一个端口上起一个实例做验证# 用独立配置文件 独立端口起一个测试实例 /opt/ssh92/sbin/sshd -f /opt/ssh92/etc/sshd_config -p 2222 -D -e 这里-D是前台运行-e是把日志打到标准错误方便你直接看到启动过程中的所有告警。用另一个终端连2222端口把下面几件事都验证一遍密钥登录能不能过密码登录能不能过这一步验证 PAM 是否正常工作sshd -T输出的配置是否符合预期老客户端用你预期要保留的算法能不能连上验证通过了再走正式安装。正式安装时我的做法是把新二进制装到/usr下的同时把老二进制改名保留# 备份老版本前面已经做过一次这里再确认一遍 test -f /usr/sbin/sshd.bak || cp -a /usr/sbin/sshd /usr/sbin/sshd.bak test -f /usr/bin/ssh.bak || cp -a /usr/bin/ssh /usr/bin/ssh.bak # 正式安装到 /usr ./configure --prefix/usr --sysconfdir/etc/ssh --with-pam --with-zlib \ --with-md5-passwords --with-privsep-path/var/lib/sshd make -j$(nproc) make install--with-md5-passwords这个开关默认是关闭的只有在你环境里确实存在需要 MD5 密码认证的极端老系统时才需要开。如果开了务必在升级后的验收清单里加一条确认 MD5 相关告警是否可接受因为它在安全审计里通常是被明确点名要求关闭的项。2.4 sshd_config 迁移新老配置项对撞的几种典型情况配置文件迁移是源码编译环节里最琐碎、也最容易出事的部分。新版 sshd 对配置项的解析是严格模式——遇到不认识的指令sshd -t会直接报错退出服务起不来。这一点跟很多软件忽略未知配置的宽容策略完全不同所以要格外小心。常见的对撞情况有这么几类第一类是指令被移除。比如早期版本里一些关于协议版本、旧算法开关的配置在新版里已经彻底消失。这类问题sshd -t会直接告诉你行号改起来不难难的是你得判断删掉这一行会不会改变行为。比如某些老配置里显式禁用了某类算法删掉之后可能反而打开了不该开的算法所以删之前要看一眼新版默认值。第二类是语义发生变化。同一个指令名新旧版本的默认值和取值范围可能不同。最典型的是算法列表类的配置新版默认列表的排序和内容跟老版不一样你留着一行旧的显式列表可能反而把新版本更安全的默认给覆盖了。这类问题sshd -t不会报错但行为已经悄悄变了。我的做法是升级后跑一次sshd -T把关键算法项的输出跟升级前的存档逐行对比有差异就逐个确认。第三类是新增必填或推荐项。新版有时会引入一些新的安全默认如果你的配置里显式写了相反的值就需要评估。这类通常会在升级后的安全扫描里被再次点名。我通常的迁移流程是这样的# 1. 先用原有配置直接测看能不能过 /usr/sbin/sshd -t # 输出报错就按行号逐个改不要一次性大改 # 2. 生成一份生效配置快照跟升级前对比 /usr/sbin/sshd -T /root/post_upgrade_sshd_T.txt diff /root/pre_upgrade_sshd_T.txt /root/post_upgrade_sshd_T.txt # 3. 重点关注算法相关的差异 diff (grep -E ^(kexalgorithms|ciphers|macs|hostkeyalgorithms) /root/pre_upgrade_sshd_T.txt) \ (grep -E ^(kexalgorithms|ciphers|macs|hostkeyalgorithms) /root/post_upgrade_sshd_T.txt)sshd -T这个命令在整个流程里的价值被严重低估了。它不只是打印配置而是按 sshd 真实解析逻辑输出最终生效值包含所有默认值展开。用它做前后对比比人肉读配置文件可靠一百倍。2.5 用临时端口做灰度不碰老服务完成新版本验证前面提到用/opt/ssh92起测试实例这里展开说一下具体的灰度策略因为这是整个流程里最能降低风险的环节。核心思路是在不动现有 22 端口监听进程的前提下让新版本在另一个端口上跑起来把所有认证路径都验一遍确认无误后再切换。具体的操作序列我一般是这么走的# 第一步以调试模式前台启动新版本观察启动日志 /opt/ssh92/sbin/sshd -f /opt/ssh92/etc/sshd_config -p 2222 -D -e # 第二步另开终端用指定端口连接 ssh -p 2222 -o PreferredAuthenticationspublickey userhost ssh -p 2222 -o PreferredAuthenticationspassword userhost # 第三步验证配置解析无误 /opt/ssh92/sbin/sshd -T -f /opt/ssh92/etc/sshd_config | head -40 # 第四步验证老算法兼容性如果环境里有老客户端 ssh -p 2222 -o KexAlgorithmsdiffie-hellman-group14-sha1 userhost第四步很关键它验证的是你觉得能连的老客户端实际上真的能连。如果这一步失败说明新版本的算法策略把老客户端挡在门外了那你就需要在正式切换前决定是升级老客户端还是在服务端显式放开某个算法。这个决策不能拖到切换之后做因为切换之后所有老客户端都会同时失联压力会非常大。还有一个细节测试实例启动时如果配置里写的是Port 22-p 2222这个命令行参数会覆盖配置文件里的端口设置这是安全的。但要注意ListenAddress指令不会被-p覆盖如果配置里写死了ListenAddress新实例可能会跟老实例抢同一个地址。测试阶段我通常会让测试实例使用一份精简过的独立配置文件只保留端口、host key、认证相关的必要项避免跟主配置互相干扰。2.6 切换与收尾systemd、SELinux 上下文和 PAM 三件事验证通过之后正式切换只有一步重启 sshd 服务。但切换之后必须确认的三件事比切换本身更重要。**第一件是 systemd 单元的实际执行路径。**很多发行版的 sshd 单元文件不是直接ExecStart/usr/sbin/sshd -D而是先跑一个密钥生成脚本类似sshd-keygen.service再启动主服务。如果你的新版本对主机密钥的类型有额外要求、或者你希望它自动补齐缺失类型的密钥那就要确认这个前置步骤确实执行了。检查方式很直接systemctl cat sshd systemctl status sshd-keygen 2/dev/null ls -l /etc/ssh/ssh_host_*主机密钥缺失或者类型不全sshd 也是能起来的它会用现有的密钥但某些只支持特定算法的客户端就会连不上。用ssh-keygen -A可以在不覆盖已有密钥的前提下补齐缺失的类型这个命令是幂等的放心跑。**第二件是 SELinux 上下文。**如果你是把二进制编译安装到/usr/sbin/sshdSELinux 策略通常能自动匹配到正确的类型sshd_exec_t一般不用额外处理。但如果是装到自定义路径比如/opt/ssh92/sbin/sshd而你又想让它作为系统服务运行那就必须处理上下文否则服务会被 SELinux 拦住日志里会有明确的avc: denied记录# 查看当前上下文 ls -Z /usr/sbin/sshd # 如果不对手动恢复 restorecon -v /usr/sbin/sshd # 自定义路径的场景需要加规则 semanage fcontext -a -t sshd_exec_t /opt/ssh92/sbin/sshd restorecon -v /opt/ssh92/sbin/sshd排查 SELinux 问题的标准动作是ausearch -m avc -ts recent只要看到denied就说明确实被拦了。不要图省事直接setenforce 0那只是把问题藏起来而且生产环境大概率不允许长期关着。**第三件是 PAM 配置。**源码编译开启 PAM 支持之后/etc/pam.d/sshd必须存在。如果系统自带的 openssh 包里有这个文件升级过程不会删掉它所以通常没问题但如果是全新装的最小化系统可能压根没有这个文件。检查方式ls -l /etc/pam.d/sshd # 内容至少要包含标准的 auth/account/password/session 四段这个文件缺失的典型表现是密钥登录正常密码登录直接被拒日志里pam相关的信息含糊不清。这一条我在下面第四章还会再展开因为它的表现实在太容易误导人。3. 打成 RPM为什么生产环境最终还是要回到包管理源码编译跑通只是证明了这套东西能用离能长期维护还差一层。我见过太多环境一开始图快直接源码装一年之后没人说得清这台机器上的 SSH 到底是哪个版本、加了什么编译参数、跟系统包是什么关系。打成 RPM 之后这些问题全部由包元数据回答而且升级回滚变成了一行命令。3.1 rpmbuild 目录树与宏定义先把地基搭正打包的第一步不是写 spec而是把rpmbuild的工作目录树建起来。这个目录结构是固定的位置由~/.rpmmacros里的%_topdir决定# 建目录树 mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} # 自定义 topdir推荐避免污染家目录 cat ~/.rpmmacros EOF %_topdir %(echo $HOME)/rpmbuild %_smp_mflags -j$(nproc) %debug_package %{nil} EOF%debug_package %{nil}这一行是我每次都会加的。默认情况下 rpmbuild 会给二进制生成 debuginfo 包而 sshd 这种带 setuid 语义、涉及大量权限操作的程序生成 debuginfo 的过程有时会引入额外的构建依赖和构建时间对生产发布毫无价值。关掉它能让构建过程干净很多。把源码包放进SOURCES目录的时候注意一个细节版本号和目录名的对应关系。上游源码包解压出来的目录名通常是openssh-9.2p1这种带p1后缀的形式而 spec 里%setup -q默认会去找%{name}-%{version}。如果Version:只写9.2%setup就会找不到目录构建直接失败。解决办法有两种把Version写成9.2p1或者在%setup里显式指定目录名%setup -q -n openssh-9.2p1。我更倾向于前者因为版本号写全了在rpm -qa的输出里也一目了然。3.2 spec 文件逐段拆解哪几行是保命的下面这份 spec 是我实际在用的一份简化版重点看注释标注的几处Name: openssh Version: 9.2p1 Release: 1%{?dist} Summary: OpenSSH remote login server and client License: BSD URL: https://www.openssh.com/ Source0: %{name}-%{version}.tar.gz BuildRequires: gcc make zlib-devel pam-devel openssl-devel perl-core Requires: pam Requires(pre): shadow-utils Provides: openssh-server %{version}-%{release} Obsoletes: openssh-server %{version}-%{release} %description Secure Shell server and client. %prep %setup -q %build %configure \ --sysconfdir%{_sysconfdir}/ssh \ --with-pam \ --with-privsep-path/var/lib/sshd \ --with-ssl-dir%{_prefix} make %{?_smp_mflags} %install rm -rf %{buildroot} make install DESTDIR%{buildroot} install -d -m 0755 %{buildroot}/var/lib/sshd install -d -m 0755 %{buildroot}%{_sysconfdir}/ssh %pre getent group sshd /dev/null || groupadd -r sshd getent passwd sshd /dev/null || \ useradd -r -g sshd -d /var/empty/sshd -s /sbin/nologin -c Privilege-separated SSH sshd %post # 主机密钥补齐幂等操作 /usr/bin/ssh-keygen -A || : # 权限修正这行很关键 chmod 0711 /var/empty/sshd 2/dev/null || : %systemd_post sshd.service %preun %systemd_preun sshd.service %postun %systemd_postun_with_restart sshd.service %files %attr(0755,root,root) %{_sbindir}/sshd %attr(0755,root,root) %{_bindir}/ssh %attr(0755,root,root) %{_bindir}/ssh-keygen %attr(0600,root,root) %config(noreplace) %{_sysconfdir}/ssh/sshd_config %config(noreplace) %{_sysconfdir}/pam.d/sshd %dir %attr(0700,root,root) %{_sysconfdir}/ssh %dir %attr(0711,root,root) /var/lib/sshd %{_mandir}/man8/sshd.8* %{_mandir}/man1/ssh.1*几个必须说的点%config(noreplace)是保命的关键。它告诉包管理器升级时如果本地配置文件被改过不要覆盖它而是把新版本的文件存成.rpmnew后缀。没有这个标记升级会把你的sshd_config直接冲掉——这在 SSH 场景下是灾难性的因为配置文件里往往有PermitRootLogin、AllowUsers、Port这些关键设置冲掉了就等于把机器门锁换了。%pre里创建 sshd 用户是权限分离机制的前提。这个用户必须是系统用户-r、不能有可用 shell/sbin/nologin、家目录指向/var/empty/sshd。漏了这一步sshd 启动时会报Privilege separation user sshd does not exist然后直接退出服务起不来。%post里的ssh-keygen -A是幂等的补齐操作它只生成缺失类型的主机密钥不会覆盖已有的所以可以放心每次安装都跑。这解决了新装机器没有主机密钥导致服务起不来的问题。%files里%attr(0700,root,root) %{_sysconfdir}/ssh这一行容易被忽略。/etc/ssh目录的权限必须是 700 或者至少不能是 group/other 可写。有些打包方式会让make install把目录建成 755虽然一般也能用但严格的安全基线要求是更紧的。同理/var/lib/sshd的 711 权限也是必须的。还有一点Obsoletes: openssh-server这一行在不同发行版上处理不一样。如果你的环境里系统自带包的包名就叫openssh那么直接用同名包升级就行不需要Obsoletes如果系统自带的拆成了openssh和openssh-server两个包那就需要显式声明替代关系否则两个包会共存并互相抢文件。这一条一定要在测试机上实际验证一次不要凭猜。3.3 依赖链的构建顺序OpenSSL 和 SSH 必须咬合如果你的场景需要自己编译 OpenSSL比如系统自带的版本太旧那打包顺序就变成了硬约束OpenSSL 的 RPM 必须先构建、先安装、再构建 SSH 的 RPM。原因很直白——SSH 的 spec 里BuildRequires需要能找到 OpenSSL 的头文件和库而Requires需要运行时能找到对应的.so版本。更麻烦的是版本咬合问题。如果你编了 OpenSSL 3.x 并在 SSH 里链接它那么装了新 SSH 包的机器上就必须同时装新 OpenSSL 包否则会报 error while loading shared libraries: libcrypto.so.3。而这个新 OpenSSL 包可能会跟系统里原有的其他组件冲突——它们可能链接的是 1.1 版本两者 ABI 不兼容。我的处理方式有两种取决于环境第一种是独立前缀 显式 rpath。把 OpenSSL 装到/opt/openssl3构建 SSH 时通过--with-ssl-dir/opt/openssl3指定同时在链接参数里加上-Wl,-rpath,/opt/openssl3/lib64。这样 SSH 会优先在自己的 rpath 里找库不会跟系统 OpenSSL 冲突。缺点是 rpath 这种做法在安全审计里有时会被质疑需要能解释清楚为什么这么做。第二种是整体升级把 OpenSSL 也纳入包管理体系让Requires显式声明依赖关系由包管理器保证一致性。这种做法更规范但影响面大——升级 OpenSSL 可能影响系统上所有依赖它的组件必须做完整回归。我个人在核心生产环境更倾向这种因为它的依赖关系是显式的、可查询的出问题能一眼看出是谁的锅。下面这张表是我对不同依赖处理方式的对比处理方式优点风险适用场景用系统 OpenSSL依赖简单无冲突版本可能不满足 SSH 要求系统 OpenSSL 较新独立前缀 rpath不影响系统其他组件rpath 需解释路径写死单机部署、容器镜像整体升级 OpenSSL 打包依赖显式、可查询影响面大需回归测试大规模统一环境3.4 签名与本地仓库让包管理系统真正认这份包包打出来不签名yum/dnf默认是拒绝安装的除非关掉 gpgcheck但那等于自废武功。所以签名是交付环节的必要一步# 生成签名密钥一次性 gpg --gen-key # 导出公钥供客户端导入 gpg --armor --export your-key-id RPM-GPG-KEY-local # 签名 rpm --addsign ~/rpmbuild/RPMS/x86_64/openssh-9.2p1-1.el8.x86_64.rpm # 验证签名 rpm --checksig ~/rpmbuild/RPMS/x86_64/*.rpm签名验证通过之后把包放进本地仓库目录生成元数据mkdir -p /var/www/repo/local/Packages cp ~/rpmbuild/RPMS/x86_64/*.rpm /var/www/repo/local/Packages/ createrepo_c /var/www/repo/local/ # 客户端配置 cat /etc/yum.repos.d/local.repo EOF [local] nameLocal Repo baseurlhttp://repo-host/local enabled1 gpgcheck1 gpgkeyfile:///etc/pki/rpm-gpg/RPM-GPG-KEY-local EOFcreaterepo_c每次新增包之后都要重新跑一次否则仓库元数据里没有新包的信息yum会报找不到包。这个坑我踩过不止一次——包明明在目录里yum list就是看不到折腾半天才想起来元数据没刷新。3.5 升级与回滚的实际命令行操作真正上线时的操作序列我一般按这个顺序走# 1. 先看会动到哪些包干跑 dnf upgrade openssh --assumeno # 2. 备份关键文件 cp -a /etc/ssh /root/ssh_etc_backup # 3. 执行升级 dnf upgrade -y openssh # 4. 立刻验证先不退出当前会话 systemctl status sshd sshd -T | grep -E ^(port|permitrootlogin|passwordauthentication) ssh -V # 5. 新开一个终端验证登录 ssh -o PreferredAuthenticationspublickey userhost # 6. 确认无误后再关闭老会话回滚的话前提是你保留了旧版本的 RPM 文件# 保留旧包 dnf install -y dnf-command(download) dnf download openssh openssh-server --destdir/root/rpm_backup # 回滚 dnf downgrade -y /root/rpm_backup/openssh-*.rpm # 或者 rpm -Uvh --oldpackage /root/rpm_backup/openssh-8.x.rpmdnf downgrade和rpm -Uvh --oldpackage这两个我都用前者会自动处理依赖关系后者在手上有完整包集合的时候更快更直接。回滚之后记得systemctl restart sshd因为包管理器替换二进制文件不会自动重启服务——这一步忘了你会以为回滚没生效。4. 现场踩过的坑故障现象与排查链路实录这一章我按现象—排查—根因—修复的顺序讲几个真实遇到过的问题。这些问题的麻烦之处在于现象看起来都差不多连不上或者连上了登不进但根因天差地别只有按链路一步步排除才能定位。4.1 服务显示 active但端口根本没在听有一次升级完systemctl status sshd显示active (running)systemctl restart也没有报错但新连接全部超时。这个现象最容易让人怀疑网络或防火墙实际上根因在 systemd 的单元机制上。排查链路是这样的# 第一步确认端口真的在监听 ss -lntp | grep :22 # 结果什么都没有。说明进程虽然跑着但没有绑定端口 # 第二步看进程实际在干什么 ps -ef | grep sshd # 结果有一个 sshd 进程但它是 socket 激活模式下的守护进程 # 第三步看单元文件的类型 systemctl cat sshd | grep -E ExecStart|Type|Accept根因是这套系统的 sshd 用的是socket 激活模式一个sshd.socket单元负责监听端口收到连接之后才拉起sshd.service处理。这种模式下你替换了/usr/sbin/sshd二进制之后socket 单元里的监听进程还是老版本的因为 socket 单元没重启或者是新二进制跟 socket 激活的调用约定不匹配。修复方式很直接systemctl daemon-reload systemctl restart sshd.socket 2/dev/null || true systemctl restart sshd.service ss -lntp | grep :22所以每次替换二进制之后标准动作应该是daemon-reload 重启所有相关单元而不是只restart sshd。如果你的环境里同时存在sshd.service和sshd.socket先确认清楚哪个在实际生效再决定重启谁。4.2 密钥能登密码登不进PAM 与权限位的双重陷阱这个故障现象特别有迷惑性密钥登录一路顺畅说明 sshd 本身没问题、网络没问题、host key 没问题但一换成密码就Permission denied。很多人的第一反应是密码错了或者账号被锁实际上密码是对的。排查链路# 1. 看认证日志找 pam 相关行 tail -50 /var/log/secure | grep -i -E pam|auth # 典型输出 # sshd[xxxx]: pam_unix(sshd:auth): authentication failure # 或者 # sshd[xxxx]: error: PAM: Authentication failure for user from x.x.x.x # 2. 确认 /etc/pam.d/sshd 是否存在且可读 ls -l /etc/pam.d/sshd # 3. 确认编译时确实开了 PAM /usr/sbin/sshd -T | grep -i pam # 如果这里什么都没有说明编译时没开 --with-pam有一次排查下来是双因叠加一是编译的时候漏了--with-pamsshd 走的是直接读 shadow 的路径但这台机器上账号的密码哈希算法是系统策略管理的直接读 shadow 对不上二是权限分离目录/var/lib/sshd被某次批量脚本改成了 777导致权限分离机制异常。修复之后我加了两条自检命令到升级流程里现在每次都必须跑# 权限分离目录的权限必须是 711 或 700不能有 group/other 写 stat -c %a %U %G /var/lib/sshd # PAM 支持必须存在 /usr/sbin/sshd -T | grep -qi pam echo PAM OK || echo PAM MISSING # sshd 用户必须存在且 shell 不可用 getent passwd sshd提示/etc/ssh目录和/var/lib/sshd目录的权限是 SSH 故障里排名前三的隐形杀手。备份恢复脚本、容器镜像构建、批量权限修复工具任何一个都可能顺手把它们改掉。升级后务必检查。4.3 端口改了、防火墙放了、还是不通SELinux 与监听地址的两道关卡改 SSH 端口这件事很多教程只讲两步改sshd_config的Port、防火墙放行。但在启用 SELinux 的系统上还有第三道隐性关卡——SELinux 对 sshd 允许监听的端口是有白名单的。现象是ss -lntp能看到新端口在听防火墙规则也确认放行了但客户端连接依然被拒。排查链路# 1. 确认端口在听 ss -lntp | grep sshd # 2. 确认 SELinux 是否拦截 ausearch -m avc -ts recent | grep sshd # 典型输出会提到 name_bind 和 port 相关的 denied # 3. 查看 sshd 允许的端口类型 semanage port -l | grep ssh_port_t修复需要把新端口加入允许列表semanage port -a -t ssh_port_t -p tcp 2222 # 如果端口已存在但类型不对用 -m 修改 semanage port -m -t ssh_port_t -p tcp 2222另一类同类问题是ListenAddress。如果配置文件里写了ListenAddress 0.0.0.0sshd 会监听所有 IPv4 地址但有些环境里 IPv6 是启用的某些客户端会优先走 IPv6结果连到的是一个没有监听的地址族。现象是有时候能连、有时候不能连特别难查。我的习惯是显式写清楚ListenAddress 0.0.0.0 ListenAddress ::或者只保留0.0.0.0并在客户端侧强制 IPv4。这种细节不值得省。4.4 老客户端握手失败算法策略变更的连锁反应前面反复提到老客户端兼容性这里给一个完整的处理方案。现象是升级完之后运维同事的终端能连但某几个自动化脚本、老版本客户端、嵌入式设备全部报no matching key exchange method found或者no matching host key type found。排查第一步是确认到底缺哪个算法# 客户端侧开详细日志 ssh -vvv userhost 21 | grep -i no matching # 服务端侧看支持的算法清单 sshd -T | grep -E ^(kexalgorithms|hostkeyalgorithms|ciphers|macs)知道了缺什么就有两个方向可选升级客户端或者在服务端显式放开算法。生产环境里如果老客户端短期无法升级通常只能选后者但要注意放开的方式——不要直接把整个算法列表替换成老的那样等于把新版本的安全改进全部丢掉。正确做法是在保留新版默认的基础上追加# 在 sshd_config 末尾追加而不是替换 KexAlgorithms diffie-hellman-group14-sha1 HostKeyAlgorithms ssh-rsa PubkeyAcceptedAlgorithms ssh-rsa前缀表示在默认列表基础上追加这是 SSH 配置里非常重要的语法-表示移除^表示移到最前。用能最大限度保留安全默认。同时必须明确一件事追加弱算法是一个有明确期限的临时措施不是最终状态。我通常会在配置里加注释标明追加原因和计划清理时间避免这个临时变成永久。4.5 RPM 升级时的文件冲突与配置被覆盖用 RPM 升级时最常见的两个报错file /usr/sbin/sshd from install of openssh-9.2p1-1.x86_64 conflicts with file from package openssh-server-8.x这说明你打的包跟系统原有包的名不同、但文件路径重叠了。解决办法是让新包在 spec 里通过Obsoletes/Provides显式声明替代关系或者干脆用同名包。测试阶段可以临时用rpm -Uvh --replacefiles绕过但生产环境不要用这个参数它会把冲突文件直接覆盖掉出了问题没法追溯。第二个是配置被覆盖。如果 spec 里%files没有写%config(noreplace)升级之后sshd_config会被新包的版本直接替换你原来的Port、PermitRootLogin、AllowUsers全部丢失。更难受的是这种覆盖不会报错升级过程成功了但服务重启后行为完全变了。验证方式很简单升级前后各跑一次md5sum /etc/ssh/sshd_config # 如果变了检查是否生成了 .rpmnew / .rpmsave ls -l /etc/ssh/sshd_config.rpm*看到.rpmnew说明你的配置被保留了、新配置放在了旁边看到.rpmsave说明你的配置被改名了、新配置顶了上来。后者的处理方式是从.rpmsave里把自定义项摘出来合并进新的sshd_config然后删掉.rpmsave。这一步做完必须再跑一次sshd -t因为合并过程很容易把新旧配置项搞混。5. 上线之后的长期维护升级完成、验证通过事情还没结束。SSH 这种东西真正的风险在于下一次。我一般会在升级收尾时做三件事把未来半年可能出的问题提前解决掉。第一件是把这次编译使用的参数、spec 文件、源码包、签名公钥统一归档到一个固定位置并在内部文档里写清楚这台机器的 SSH 是自编译还是官方包。这一条听起来像行政工作但它的价值在于下一次有人接手、或者半年后你自己再动这台机器时不至于对着ssh -V猜半天这个版本从哪来。第二件是在监控里加上 SSH 版本和端口的探针。具体做法是定期跑一次ssh -V并把版本号作为指标上报同时监控ss -lntp里 22 端口是否始终在听。这两条指标一旦变化能第一时间发现——很多时候包管理器的自动更新会在半夜把版本悄悄换回去第二天你才发现自己精心编译的 9.2 变成了系统自带的旧版本。要避免这种情况可以在包管理器配置里加excludeopenssh*把 SSH 从自动更新中排除交给自己的流程管控。第三件是把这次的算法兼容性决策记录成清单。哪些老客户端还在用、放了哪些算法、计划什么时候清理全部落到文档里。我在实际操作中的体会是SSH 升级真正难的不是编译也不是打包而是改完之后三个月某个没人记得的脚本突然连不上了这一类问题。而这类问题唯一的预防手段就是当时把兼容性决策写清楚。最后分享一个我认为很有用的小习惯做任何 SSH 相关变更之前先在另一个终端开一个sleep 3600的长连接挂着。它占用的资源可以忽略不计但在你手滑把 sshd 弄挂的时候那个窗口就是唯一的救命通道比任何回滚方案都直接。这个习惯我保持了几年救过我至少三次。