2024年7月1日,OpenSSH官方紧急发布了9.8p1,修复了一个代号为regreSSHion的远程代码执行漏洞,对应CVE-2024-6387。当时我的第一反应不是"又一个高危漏洞",而是"这次轮到了sshd"。做过Linux运维的人都清楚,sshd长期暴露在22端口上,承担着所有远程登录入口的职责,一旦它出了可被远程利用的漏洞,整台机器的安全边界就形同虚设。这篇保姆级教程,就把我从收到漏洞通告到完成OpenSSH 9.8p1升级、顺带处理OpenSSL老版本依赖问题的完整过程写出来。整个流程覆盖环境评估、OpenSSL 3.0编译安装、OpenSSH 9.8p1源码编译、服务切换验证和回滚方案,适用于CentOS 7、Ubuntu 22.04这些主流Linux环境,照着做就能把风险控制住。
1. 为什么偏偏要赶在9.8p1:regreSSHion不是普通漏洞
很多人看到"OpenSSH又发新版了"第一反应是“日常更新”,但9.8p1这次的发布节奏和版本跨度非常反常。官方直接把版本号从9.7跳到了9.8,而且同步修复了一个可以直接导致root权限远程代码执行的严重安全问题。升级不再是“选做题”,而是“必答题”。
1.1 一个2006年修复过的老问题为何会复活
CVE-2024-6387的本质是sshd中的一个信号处理竞争条件(signal handler race condition)。sshd在客户端连接建立后,如果客户端一直不完成身份认证,LoginGraceTime设置的超时计时器会触发SIGALRM信号。问题在于,这个信号处理器内部调用的函数并不是异步信号安全的,当它和主流程中的其他操作同时发生时,就可能出现内存损坏,进而被攻击者利用执行任意代码。
最棘手的一点是,这个漏洞不需要任何认证信息,也不需要合法的账号密码。攻击者只要能够通过网络访问到sshd端口,反复建立空闲连接来触发竞争窗口,就有可能拿到root shell。整个利用过程通常在几小时内完成,如果目标系统没有开启地址空间随机化(ASLR),利用难度还会进一步降低。
这个问题的讽刺之处在于,它其实是2006年CVE-2006-5051修复过的老问题。当时OpenSSH通过修改signal handler解决了不安全函数调用的问题,但在后续代码重构中,这个修复被部分回退了,导致8.5p1到9.7p1之间的版本重新暴露在风险之下。官方称之为regreSSHion,既是在描述"回归",也是在提醒运维人员别轻视历史包袱。
1.2 升级范围的判定:哪些版本中招、哪些不受影响
判断自己是否需要升级,先看两个维度:系统架构和OpenSSH版本。
受影响的是:64位glibc Linux系统上,OpenSSH版本在8.5p1到9.7p1之间的sshd。这也是当前绝大多数云服务器和物理机默认使用的组合。不受影响的情况包括:OpenBSD系统、32位Linux、使用musl libc的Alpine Linux、以及低于8.5p1但已经应用过旧补丁的Red Hat系发行版。
我在实际排查中发现,很多CentOS 7默认自带的是OpenSSH 7.4p1,单看版本号确实不在受影响区间内,但问题在于这类老系统往往还带着老旧的OpenSSL 1.0.2k,而OpenSSL 1.0.2和1.1.1两个长期支持分支都已经停止维护。也就是说,即使sshd本身暂时不被这个CVE波及,配套的加密库也已经是安全盲区。这也是为什么本次升级我会把OpenSSL一起纳入范围:既然要动编译链,就一次把技术债还清。
2. 动手前先盘点环境:版本、系统、服务管理方式
源码编译升级OpenSSH最怕两件事:一是远程操作中断导致失联,二是新版本与系统既有配置不兼容导致sshd无法启动。这两件事都可以通过充分的预检来避免。花十分钟做环境盘点,比升级失败后花两小时救数据划算得多。
2.1 三步看清当前版本和依赖
第一步,确认当前OpenSSH和OpenSSL版本。注意ssh -V的输出是写到stderr的,直接执行屏幕上能看到,但如果不重定向,脚本或文档里抓取时容易漏掉信息。
ssh -V 2>&1 # 或者查看sshd本身的版本 /usr/sbin/sshd -V 2>&1 openssl version -a第二步,确认操作系统发行版和init系统。不同发行版在编译依赖、服务名和配置文件路径上都有差异,不能一概而论。
cat /etc/os-release ps -p 1 -o comm= systemctl status sshd 2>/dev/null || service sshd status第三步,确认当前sshd的监听端口、配置文件和认证方式。升级前必须知道当前的登录链路是什么样,否则替换完二进制后可能发现配置文件里有关键参数变动,导致彻底无法登录。
ss -tlnp | grep :22 grep -E '^(Port|PermitRootLogin|PubkeyAuthentication|PasswordAuthentication)' /etc/ssh/sshd_config2.2 不同发行版的兼容性差异
下面这个表是我在实际操作中踩过坑后总结出来的,直接决定你后面要不要额外处理一些环境问题。
| 发行版 | 默认OpenSSL | 默认OpenSSH | 注意点 |
|---|---|---|---|
| CentOS 7 | 1.0.2k | 7.4p1 | 系统自带的OpenSSL老得离谱,但yum、curl等大量系统组件依赖它,绝对不能直接覆盖 |
| CentOS 8 | 1.1.1k | 8.0p1 | 依赖库相对新,编译OpenSSL 3.0基本无障碍 |
| Rocky/AlmaLinux 9 | 3.0.7 | 8.7p1 | 系统自带OpenSSL 3.x,可以跳过OpenSSL升级步骤,直接编译OpenSSH |
| Ubuntu 20.04 | 1.1.1f | 8.2p1 | 需要额外确认libpam0g-dev和zlib1g-dev已经安装 |
| Ubuntu 22.04 | 3.0.2 | 8.9p1 | 系统自带OpenSSL 3.x,同样可以跳过OpenSSL升级,但为了统一版本我一般还是会编译安装最新3.0.x |
| Debian 11/12 | 1.1.1n / 3.0.x | 8.4p1 / 9.2p1 | apt源里openssl版本比CentOS 7干净很多,编译依赖用build-dep最省事 |
如果是CentOS 7这种老系统,我强烈建议OpenSSL和OpenSSH都编译安装到独立目录,不要动系统自带的任何openssl相关库。因为yum在安装或更新软件包时会重新生成依赖关系,一旦发现libssl.so.10被替换或删除,整个包管理器都可能瘫痪。
2.3 备份策略:不备份就升级,是给自己埋雷
源码编译升级OpenSSH,备份的重点不是"整个系统快照",而是下面这几个具体对象:
# 备份配置目录 cp -a /etc/ssh /etc/ssh.bak.$(date +%Y%m%d) # 备份关键二进制 mkdir -p /opt/ssh-backup/bin cp -a /usr/sbin/sshd /opt/ssh-backup/ cp -a /usr/bin/ssh /opt/ssh-backup/ cp -a /usr/bin/scp /opt/ssh-backup/ cp -a /usr/bin/sftp /opt/ssh-backup/ # 确认没有任何进程正在占用待替换的文件(正常sshd常驻时会占用,但备份不受影响) lsof /usr/sbin/sshd另外要多说一句:如果服务器上有大量存量会话,替换sshd二进制后,已经建立的连接不会断,新连接才会走新版本的sshd。所以备份完二进制后,不需要担心当前会话被切断。真正要担心的是restart服务之后新版本启动失败。因此,强烈建议操作前先打开一个"逃生"通道,比如云控制台的VNC、带外管理口,或者在另一个端口临时启动一个老版本的sshd实例。这样即使正式服务挂了,也不至于被锁在门外。
3. OpenSSL 3.0升级:单独编译安装,别碰系统自带库
OpenSSL升级是整个过程中最需要克制的一步。很多第一次做升级的人,下意识想的是"把系统自带的OpenSSL卸载了,装新版本上去"。这个想法在纯实验环境可行,在真实服务器上就是灾难。系统里几十个程序链接的都是/lib64/libssl.so.10这类旧库文件,你把它换成新版本,ABI不兼容,轻则curl断掉,重则整个系统网络栈崩溃。
3.1 为什么OpenSSH 9.8p1建议搭配OpenSSL 3.x
OpenSSH 9.8p1在configure阶段会检测OpenSSL版本,虽然它也能连旧版本的libssl,但从维护角度讲,OpenSSL 1.1.1和1.0.2都已经EOL,后续不会再收到任何安全补丁。如果继续用它们编译OpenSSH,等于把升级OpenSSH的努力浪费了一半:sshd是新的,底层加密库全是老的已知漏洞。
长期支持分支里我建议选择OpenSSL 3.0.x。它经过多年迭代,稳定性已经接近1.1.1的成熟度,同时又具备QUIC、FIPS模块等新能力。我写这篇文章时,3.0分支的小版本已经更新到3.0.15,建议下载这个版本或者去官网选3.0系列的最新补丁版本。别追大版本号,够用就好。
3.2 编译三步走:解压、config、make install
把源码下载到/usr/local/src下进行编译,是最经典也最清晰的路径:
cd /usr/local/src wget https://www.openssl.org/source/openssl-3.0.15.tar.gz tar xzf openssl-3.0.15.tar.gz cd openssl-3.0.15 ./config --prefix=/usr/local/openssl-3.0.15 \ --openssldir=/usr/local/openssl-3.0.15/ssl \ shared no-async make -j$(nproc) make install这里几个参数需要解释一下。--prefix指定了OpenSSL的安装根目录,我选择装到独立的/usr/local/openssl-3.0.15下面,这样所有文件都在一个目录里,后续回滚非常方便。--openssldir指定证书配置目录,OpenSSL会在这里放置openssl.cnf和证书目录结构。shared表示生成动态库,这是必须的,因为OpenSSH编译时默认会链接libcrypto动态库。no-async是很多人在编译时忽略的坑,它禁用异步I/O模块。在某些旧内核或者缺少完整内核头文件的环境里,async模块会编译失败,加上这个参数可以省掉很多折腾。
make -j$(nproc)是并行编译,nproc获取CPU核心数。如果服务器内存比较小,建议把并行度降一降,比如-j2,避免编译时内存被打满导致OOM。
3.3 动态库路径:ld.so.conf还是rpath
编译完OpenSSL后,最重要的一个环节是让系统找到新库。很多教程会教你把/usr/local/openssl-3.0.15/lib64写进/etc/ld.so.conf.d/,然后执行ldconfig。这个方案简单高效,但影响范围很大:只要程序加载了libcrypto.so.3,都会切到这个新版本。如果这个新版本和某个老程序不兼容,那个老程序就会开始报错。
更精细的方案是用rpath。在编译OpenSSH时,把新OpenSSL库的路径固化到可执行文件里,这样一来,只有ssh、sshd这些OpenSSH二进制会使用新OpenSSL库,其他系统程序仍然使用自带的旧库。这个隔离效果正好满足了"只升级目标服务"的需求:
export LDFLAGS="-Wl,-rpath,/usr/local/openssl-3.0.15/lib64 -L/usr/local/openssl-3.0.15/lib64" export CPPFLAGS="-I/usr/local/openssl-3.0.15/include"如果你的服务器用途比较单一,比如就是一个内网堡垒机,那用ld.so.conf方案也可以。但我个人的习惯是:能用rpath隔离就尽量隔离,避免"为了修OpenSSH的漏洞,反而引入了新的系统级隐患"。
3.4 验证新OpenSSL是否可用
编译安装完成后,用绝对路径执行新版本,确认它正常加载:
/usr/local/openssl-3.0.15/bin/openssl version # 预期输出:OpenSSL 3.0.15 31 Jul 2024 (Library: OpenSSL 3.0.15 ...)再检查一下动态库路径是否被正确嵌入到后续要编译的程序中。现在暂时不用管,等OpenSSH编译完,用ldd /usr/sbin/sshd检查libcrypto.so.3的来源,就能确认rpath是否生效。
4. OpenSSH 9.8p1编译安装:configure参数才是重头戏
OpenSSL就位后,开始编译OpenSSH。这个阶段真正决定成败的不是make那一下,而是configure参数是否匹配你的系统环境。参数配错,后面启动阶段会报出一堆莫名其妙的错误。
4.1 下载源码与准备编译依赖
OpenSSH 9.8p1的源码可以到OpenSSH官网或国内镜像站下载。建议下载到/usr/local/src统一管理:
cd /usr/local/src wget https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-9.8p1.tar.gz tar xzf openssh-9.8p1.tar.gz cd openssh-9.8p1编译依赖方面,CentOS 7和Ubuntu的命令完全不同。CentOS 7需要安装如下软件包:
yum install -y gcc make pam-devel zlib-devel perlUbuntu 22.04则需要:
apt update apt install -y build-essential libpam0g-dev zlib1g-dev这里有个容易忽略的点:因为我们要用新编译的OpenSSL,所以系统自带的openssl-devel或libssl-dev装不装都不影响OpenSSH编译。但zlib-devel是必须的,OpenSSH默认启用zlib压缩支持,缺少这个头文件,configure会提醒找不到zlib库。
4.2 configure关键参数逐一说明
下面是完整的configure命令,我加了很多注释,方便你对照自己的环境修改:
./configure \ --prefix=/usr \ --sysconfdir=/etc/ssh \ --with-ssl-dir=/usr/local/openssl-3.0.15 \ --with-pam \ --with-privsep-path=/var/empty/sshd \ --with-md5-passwords \ --with-kerberos5=/usr/lib64 \ LDFLAGS="-Wl,-rpath,/usr/local/openssl-3.0.15/lib64 -L/usr/local/openssl-3.0.15/lib64" \ CPPFLAGS="-I/usr/local/openssl-3.0.15/include"逐个讲清楚,这直接关系到后续能不能少踩坑:
- --prefix=/usr:指定安装目录为/usr。这样make install会把新的ssh、sshd、scp等二进制直接替换到/usr/bin和/usr/sbin下,当前shell的PATH立刻生效,不需要额外做符号链接。如果你更保守,可以装到/usr/local/openssh,然后手动维护PATH和systemd服务指向,但操作会复杂很多。
- --sysconfdir=/etc/ssh:配置文件目录。保持系统原有路径,能够直接沿用已有的sshd_config和host key。
- --with-ssl-dir=/usr/local/openssl-3.0.15:指定使用新编译的OpenSSL头文件和库。
- --with-pam:启用PAM支持。绝大多数Linux发行版在sshd中启用PAM是为了支持密码认证、账户锁定、审计等功能。如果不启用,可能会出现"能登录但last记录不到、登录失败但没有任何审计日志"等奇怪问题。
- --with-privsep-path=/var/empty/sshd:权限分离目录。OpenSSH通过把sshd拆分成特权进程和非特权进程来提高安全性,这个目录是非特权进程的临时运行空间,必须存在且权限正确,否则sshd启动时直接报错。
- --with-md5-passwords:允许使用MD5加密的密码字段。如果你系统里的用户密码是MD5格式而你想兼容它们,就加上这个选项。
- LDFLAGS和CPPFLAGS:这里就是我上一步提到的rpath隔离方案。让OpenSSH二进制运行时直接找到/usr/local/openssl-3.0.15/lib64下的libcrypto.so.3,而不会去抢其他程序用的旧库。
如果你在configure阶段遇到"OpenSSL version headers do not match library"之类的版本不一致报错,可以先确认/usr/local/openssl-3.0.15/include/openssl/opensslv.h和lib目录下的libcrypto确实来自同一版本。如果确认无误仍然报错,可以加一个--without-openssl-header-check跳过版本检查,但务必保证版本本身是一致的。
4.3 make与install:覆盖旧版本前先做三件事
configure通过后,执行编译:
make -j$(nproc)编译过程通常在几分钟内结束。在make install前,先把这三件事做完:
- 确认备份目录里的二进制齐全,见2.3节的清单。
- 查看一下当前系统sshd_config里是否存在自定义配置块。虽然9.8p1向下兼容大部分旧配置,但极少数废弃关键字会在sshd -t阶段就被拒绝。先心里有数,后面处理时就不会慌。
- 确认带外管理通道可用。至少确保有一个通过KVM、控制台或其他非SSH方式登录服务器的途径。
完成后执行安装:
make install安装过程会覆盖/usr/bin/ssh、/usr/sbin/sshd、/usr/bin/scp等文件。安装结束后,先不要立刻重启服务,先做一次配置语法检查:
/usr/sbin/sshd -t这一步非常关键。它输出为空说明配置正常,如果有问题,会明确指示哪一行配置有问题。看到报错先冷静,逐行解决,千万不要在重启服务后再去排查。
4.4 检查关键目录和权限
安装完成后,检查权限分离目录是否存在且属主正确:
mkdir -p /var/empty/sshd chown -R root:root /var/empty chmod 711 /var/empty chmod 755 /var/empty/sshd同时检查host key的权限。OpenSSH 9.8p1对host key的权限要求更严格,私钥权限必须是600,公钥可以644:
chmod 600 /etc/ssh/ssh_host_*_key chmod 644 /etc/ssh/ssh_host_*.pub检查完这些,用ldd再确认一下动态库链接到了新OpenSSL:
ldd /usr/sbin/sshd | grep ssl # 预期输出包含类似: # libcrypto.so.3 => /usr/local/openssl-3.0.15/lib64/libcrypto.so.3如果看到libcrypto.so.1.1或者libssl.so.10,说明rpath没有生效,这时候就检查configure时的LDFLAGS是否传入正确,重新编译。
5. 服务切换与验证:别把自己锁在门外
编译安装完成,但服务还是旧版在跑,zi只完成了80。最终的切换和验证环节,最容易出问题的地方往往不在"启动新版本"本身,而在于切换方式不够平滑。
5.1 先测试新sshd,再动正式服务
我不推荐直接systemctl restart sshd,除非你完全确认配置没有兼容性问题。更稳妥的方式是先开一个测试端口,手动拉起一个新版sshd实例:
/usr/sbin/sshd -f /etc/ssh/sshd_config -p 2222 -E /var/log/sshd-test.log这条命令会在2222端口启动新的sshd进程,并把日志输出到/var/log/sshd-test.log。然后从本机或另一台机器连接测试:
ssh -v -p 2222 root@127.0.0.1如果这个过程能正常弹出密码或密钥验证、看到Shell,说明新版sshd运行正常,可以放心替换正式服务。测试完毕,kill掉这个临时进程:
pkill -f "sshd.*-p 2222"注意这个pkill可能会匹配到别的进程,谨慎起见可以用ss -tlnp先查一下2222端口的PID,再精确删除。
5.2 重启正式服务并验证版本
确认测试无误后,正式重启服务:
systemctl restart sshd # 如果是SysVinit系统: # service sshd restart重新加载后,用下面几条命令核对版本、监听状态和进程信息:
ssh -V 2>&1 /usr/sbin/sshd -V 2>&1 ss -tlnp | grep :22 ps -ef | grep [s]shd tail -n 20 /var/log/secure 2>/dev/null || journalctl -u sshd -n 20此时ssh -V输出应该明确显示OpenSSH_9.8p1,并且日志里没有报错。
5.3 启动失败后的冷启动排查
冷启动必须提到台面上讲。如果sshd -t明明显示没问题,但restart后进程起不来,最常见的三个原因是:
- host key不存在或权限异常。表现为日志里出现"sshd: no hostkeys available"或"sshd: permission denied on key"。解决办法是重新生成host key:ssh-keygen -A。
- /var/empty/sshd权限不对。表现为"missing privilege separation directory"或"sshd: /var/empty/sshd must be owned by root and not group or world-writable"。照着4.4节修复权限即可。
- libcrypto动态库找不到。表现为"error while loading shared libraries: libcrypto.so.3: cannot open shared object file"。用ldd检查,确认rpath或ldconfig配置是否正确。
真到了无法救场的地步,回滚方案要能立刻执行。我用的是备份目录直接覆盖回去:
systemctl stop sshd cp -a /opt/ssh-backup/sshd /usr/sbin/sshd cp -a /opt/ssh-backup/ssh /usr/bin/ssh systemctl start sshd执行完这几条,服务会退回升级前的状态。回滚后不要急着离开,把配置文件改动也一并还原,然后重新检查一次sshd -t。
6. 升级后的配置兼容与安全收尾:别让9.8p1的优势打折扣
二进制升级成功,不代表整个工作就完结了。OpenSSH版本升级后,最常遇到的问题有两类:旧参数被废弃导致配置校验失败,以及新版本默认策略更严格导致某些旧登录方式失效。
6.1 旧版sshd_config在新版本下的兼容性处理
升级后,用这条命令查看实际生效的配置:
/usr/sbin/sshd -T它会输出所有生效配置项,即使你没写在sshd_config里。如果你在日志中看到类似"Deprecated option"的警告,直接编辑/etc/ssh/sshd_config,删掉对应行。常见的需要关注的关键字有过时的UseDNS、GSSAPIAuthentication等。
另外,9.8p1对公钥算法和主机密钥类型的要求比老版本更严格。如果之前还在使用DSA或较弱的RSA host key,建议直接生成新的ed25519密钥,并在sshd_config中明确HostKey路径。升级后我一般会用下面这组命令把host key刷新一遍,并保持配置一致性:
install -m 600 /dev/null /etc/ssh/ssh_host_ed25519_key ssh-keygen -A chmod 600 /etc/ssh/ssh_host_*_key /usr/sbin/sshd -t systemctl restart sshd6.2 9.8p1实际使用中给运维带来的变化
从我实机操作的经验看,9.8p1最直观的变化是编译产物对OpenSSL的依赖链路更敏感了。如果你按照本文的方案编译,会不会发现sshd二进制启动内存占用略高了那么一点,这正常,因为它使用的OpenSSL 3.0模块在加载时会有额外的初始化开销,不必过度担心。
另外,新版本对错误日志的记录粒度更细。例如网络上有人扫描22端口,以前可能只记录"Connection closed by authenticating user",9.8p1会记录更多连接细节和认证失败前的状态。这对日常安全审计是好事,但也意味着/var/log/secure或journald日志量会上升。建议提前配置logrotate,避免日志把磁盘占满。
6.3 加固三板斧:登录方式、密钥轮换、日志监控
升级版本只是排掉了已知的雷,真正的安全水位还要靠配置加固来维持。我每次完成sshd版本升级后,都会顺手做三件事。
第一件事是收紧登录入口。如果运维场景不需要密码登录,直接关掉密码认证:
# /etc/ssh/sshd_config PermitRootLogin prohibit-password PubkeyAuthentication yes PasswordAuthentication no第二件事是确认host key和用户authorized_keys的属主权限。sshd对.ssh目录权限要求很严格,用户目录的.ssh权限应该是700,authorized_keys是600,否则会出现"Authentication refused: bad ownership or modes"。
第三件事是增加失效熔断。在sshd_config中配置合理的MaxAuthTries和LoginGraceTime,降低暴力破解风险:
MaxAuthTries 3 MaxSessions 4 LoginGraceTime 30配置修改后同样要跑一遍/usr/sbin/sshd -t再重启。
最后再分享一个关于编译升级的小技巧
编译OpenSSH时,不同版本之间对configure参数的容忍度差异不小。我在CentOS 7上第一次编译9.8p1时,曾因为系统自带的PKG_CONFIG_PATH指向了老版OpenSSL的pc文件,导致configure阶段始终检测到1.0.2的头文件。当时排查了很久,最终找到的办法是在configure前把环境变量强制清干净:
export PKG_CONFIG_PATH=/usr/local/openssl-3.0.15/lib64/pkgconfig这个小细节看起来不起眼,但在跨版本升级场景下特别管用。如果你卡在configure的版本检测上,先检查PKG_CONFIG_PATH,再考虑加--without-openssl-header-check这类跳过参数。这样的问题往往一查一个准。