1. 漏洞与修复选型背景
1.1 最近暴露的 OpenSSH 高危漏洞到底威胁在哪
这两年 OpenSSH 的漏洞报告就没消停过。我这边实际遇到的情况是,第三方漏扫工具一到月底就会拉出一批“高危”清单,几乎每台 CentOS 7.9 的机器都躲不开。仔细看漏洞编号,最常命中的是 CVE-2023-38408、CVE-2023-51385,还有些环境因为是后升级过的 OpenSSH 8.x、9.x 中间版本,会额外命中 CVE-2024-6387。
这几个漏洞的性质完全不同,一个是攻击面比较窄但一旦触发就是远程代码执行,另一个是配置污染导致的信息泄露风险,还有一个是信号处理器竞态问题。CVE-2023-38408 针对的是 ssh-agent 转发场景,如果客户端开启了 Agent Forwarding,攻击者可以间接调用本机已有的 ssh-agent 私钥操作接口,配合系统里的动态库完成代码执行。CVE-2023-51385 则更底层,它和 ssh 命令解析参数、authorized_keys 指令时的转义处理有关,利用精心构造的命令名或环境变量,有机会在登录时执行额外命令。CVE-2024-6387 就是 2024 年夏天那个 regreSSHion,sshd 的 SIGALRM 信号处理里存在条件竞争,在 glibc 环境下被绕过的概率被放大了,理论上不需要账号密码就能拿到 shell,虽然利用门槛和时间窗口都比较苛刻,但漏扫工具不会管你这些细节,看到版本号低于修复版本就直接报高危。
很多运维同学第一反应是“我服务器只是内网,也没开放公网 SSH,应该没事”。但内网横向渗透恰恰最喜欢这种存量漏洞,尤其 CentOS 7 这种已经停止运维更新的系统,一旦被拿到任意一个低权限入口,后续提权路径基本都是现成的。所以别赌“没人打”,先把版本拉上去是唯一稳妥的办法。
1.2 为什么 CentOS 7.9 的 OpenSSH 不能靠 yum 修复
CentOS 7.9 自带的 OpenSSH 是 7.4p1,这个版本在系统光盘和默认 yum 源里已经封版了,官方仓库不会再更新。也就是说,你执行多少次yum update openssh,得到的还是那个老版本,只是把一些小补丁打上,CVE 列表里的修复版本号根本不会变。这是 CentOS 7 生命周期进入尾声后的一个痛点:核心软件停留在旧版本,安全修复不再持续。
有人会想,那换第三方 yum 源行不行?我早几年也这么干过,后来踩坑踩怕了。第三方源里的 OpenSSH rpm 包,一是更新节奏不稳定,二是有可能跟系统里的 PAM、Kerberos、SELinux 策略版本打架,最麻烦的是它一旦替换了 openssh 相关 rpm 包,后续系统升级或者安装其他软件时可能出现依赖冲突。比如某些源提供的 OpenSSH 9.x rpm 包依赖更高版本的 openssl-libs,而 CentOS 7 的 openssl-libs 停留在 1.0.2k,你为了装一个包被迫去动系统 OpenSSL,风险就不是一个 SSH 服务能兜得住的。
还有一类思路是用高版本 OpenSSH 的 Docker 容器做端口映射,或者在 SSH 前面加一层代理跳板。但这样只是把问题往后推,宿主机本身的 sshd 还是暴露的,而且容器方案对已经批量部署在裸机或者虚拟机上的存量环境来说,改造工作量太大,很多内网环境连 Docker 都没有。所以实际生产场景里,源码编译升级 OpenSSH 仍然是主流方案,可控性最强,不依赖外部源,出了问题也可以快速回滚。
1.3 三套修复方案对比与最终选择
我把能走通的方案粗略列一下,给还没拿定主意的同学做个参考:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 源码编译升级 OpenSSH | 版本完全可控、不依赖第三方源、可自定义编译参数 | 每次升级要重复编译、需要解决 OpenSSL 依赖 | 存量 CentOS 7.9 批量修复 |
| 第三方 yum 源 rpm 替换 | 安装快、能走 yum 管理 | 源可靠性不一、依赖可能冲突、审查困难 | 少量机器、网络环境允许 |
| Docker / 代理跳板 | 不动宿主机 SSH | 改造大、宿主机仍暴露、维护复杂 | 新环境从零规划 |
最终我选择源码编译,而且是把新版 OpenSSH 和新版 OpenSSL 一起编译,其中 OpenSSL 装到独立目录,不覆盖系统自带的 OpenSSL。这样有两个直接好处:一是 OpenSSH 9.6 以上版本对 OpenSSL 1.1.1 有硬性要求,系统自带的 1.0.2k 编译不过去;二是把新版 OpenSSL 放在/usr/local/openssl这种独立前缀下,不会影响系统里依赖旧 OpenSSL 的 yum、curl、wget 等组件,风险面被控制住了。
这套方案我在几十台机器上反复执行过,配合后面的完整脚本,单台机器从准备依赖到验证完成,大约十五分钟,如果只是修漏洞不额外调优的话,时间还能更短。
2. 修复脚本的设计思路与关键决策
2.1 脚本整体流程图解
在写脚本之前,我先把整个升级流程拆成了下面几个阶段,每一步都有明确目的:
- 环境检查:确认系统版本、当前 OpenSSH 版本、Python/Perl 等编译辅助工具是否存在,避免中途报错。
- 编译依赖安装:通过 yum 安装 gcc、make、zlib-devel、pam-devel 等基础包,让源码编译环境可用。
- 独立安装新版 OpenSSL:编译 OpenSSL 1.1.1w 到
/usr/local/openssl,这是新版 OpenSSH 能编过的前置条件。 - 编译安装新版 OpenSSH:链接前面装好的 OpenSSL,同时启用 PAM、密钥权限分离等生产必要特性。
- 配置备份与恢复:把原有
/etc/ssh/sshd_config、Host Key 等关键文件备份,安装后用回原配置。 - 权限与 SELinux 修正:防止因为文件上下文变化导致 sshd 起不来。
- 配置校验与重启:用
sshd -t校验配置,再平滑重启服务并验证版本。
这个顺序看起来简单,但为什么把 OpenSSL 放在 OpenSSH 前面,为什么备份放在安装前后都要做,其实都有讲究。后面每个环节我都会展开讲。
2.2 为什么先编译新版 OpenSSL 再编译 OpenSSH
先说一个最容易踩的坑:很多人拿着网上的编译教程,下载 OpenSSH 9.6p1 的源码包,直接在 CentOS 7.9 上执行./configure && make,结果跑到一半报 “OpenSSL version is too old” 或者提示找不到openssl头文件,然后卡住了。根本原因是 CentOS 7.9 的系统 OpenSSL 是 1.0.2k,而 OpenSSH 9.x 的 configure 脚本已经明确要求 OpenSSL 1.1.1 以上。
这里我需要说明一下版本关系。OpenSSH 的源码编译不会自己去下载 OpenSSL,它只是检测系统里已有的 OpenSSL 开发库。CentOS 7 默认安装的openssl-devel对应 1.0.2 版本,老版本 OpenSSH 7.4 用它没问题,但新版 OpenSSH 在代码里开始使用一些 OpenSSL 1.1.1 才提供的 API 和常量。所以你必须先给编译环境准备一个满足版本要求的 OpenSSL。
有同学会问,那我直接把系统 OpenSSL 换成 1.1.1 不就行了吗?不行。CentOS 7 上大量系统组件(yum、curl、Python 等)依赖 OpenSSL 1.0.2 的 ABI,直接替换系统 OpenSSL 极易导致系统基础命令崩掉,甚至 SSH 都连不上。最稳妥的做法就是把新版 OpenSSL 装到一个独立目录,然后让 OpenSSH 的编译和运行都指向它。这也是脚本里把 OpenSSL 放到/usr/local/openssl并在启动时通过 rpath 指定动态库路径的原因。
2.3 备份与回滚:这步真的不能省
远程升级 SSH 服务有一点非常刺激:一旦 sshd 重启失败,你当前这台机器的 SSH 连接虽然不会立刻断,但你再也无法新建连接了。如果没有带外管理通道,就只能去机房或者让同事帮忙重启。所以脚本里我把备份和回滚逻辑放在很靠前的位置,而且备份的不是一个文件,而是完整的一整包。
/etc/ssh/整个目录:包含 sshd_config、ssh_host_rsa_key、ssh_host_ecdsa_key 等。/usr/sbin/sshd旧二进制:万一新版安装后有问题,可以整个换回去。- 系统 OpenSSL 状态记录:主要是让你知道系统原本的 openssl 包有哪些,回滚的时候别误删。
在make install之前备份好旧文件,是因为新版本安装时会覆盖/etc/ssh/下的默认配置模板。如果你不提前备份,原配置里自定义的端口、黑白名单、认证方式可能全部丢失,这比漏洞本身更麻烦。
2.4 前缀目录与证书路径怎么定
编译 OpenSSH 的时候,./configure里有几个参数不能随便填。我常用的组合是:
./configure --prefix=/usr --sysconfdir=/etc/ssh --with-pam --with-zlib --with-privsep-path=/var/empty/sshd--prefix=/usr是为了让安装后的 sshd 落在/usr/sbin/sshd,跟系统原来的位置一致。CentOS 7 的 systemd 单元文件/usr/lib/systemd/system/sshd.service默认 ExecStart 指向/usr/sbin/sshd,如果你把 prefix 设成/usr/local,新 sshd 装在/usr/local/sbin/sshd,要么改 service 文件,要么做软链,凭空多出一些麻烦事。
--sysconfdir=/etc/ssh是让新版本生成配置时也写到系统约定目录,否则会跑到/usr/local/etc/这种冷门路径,排查问题的时候很容易忽略。
--with-privsep-path=/var/empty/sshd是安全相关选项。OpenSSH 在设计上有一个权限分离机制,sshd 会 fork 出一个无权限子进程处理网络数据,/var/empty/sshd这个目录就是给这个子进程当 chroot 用的。CentOS 7 系统里已经存在这个目录,不需要额外创建,但编译时如果不指定,configure 可能会使用默认路径,在部分环境里会导致重启时目录不存在、sshd 起不来。
OpenSSL 的编译参数也要说明一下,我使用的是:
./config shared zlib --prefix=/usr/local/openssl --openssldir=/etc/sslshared表示生成动态库,zlib表示启用压缩支持。--openssldir=/etc/ssl是让 OpenSSL 的默认配置目录继续使用系统原来的/etc/ssl,这样 CA 证书路径不会错乱,避免后续 ssh 客户端去验证证书时找不到根证书。
3. 完整脚本与执行过程
3.1 前期环境检查与依赖安装
在跑自动化脚本之前,先手摸一下环境基础信息,可以少走很多弯路。执行的命令和参考输出如下:
cat /etc/redhat-release ssh -V uname -m正常情况下,你会看到类似这样的输出:
CentOS Linux release 7.9.2009 (Core) OpenSSH_7.4p1, OpenSSL 1.0.2k-fips 26 Jan 2017 x86_64确认是 7.9、老版本 OpenSSH 之后,先把编译要用到的系统包装上:
yum install -y gcc make perl pam-devel zlib-devel openssl-devel wget tar这里有一个很容易被忽略的点:openssl-devel这个包虽然是老版本,但 OpenSSH 编译时仍然可能需要它提供的头文件,因为某些底层兼容层要同时参考系统 OpenSSL 和新版 OpenSSL 才能顺利通过检测。当然,也有的环境不装它也能编过,但本着稳定复现的原则,我建议安装,反正它只是个开发包,不会替换系统运行库。
3.2 修复脚本全文
这是我在多台 CentOS 7.9 机器上反复用过的脚本,做了适度裁剪,去掉了环境里特有的邮件通知和告警逻辑,方便你根据自己的情况修改。直接复制到服务器上保存成upgrade_openssh.sh,然后bash upgrade_openssh.sh执行即可。
#!/bin/bash # ============================================================= # CentOS 7.9 OpenSSH 漏洞修复脚本 # 功能:源码编译升级 OpenSSH,并配套独立 OpenSSL # 用法:root 用户执行 bash upgrade_openssh.sh # ============================================================= set -euo pipefail # ---------- 可调参数 ---------- OPENSSL_VER="1.1.1w" OPENSSH_VER="9.8p1" OPENSSL_URL="https://www.openssl.org/source/openssl-${OPENSSL_VER}.tar.gz" OPENSSH_URL="https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-${OPENSSH_VER}.tar.gz" PREFIX_SSL="/usr/local/openssl" TMP_DIR="/tmp/ssh_upgrade_$$" LOG_FILE="/var/log/openssh_upgrade_$(date +%Y%m%d_%H%M%S).log" BACKUP_DIR="/root/ssh_backup_$(date +%Y%m%d_%H%M%S)" # ---------- 日志函数 ---------- log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG_FILE" } # ---------- 1. 前置检查 ---------- if [ "$(id -u)" -ne 0 ]; then echo "请以 root 身份执行" exit 1 fi mkdir -p "$TMP_DIR" "$BACKUP_DIR" cd "$TMP_DIR" log "开始执行 OpenSSH 升级修复" log "当前系统版本:$(cat /etc/redhat-release)" log "当前 OpenSSH 版本:$(ssh -V 2>&1)" # ---------- 2. 安装编译依赖 ---------- yum install -y gcc make perl pam-devel zlib-devel openssl-devel wget tar # ---------- 3. 备份现有 SSH 配置与二进制 ---------- cp -a /etc/ssh "$BACKUP_DIR/etc_ssh" if [ -f /usr/sbin/sshd ]; then cp -a /usr/sbin/sshd "$BACKUP_DIR/sshd.old" fi log "备份完成,备份目录:$BACKUP_DIR" # ---------- 4. 编译安装新版 OpenSSL 到独立目录 ---------- if [ ! -f "$PREFIX_SSL/lib/libssl.so" ]; then log "开始编译 OpenSSL ${OPENSSL_VER}" wget -q "$OPENSSL_URL" tar xzf "openssl-${OPENSSL_VER}.tar.gz" cd "openssl-${OPENSSL_VER}" ./config shared zlib \ --prefix="$PREFIX_SSL" \ --openssldir=/etc/ssl make -j"$(nproc)" make install cd "$TMP_DIR" log "OpenSSL 编译安装完成" else log "检测到 $PREFIX_SSL 已存在,跳过 OpenSSL 编译" fi # 让动态链接器能找到新版 OpenSSL 库 echo "$PREFIX_SSL/lib" > /etc/ld.so.conf.d/openssl-local.conf ldconfig # ---------- 5. 编译安装新版 OpenSSH ---------- log "开始编译 OpenSSH ${OPENSSH_VER}" wget -q "$OPENSSH_URL" tar xzf "openssh-${OPENSSH_VER}.tar.gz" cd "openssh-${OPENSSH_VER}" export PKG_CONFIG_PATH="${PREFIX_SSL}/lib/pkgconfig" export CPPFLAGS="-I${PREFIX_SSL}/include" export LDFLAGS="-L${PREFIX_SSL}/lib -Wl,-rpath,${PREFIX_SSL}/lib" ./configure \ --prefix=/usr \ --sysconfdir=/etc/ssh \ --with-pam \ --with-zlib \ --with-privsep-path=/var/empty/sshd \ --mandir=/usr/share/man make -j"$(nproc)" make install log "OpenSSH 编译安装完成" # ---------- 6. 恢复原有配置与密钥 ---------- cp -a "$BACKUP_DIR/etc_ssh/sshd_config" /etc/ssh/sshd_config cp -a "$BACKUP_DIR/etc_ssh/ssh_host_"* /etc/ssh/ 2>/dev/null || true cp -a "$BACKUP_DIR/etc_ssh/moduli" /etc/ssh/ 2>/dev/null || true chmod 600 /etc/ssh/ssh_host_*_key chmod 644 /etc/ssh/ssh_host_*_key.pub 2>/dev/null || true # ---------- 7. 修正权限与 SELinux 上下文 ---------- restorecon -Rv /etc/ssh /usr/sbin/sshd 2>/dev/null || true # ---------- 8. 校验配置并重启 sshd ---------- if sshd -t; then systemctl restart sshd sleep 2 systemctl status sshd --no-pager | head -15 log "升级完成,当前版本:$(ssh -V 2>&1)" else log "sshd -t 检查失败,尝试恢复旧二进制" if [ -f "$BACKUP_DIR/sshd.old" ]; then cp -a "$BACKUP_DIR/sshd.old" /usr/sbin/sshd restorecon -Rv /usr/sbin/sshd 2>/dev/null || true systemctl restart sshd fi exit 1 fi3.3 逐段解读核心逻辑
上面脚本里有几处细节,我单独拎出来讲清楚,免得你复制过去之后只知其然。
set -euo pipefail是我个人非常依赖的脚本开关。set -e让脚本在遇到非零退出码时直接停止,避免在依赖缺失的情况下继续编译,最后安装出一个半成品;set -u防止使用未定义变量,这在多个变量拼接 URL 时特别有用;pipefail保证管道命令中只要有一个环节失败,整条管道就算失败,不然make 2>&1 | tee log这种写法会把真正失败的动作吞掉。
PKG_CONFIG_PATH那一段是 OpenSSH 9.x 编译的关键。新版 OpenSSH 的 configure 脚本会通过pkg-config去查找 OpenSSL 的openssl.pc文件,而不是像老版本那样直接靠--with-ssl-dir指定路径。系统默认的 pkg-config 路径里优先是/usr/lib64/pkgconfig,那里面是 OpenSSL 1.0.2 的配置,如果不显式把/usr/local/openssl/lib/pkgconfig加进去,configure 检测到的还是老版本,一样会报错。脚本里我用了export,让这个变量在整个编译过程中生效。
LDFLAGS里的-Wl,-rpath,/usr/local/openssl/lib是给 sshd 写死运行期动态库搜索路径的。因为新版 OpenSSH 依赖的是我们编译的新版 libssl.so.1.1,而系统默认加载路径里没有它,如果不加 rpath,即使编译安装成功了,启动时也会因为找不到 libssl.so.1.1 而失败。加了 rpath 后,sshd 自己就知道去哪里找这个动态库,不用依赖设置LD_LIBRARY_PATH这种全局变量。
cp -a "$BACKUP_DIR/etc_ssh/ssh_host_"* /etc/ssh/这一步看起来简单,实际上把权限也可能覆盖回去。cp -a会保留源文件的属主和权限位。SSH 对私钥文件的权限要求很严格,ssh_host_rsa_key这类私钥必须是 600 权限且属主是 root,否则 sshd 启动时会直接报 “bad permissions”。后面我又手动执行了一遍chmod 600和chmod 644,属于双保险,毕竟备份文件在某些场景下可能被系统的 umask 改过。
3.4 执行与验证步骤
在测试环境上跑一次脚本,你会在日志里看到类似这样的关键输出:
[2026-01-10 10:22:35] 开始执行 OpenSSH 升级修复 [2026-01-10 10:22:36] 当前 OpenSSH 版本:OpenSSH_7.4p1, OpenSSL 1.0.2k-fips 26 Jan 2017 [2026-01-10 10:24:10] OpenSSL 编译安装完成 [2026-01-10 10:26:48] OpenSSH 编译安装完成 [2026-01-10 10:26:49] 备份完成,备份目录:/root/ssh_backup_20260110_102235 [2026-01-10 10:26:50] 升级完成,当前版本:OpenSSH_9.8p1, OpenSSL 1.1.1w 11 Sep 2023执行完成之后,不要着急关掉 SSH 窗口,先开一个新终端,用新会话测试登录,确认能正常进来再放行。这一步虽然很基础,但我见过太多人在同一个窗口里直接把 sshd restart,结果配置有问题,新连接进不去,只能走带外通道。我一般按下面的顺序做验证:
ssh -V systemctl status sshd ss -tlnp | grep :22 ssh -T -o StrictHostKeyChecking=no localhost tail -20 /var/log/securess -tlnp | grep :22是确认 sshd 确实在监听 22 端口,tail -20 /var/log/secure是看有没有 PAM、认证相关的报错信息。版本号看着是 9.8 了,但服务没有真正工作,一切都白搭。
4. 常见问题与排查心得
4.1 configure 阶段报 OpenSSL 版本过旧
脚本执行中如果卡在./configure之前,日志里出现OpenSSL version must be at least 1.1.1类似的提示,基本可以断定是PKG_CONFIG_PATH没有生效,或者新版 OpenSSL 没有编译成功。
排查思路先分两层:第一层,确认/usr/local/openssl/lib/pkgconfig/openssl.pc是否存在,如果不存在,说明前面make install没有正常完成,需要重新回去看 OpenSSL 编译日志;第二层,确认当前 shell 里的PKG_CONFIG_PATH是否真的指向了/usr/local/openssl/lib/pkgconfig。有些同学喜欢在网上抄一段命令手动执行,结果 export 只对当前终端生效,脚本一开新进程又丢失了。脚本里我用的是同一个进程内 export,所以不会有这个问题。
另外一个容易忽略的点是系统里同时存在多个openssl.pc时,pkg-config 的搜索顺序。PKG_CONFIG_PATH里的路径优先级高于默认路径,如果你把/usr/local/openssl/lib/pkgconfig加进去了,理论上会优先找到新版。但如果你在系统环境变量里设了别的PKG_CONFIG_PATH,或者configure脚本有缓存,那就要先把缓存清理掉再重新跑。
4.2 重启 sshd 后远程连接直接断开
这个场景通常分两种原因:一种是 sshd 配置校验通过了,但进程启动失败,导致端口没有监听;另一种是 sshd 启动了,但新版本对客户端加密算法、主机密钥类型有更严格的默认限制,老客户端连不上。
如果是第一种,登录服务器查看/var/log/messages和/var/log/secure,重点看有没有sshd: private host keys not found或者sshd: error while loading shared libraries。前者大概率是 host key 没有恢复成功,后者是LD_LIBRARY_PATH或 rpath 设置没生效,找不到 libssl.so.1.1。
如果是第二种,症状是端口在监听,但你当前正在使用的 SSH 客户端工具版本太老,比如用 Windows 2008 自带的 SSH 客户端去连接,协商过程中找不到共同支持的 KEX 算法,直接握手失败。解决办法是在 sshd_config 里显式放宽算法列表,但这种做法不推荐长期使用,最好还是升级客户端。
4.3 提示 bad permissions 或者无法加载 host key
新版本 OpenSSH 对私钥文件的权限检查比旧版更严格。旧版本可能只是警告一下,新版本直接罢工。如果你备份恢复时用了cp -a,权限一般不会出错,但有些时候管理员手动改过/etc/ssh目录属主,或者从别人那里拷过来的备份文件权限已经错了,就会报这个问题。
处理方式简单粗暴:
chmod 600 /etc/ssh/ssh_host_*_key chmod 644 /etc/ssh/ssh_host_*_key.pub restorecon -Rv /etc/sshrestorecon是配合 SELinux 的,CentOS 7 默认开启 SELinux,如果 SSH 文件的安全上下文不对,即使你有 root 权限也可能感觉“莫名其妙起不来”。
4.4 PAM 认证失败、密码正确也登不上
编译时已经带了--with-pam,系统/etc/pam.d/sshd也存在,但升级后密码登录报Permission denied,这种情况多出现在 sshd_config 里的UsePAM被默认值改变了。
CentOS 7 的默认UsePAM yes,但新版本 OpenSSH 安装后生成的新sshd_config模板在部分环境会以UsePAM no呈现,而脚本里面虽然最后恢复了原配置,但如果原来的sshd_config里根本没有显式写UsePAM,恢复回去之后依然是新版本的默认行为,这时就会用传统 shadow 密码方式认证,部分带特殊 PAM 模块的场景就会出问题。
解决办法是在/etc/ssh/sshd_config里显式加一行:
UsePAM yes然后sshd -t && systemctl reload sshd。这个坑非常隐蔽,我遇到过两三次,都是登录时密码明明正确却一直弹Permission denied, please try again才反应过来。
4.5 SELinux 引发的启动异常
有一种坑是,编译后的 sshd 二进制自带的安全上下文和旧二进制不一致,SELinux 不会允许它绑定 22 端口或者读取/etc/ssh下的敏感文件。脚本里已经跑了restorecon,但如果你自己改过 SELinux 布尔值,或者之前在/etc/ssh里放了一些非常规命名的文件,那么restorecon不会全部覆盖到。
最简单的排查流程:执行getenforce看当前是不是 Enforcing,然后ausearch -m avc -ts recent | tail -20看有没有 AVC 拒绝记录。有记录就针对具体路径执行restorecon,实在不行临时setenforce 0验证一下是不是 SELinux 的锅,但记得验证后马上开回来。
4.6 新版本不支持旧算法导致账号登不上
从 OpenSSH 7.x 升到 9.x,算法默认策略变化很大,尤其是 DSA、ssh-dss、部分老旧的 HMAC 和 KEX 算法被默认禁用。如果你有一批老运维终端还在用 DSA 密钥登录,升级后很可能直接报no mutual signature algorithm。
遇到这种情况,第一选择是重新生成 RSA 或 Ed25519 密钥对,替换客户端公钥;第二选择是在 sshd_config 里临时恢复旧算法,但需要在sshd -T之后确认客户端确实协商上了。我不建议长期开着旧算法,因为它本来就是漏洞挖掘的重点目标。
5. 最后再分享几个实战建议
脚本在测试环境跑通之后,批量上线前有几件事一定要做,都是我被现实教育出来的经验。
第一,务必保留一个带外通道。什么叫带外通道?就是机房 IPMI、云厂商的 VNC 控制台,或者一台不走 SSH 的跳板机。升级 SSH 的维护窗口里,所有远程连接都依赖新版本 sshd 正常启动,万一脚本在某个环节意外退出,你唯一的救命稻草就是控制台。哪怕是再熟练的运维,也不敢保证一百台机器全都能一次通过。
第二,分批执行,不要一次性把所有服务器全部升级。哪怕脚本已经在一台机器上验证过了,也要把服务器按业务重要性分组,先升级几台不重要的边缘节点观察一小时,再逐步推广。SSH 升级不像其他组件,它直接影响所有人的远程登录入口,出了问题就是事故。我习惯第一批先挑两台压测机,第二批是测试环境,第三批才是生产环境,每批之间隔至少半天。
第三,记录每台机器的原版本和备份目录位置。脚本已经把备份放在/root/ssh_backup_*下面了,但时间一长容易忘记哪台机器备份在哪。我一般会在升级完成后把BACKUP_DIR路径写进服务器资产表里,同时写一份ssh_upgrade.log留存,这样即使半年后发现问题,也能找到回滚所需的旧文件和完整日志。
刚才提到升级后 sshd 会用上新的 Host Key,如果你原本只配置了 RSA 密钥,建议顺手生成一份 Ed25519 密钥,算法强度更高,现代客户端都支持,也能避免以后客户端强制要求 Ed25519 时再次踩坑。
这套脚本和流程我修修补补用了将近两年,从最开始的手动逐条命令执行,到后来整合成脚本批量跑,踩过的坑基本都在上面几个常见问题里了。如果你的环境比这个更特殊,比如用了 LDAP、Kerberos 或者自定义 PAM 模块,建议在测试环境把对应认证方式完整测一遍再上生产,尤其是 PAM 那一段,最容易出幺蛾子。