做运维的都知道,只要系统上了等保或者安全扫描,OpenSSH升级几乎是躲不过去的一道坎。尤其是RHEL / CentOS 7.9这种已经进入生命周期末期的系统,自带的OpenSSH版本普遍偏老,扫描器随便照一下就是CVE列表里躺了好几年的大洞。更麻烦的是生产环境往往在内网,隔离网段根本连不上外网yum源,想升级只能把源码包拷进去离线编译。最近我正好接手了一批7.9机器的OpenSSH离线升级任务,前前后后踩了不少坑,把完整流程和排查思路整理出来,给同样被这个问题困住的兄弟做个参考。
这篇文章覆盖从环境盘点、依赖编译、OpenSSH安装到服务切换的完整链路,所有操作都在纯内网环境验证过,适用于RHEL 7.9和CentOS 7.9(x86_64架构)。新手可以照着一步步操作,老手可以重点看第四部分的坑点总结,尤其是升级后登录不上这种翻车现场怎么救回来。
1. 为什么要离线升级OpenSSH:背景与坑点
1.1 等保合规与漏洞背景
先说最现实的驱动力。等保2.0和行业安全基线检查里,SSH服务版本是必查项。OpenSSH 7.4这个CentOS 7.9自带的版本,在漏洞库里挂着不少中高危CVE,包括但不限于用户名枚举、信息泄露、权限提升这类问题。扫描报告一旦出来,限期整改是跑不掉的。
内网机器还有个特点——系统盘和网络都是隔离的,安全团队只给一个整改窗口,比如周末凌晨两小时。这就意味着升级方案必须一次成功,而且不能影响现有业务连接。还有一个隐藏问题:内网机器很多是用了多年的老环境,glibc版本、编译工具链都可能不全,离线升级不是简单把rpm包装上就完事。
1.2 离线环境下的升级难点分析
离线升级OpenSSH,本质上是解决三个问题:依赖缺失、版本兼容、服务连续性。
依赖这块,OpenSSH编译需要OpenSSL、zlib、PAM开发库。OpenSSL尤其关键,OpenSSH的加密通道完全依赖它。CentOS 7.9自带的OpenSSL是1.0.2k,这个版本本身也有漏洞,所以升级OpenSSH时通常要一起把OpenSSL升掉。但OpenSSL又不是随便乱升的——yum、curl、wget这些系统组件都动态链接了系统OpenSSL,不能直接替换系统库,否则yum可能直接罢工。
版本兼容上,OpenSSH 8.8以上版本默认禁用了ssh-rsa算法(SHA-1),老客户端会连不上。很多内网环境里还有老设备用ssh-rsa登录,升级完新版本后这些设备就是"Connection closed by remote host",得提前想好对策。
服务连续性最让人头疼。升级sshd过程中一旦配置出错或者服务起不来,远程连接就断了,而人还在机房外面。所以保底通道是必须的——telnet虽然明文不安全,但关键时刻能救命。
1.3 方案选型:源码编译 vs 第三方RPM
升级OpenSSH有两条主流路线:一是拿源码编译,二是找第三方维护的RPM包(比如知名第三方仓库)。
我的建议是:纯内网环境优先源码编译。理由有三个:第一,第三方RPM依赖关系复杂,离线环境下搞依赖树本身就容易翻车;第二,源码编译可以把OpenSSL、OpenSSH都安装到自定义路径,不影响系统默认组件,风险可控;第三,等保整改往往有版本下限要求,源码编译可以精确选版本。
源码编译也有缺点——编译期间需要gcc、make等工具链,有些精简安装的机器连gcc都没有。这个问题在后面准备工作里会细说。
2. 动手前的准备工作:依赖盘点与保底通道
2.1 版本盘点与环境检查
在碰任何源码包之前,先把目标机器的家底摸清楚。需要确认四件事:系统版本、架构、当前OpenSSH版本、当前OpenSSL版本。
# 查看系统版本 cat /etc/redhat-release # 查看架构 uname -m # 查看当前OpenSSH版本 ssh -V 2>&1 # 查看当前OpenSSL版本 openssl version我这次操作的机器都是CentOS 7.9 x86_64,自带的OpenSSH 7.4p1,OpenSSL 1.0.2k。升级目标是OpenSSH 9.6p1,同时把OpenSSL升到1.1.1w。
还要检查编译工具链:
# 检查gcc、make是否安装 gcc --version make --version # 检查PAM开发库 rpm -qa | grep pam-devel如果没有gcc,这就麻烦了。内网环境没法yum install,只能找同版本系统的安装光盘或者yum仓库缓存来补。CentOS 7.9的安装ISO里自带gcc、make这些包,mount上去用rpm -ivh逐个安装就行。这一步虽然繁琐,但务必在升级前做好,否则编译到一半发现没编译器,进退两难。
2.2 依赖软件包的获取清单
离线环境所有东西都要提前准备好。我列一下我这次用的源码包清单和下载地址(在内网外的机器上下好,再拷进去):
| 软件包 | 版本 | 说明 |
|---|---|---|
| zlib | 1.2.13 | OpenSSH编译依赖 |
| openssl | 1.1.1w | 加密库,OpenSSH依赖 |
| openssh | 9.6p1 | 目标升级版本 |
| openssl-devel | 1.0.2k(系统自带) | 如果系统里没有,需要从ISO补 |
| pam-devel | 系统自带 | 没有的话也要从ISO补 |
源码包下载后,用MD5/SHA256校验一下完整性,然后传到目标机器的/usr/local/src目录。传文件的方式,内网环境常用scp、ftp,或者直接U盘拷。U盘拷贝时注意挂载格式,FAT32单文件不能超过4GB,不过这几个源码包都没那么大,问题不大。
2.3 提前开通telnet保底通道
这是整个升级过程中最重要的保险措施,没有之一。OpenSSH升级过程中任何一步出错,sshd服务可能起不来,telnet就是唯一的救命通道。
CentOS 7.9默认没装telnet-server,需要提前装好并启动:
# 检查是否已安装 rpm -qa | grep telnet-server # 如果没有,从系统ISO或镜像站下载安装: # telnet-server-0.17-64.el7.x86_64.rpm # telnet-0.17-64.el7.x86_64.rpm # xinetd-2.3.15-14.el7.x86_64.rpm rpm -ivh telnet-server-*.rpm telnet-*.rpm xinetd-*.rpm # 配置xinetd管理telnet cat > /etc/xinetd.d/telnet << 'EOF' service telnet { disable = no flags = REUSE socket_type = stream wait = no user = root server = /usr/sbin/in.telnetd log_on_failure += USERID } EOF # 启动服务 systemctl restart xinetd systemctl enable xinetd防火墙放行telnet端口(默认23):
firewall-cmd --permanent --add-port=23/tcp firewall-cmd --reload然后从另一台机器测试telnet连接,能登录成功再继续后面的操作。
注意:telnet是明文传输,只在升级期间临时开放,升级完成后必须立即关闭。如果公司安全策略不允许开telnet,退而求其次是准备好物理console(机房KVM),或者确认带外管理卡(如iLO/iDRAC)可用。
2.4 下载源码包与传输
源码包版本选择上,我建议OpenSSH选9.x的当前最新稳定版,OpenSSL选1.1.1系列(1.1.1w是最终维护版)。OpenSSL 3.x在CentOS 7.9上编译会面临glibc版本过老的问题,尽量别碰。
下载完成后,我习惯把所有源码包放在/usr/local/src,统一解压:
cd /usr/local/src tar -zxvf zlib-1.2.13.tar.gz tar -zxvf openssl-1.1.1w.tar.gz tar -zxvf openssh-9.6p1.tar.gz解压后建议先看下编译说明(README、INSTALL文件),确认有没有特别要求。OpenSSH 9.6的INSTALL文件里会写明依赖的最低版本,我之前见过有人不看说明直接编译,因为PAM版本不够导致configure直接失败。
3. 离线升级OpenSSH完整实操
3.1 编译安装zlib
zlib是基础库,OpenSSL和OpenSSH都依赖它。虽然CentOS 7.9系统里已经有zlib,但版本比较老(1.2.7),为了兼容性我们把它安装到独立目录。
cd /usr/local/src/zlib-1.2.13 ./configure --prefix=/usr/local/zlib make -j4 make install-j4是启用4个并行编译任务,机器核多的话能明显加快速度。如果机器的CPU核数少,可以改成-j2或直接不写这个参数。
编译完成后,zlib会被安装到/usr/local/zlib,不影响系统原有的zlib库。注意编译OpenSSL时要用--with-zlib-lib=/usr/local/zlib/lib这样的参数指定它的位置。
3.2 编译安装OpenSSL到自定义路径
OpenSSL是OpenSSH的核心依赖,也是最容易出问题的环节。直接覆盖系统OpenSSL会引发连锁反应(yum、wget都会坏),所以这里必须安装到自定义路径。
cd /usr/local/src/openssl-1.1.1w ./config --prefix=/usr/local/openssl --openssldir=/usr/local/openssl shared zlib \ --with-zlib-lib=/usr/local/zlib/lib --with-zlib-include=/usr/local/zlib/include注意几个关键配置项:
--prefix:指定安装路径,OpenSSL会装到/usr/local/opensslshared:生成动态共享库,OpenSSH编译需要链接动态库zlib:启用zlib压缩支持-fPIC:如果configure报错说需要PIC,可以在config后面追加这个参数,尤其当系统里其它库是以PIC方式编译时
make -j4 make install编译完成后,验证一下安装结果:
/usr/local/openssl/bin/openssl version注意:这里不要设置
LD_LIBRARY_PATH指向/usr/local/openssl/lib,否则会影响系统全局的动态库加载顺序。OpenSSH编译时会通过--with-ssl-dir明确指定使用哪个OpenSSL,运行时会通过rpath找到对应的库。
如果OpenSSL编译过程中报错No such file or directory: perl或者Can't locate IPC/Cmd.pm,这是系统Perl版本太老导致的。解决办法是用./config no-tests跳过测试模块构建,或者手动升级Perl(这个成本更高,建议先试no-tests)。
3.3 编译安装OpenSSH
这是整个升级的核心步骤。OpenSSH的configure参数决定了它的依赖位置、认证方式、运行路径,一个参数不对就可能导致编译失败或登录异常。
cd /usr/local/src/openssh-9.6p1 ./configure --prefix=/usr/local/openssh \ --sysconfdir=/etc/ssh \ --with-ssl-dir=/usr/local/openssl \ --with-zlib=/usr/local/zlib \ --with-pam \ --with-md5-passwords \ --mandir=/usr/share/man参数含义如下:
--prefix:安装到/usr/local/openssh--sysconfdir:配置文件目录设置为/etc/ssh,这样sshd_config、ssh_host_*_key等文件沿用系统原有位置,避免迁移配置的麻烦--with-ssl-dir:指定OpenSSL目录--with-zlib:指定zlib目录--with-pam:启用PAM认证支持,否则会绕开系统的密码认证策略、账户锁定策略,这是很多运维踩的坑--with-md5-passwords:兼容旧系统的MD5密码哈希--mandir:man手册页安装到系统默认位置
--sysconfdir=/etc/ssh这一点我特别强调一下。如果保留默认的--prefix下的目录,新版本sshd会使用新的配置目录,而系统原来的sshd配置(比如PermitRootLogin、PasswordAuthentication这些)不会被加载。我把配置目录直接指到/etc/ssh,新版本sshd会读取原有配置,同时用新的配置模板覆盖部分缺失项。
configure完成后,编译安装:
make -j4 make install编译过程可能会报openssl/xxx.h: No such file or directory,这是OpenSSL头文件路径没被找到。检查configure时--with-ssl-dir参数是否正确,或者确认OpenSSL是否成功安装到指定目录。
3.4 配置与启动sshd服务
新版OpenSSH安装完成后,需要处理几个关键配置和服务管理文件。
用/usr/local/openssh下的新版服务程序替换系统默认的sshd程序:
# 停止系统自带sshd服务 systemctl stop sshd # 替换二进制文件 cp /usr/sbin/sshd /usr/sbin/sshd.bak.$(date +%Y%m%d) cp /usr/local/openssh/sbin/sshd /usr/sbin/sshd # 替换客户端工具 cp /usr/bin/ssh /usr/bin/ssh.bak.$(date +%Y%m%d) cp /usr/local/openssh/bin/ssh /usr/bin/ssh cp /usr/local/openssh/bin/scp /usr/bin/scp cp /usr/local/openssh/bin/sftp /usr/bin/sftp这样做的目的是让systemctl restart sshd时依然通过原有的systemd服务单元启动新版程序,后续管理方式不变。
然后校验配置并重启服务:
# 校验sshd配置语法 /usr/sbin/sshd -t # 如果输出类似 /etc/ssh/sshd_config line 25: Bad configuration option 的报错 # 说明旧配置里有新版已移除的选项,需要注释掉 # 重启sshd systemctl restart sshd # 查看服务状态 systemctl status sshd关键配置项检查:
# /etc/ssh/sshd_config 核心配置确认 Port 22 PermitRootLogin yes # 如果运维需要root远程登录,保持yes,但建议配合密钥认证 PasswordAuthentication yes # 升级期间先保持yes,避免密码登录被卡 PubkeyAuthentication yes # 允许密钥登录 UsePAM yes # 必须为yes,否则PAM认证不生效配置/usr/local/openssh/etc下的PAM文件:
# OpenSSH 9.6 安装时会生成/etc/pam.d/sshd的模板 # 确认系统原有的PAM配置还在,没有被覆盖 cp /etc/pam.d/sshd /etc/pam.d/sshd.bak.$(date +%Y%m%d) cp /usr/local/openssh/etc/sshd.pam /etc/pam.d/sshd不过这个替换不建议直接做,因为系统原版的PAM配置包含了pam_localuser、pam_succeed_if等规则,直接替换可能会影响本地账号登录。我建议保留系统原版的/etc/pam.d/sshd,只用它来做PAM认证,OpenSSH自带的模板仅做参考。
设置SELinux(如果系统启用了):
# 查看SELinux状态 getenforce # 如果是Enforcing,需要放行sshd相关端口和连接 # 新版本sshd二进制文件的SELinux上下文可能不对,恢复一下 restorecon -Rv /usr/sbin/sshd /usr/bin/ssh重启后如果SELinux拦截了sshd,日志里能看到SELinux is preventing /usr/sbin/sshd from name_connect access on the tcp_socket port 22。处理方法是用audit2why或直接搜索/var/log/audit/audit.log里的相关记录,然后设置布尔值:
# 允许sshd访问网络 setsebool -P ssh_sysadm_login on接着处理systemd服务配置,让sshd服务可以管理新版二进制:
# 查看原有的sshd服务定义 systemctl cat sshd # ExecStart=/usr/sbin/sshd -D $OPTIONS # 我们已经替换了/usr/sbin/sshd,所以服务定义不用改最后确认sshd监听正常:
netstat -tlnp | grep :22 ss -tlnp | grep :22 # 应该看到sshd监听在0.0.0.0:223.5 升级后的验证步骤
升级完成后不要急着收工,验证环节必须做足。先查版本号:
ssh -V 2>&1 # 输出:OpenSSH_9.6p1, OpenSSL 1.1.1w 11 Sep 2023然后从客户端测试连接。这里要特别注意:升级后新版本默认禁用了ssh-rsa算法,如果你的客户端工具比较老(比如Windows自带的ssh还是老版本),连接时可能报Unable to negotiate with ...: no matching host key type found。
解决办法是临时在服务端/etc/ssh/sshd_config里加一行,允许老算法:
HostKeyAlgorithms +ssh-rsa PubkeyAcceptedAlgorithms +ssh-rsa重启sshd后老客户端就能连了。但这个属于妥协方案,安全巡检时这个配置会拉低评分,建议在用密钥认证逐步替换老客户端后移除。
还要验证密钥登录是否正常:
ssh -i ~/.ssh/id_rsa user@server以及配置文件关键项是否生效:
sshd -T | grep -E 'permitrootlogin|passwordauthentication|usepam'如果之前开通了telnet保底通道,认证ssh连接一切正常后,关闭telnet:
systemctl stop xinetd systemctl disable xinetd firewall-cmd --permanent --remove-port=23/tcp firewall-cmd --reload至此,OpenSSH离线升级主流程完成。
4. 常见问题与排查技巧实录
实际升级过程中,我遇到的问题比预设的要多得多。下面这些是我在多次操作中积累的经验,按问题类型分类整理,每个都有背景和解决过程。
4.1 configure阶段报错与处理
报错:checking for openSSL... no
这是最经典的报错。原因多半是configure没找到OpenSSL头文件或库文件。检查--with-ssl-dir的路径是否正确,确认 OpenSSL 是否真的装到了那个目录:
ls /usr/local/openssl/include/openssl/ssl.h ls /usr/local/openssl/lib/libssl.so如果文件不存在,说明OpenSSL编译安装失败,回头重新做OpenSSL那一步。还有个小技巧:configure时加上CFLAGS=-I/usr/local/openssl/include LDFLAGS=-L/usr/local/openssl/lib能提高成功率。
报错:configure: error: *** zlib.h missing
原因是zlib开发库没找到。用--with-zlib=/usr/local/zlib指定路径,或者加上CPPFLAGS和LDFLAGS环境变量。
报错:configure: error: *** Cannot find PAM headers
缺PAM开发库。CentOS 7.9系统里通常有pam-devel,如果确认没有,需要从ISO里找pam-devel包补装。
4.2 升级后登录不了的排查思路
升级后ssh端口连不上,是最吓人的情况。我在自己测试机器上就翻过一次车,好在有telnet保底。排查步骤记好,按顺序来:
- 确认服务状态:
systemctl status sshd,看是否active (running) - 看监听端口:
ss -tlnp | grep 22,确认sshd确实在监听 - 看防火墙:
firewall-cmd --list-all,检查22端口是否被放行;还要注意有没有装iptables,两个防火墙可能同时存在 - 看SELinux:
getenforce,如果是Enforcing,参考上面restorecon和布尔值处理 - 看日志:
journalctl -u sshd -n 50,tail -50 /var/log/secure
其中/var/log/secure里的记录最有价值。比如常见的:
Jul 11 02:34:17 server sshd[12345]: fatal: Cannot bind any address这是端口被占用或者监听地址无效。可能是原来还有别的sshd实例在跑,先pkill -9 sshd清理再重启。
如果登录时提示Connection closed by remote host,多半是算法协商失败,按上文的方法打开HostKeyAlgorithms +ssh-rsa或PubkeyAcceptedAlgorithms +ssh-rsa。
还有个容易忽略的点——sshd_config里的AllowUsers或AllowGroups。如果配置了白名单,而白名单里没有当前登录用户,就会直接拒绝连接。升级前备份的配置文件里如果有这类限制,要检查是否要保留。
4.3 服务启动失败与日志分析
编译安装后第一次启动sshd失败,多数原因集中在四个方面:配置语法错误、二进制库依赖缺失、权限上下文错误、PID文件冲突。
配置语法错误:/usr/sbin/sshd -t会直接告诉你哪行配置写错了。常见的是旧版配置选项在新版已废弃,比如UsePrivilegeSeparation在9.6里被移除了,Protocol 2也默认不需要写了。处理方式就是注释掉。
库依赖缺失:新编译的sshd二进制可能找不到libcrypto.so之类的库。
/usr/sbin/sshd -t # 如果输出:error while loading shared libraries: libcrypto.so.1.1: cannot open shared object file解决办法是让系统动态库加载器找到新库的位置:
echo "/usr/local/openssl/lib" > /etc/ld.so.conf.d/openssl-1.1.1.conf ldconfig /usr/sbin/sshd -t权限上下文错误:如果升级后的sshd被SELinux拦截,日志里会出现SELinux is preventing /usr/sbin/sshd from write access on the file /var/run/sshd。恢复上下文:
restorecon -Rv /var/run /usr/sbin/sshdPID文件冲突:如果原来sshd的PID文件还在,新进程可能无法创建,报sshd: fatal: Cannot open PID file /var/run/sshd.pid as mode 0644。删掉旧PID文件重启即可:
rm -f /var/run/sshd.pid systemctl restart sshd4.4 独家避坑经验总结
最后整理几条我在多次实操中总结出的经验,这些都是踩过坑才记住的:
第一,永远保留一份旧版sshd二进制副本。我在替换/usr/sbin/sshd前,一定会备份到/usr/sbin/sshd.bak.yyyymmdd。万一新版程序有问题,可以秒级回滚。同时也要备份/etc/ssh/sshd_config、/etc/pam.d/sshd这些关键配置文件。
第二,编译参数里的--with-pam一定不能省。我第一次升级时觉得系统密码认证够用了,没加PAM,结果系统账号的密码策略、fail2ban、账户锁定全部失效。运维登录审计发现密码错误次数限制不生效,这个坑相当深。
第三,升级完了别急着关telnet,先重启一次系统验证自动启动。我之前在一台机器上验证了ssh连接正常就关了telnet,结果后面机器重启发现sshd服务没起来(因为pid文件和xxx机制的锅),人又不在现场,只能让机房的人帮忙做console操作。
第四,/etc/ssh目录下sshd_config的文件权限要正确。OpenSSH对配置目录权限敏感,sshd_config不能对group和其他用户可写,否则启动时会报Bad permissions。如果从别的机器拷贝了配置文件过来,记得chmod 600 /etc/ssh/sshd_config,chmod 700 /etc/ssh。
第五,升级完成后检查一下sshd的依赖库引用。用ldd /usr/sbin/sshd检查是否有 "not found" 的依赖。我之前就因为OpenSSL的libssl.so版本不对,导致新sshd运行时使用的是系统旧库,虽然能启动,但部分加密算法不可用。正确的依赖应该是:
libcrypto.so.1.1 => /usr/local/openssl/lib/libcrypto.so.1.1 libssl.so.1.1 => /usr/local/openssl/lib/libssl.so.1.1 libz.so.1 => /usr/local/zlib/lib/libz.so.1如果ldd显示的是/lib64/libcrypto.so.10,说明rpath没生效,需要重新编译,或者在编译时用LDFLAGS显式指定rpath。
第六,关于升级时机的选择。我一般会把操作拆成两个窗口:第一个窗口做环境准备(装编译工具、开通telnet、传包),这个可以在工作日的低峰期完成;第二个窗口才做正式的编译和替换,这个必须放在停机窗口内。为什么?因为编译期间CPU占用高,可能影响线上业务,而且configure阶段不停止服务,不会影响现有连接,完全可以在低峰期把这些前期工作做掉。
个人经验是,OpenSSH升级本身不算复杂,真正考验人的是在极端情况下的应变能力。把保底通道准备好,把备份做足,把每个步骤的验证做细,基本不会出大问题。如果后续想降低手工操作的风险,可以把这个流程写成脚本,把版本检查、依赖清理、编译安装、配置替换、服务重启、自检这几个环节串起来,脚本化之后运维效率会高很多。