ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Rocky Linux 9.5 OpenSSH RPM升级加固脚本实战:从8.7p1到10.0p1

Rocky Linux 9.5 OpenSSH RPM升级加固脚本实战:从8.7p1到10.0p1

简介:面向 Rocky Linux 9.5 服务器的运维与安全加固场景,这套升级加固脚本包将 OpenSSH 升级至 10.0p1、OpenSSL 升级至 3.5.0,并以 RPM 方式集成,免去源码编译的繁琐步骤。脚本内置 sshd 服务自启动与状态检查逻辑,适合需要快速修复 SSH 高危漏洞、满足等保或合规要求的中高级运维人员。资源共 6 个文件、压缩包约 10.96 MB,包含 5 个 RPM 包和 1 个升级脚本。RPM 覆盖 openssh、openssh-clients、openssh-server 及 chkconfig、initscripts 等依赖组件,shell 脚本则负责一键执行升级、服务启停与加固配置,整体结构清晰,可直接上传服务器使用。目前已有 205 人学习下载。通过配套脚本与 RPM 组合,使用者可避开依赖冲突和编译报错,在 Rocky Linux 9.5 x86_64 环境上快速完成 SSH 版本替换与基础加固,并获得验证命令与状态输出参考,适合内网批量部署或应急升级场景。

1. 给 Rocky Linux 9.5 换一颗更稳的 SSH 心脏:这个 openssh 加固脚本在解决什么

如果你在维护 Rocky Linux 9.5 的机器,大概率已经收到过 openssh 加固的整改通知——系统默认带的 OpenSSH 8.7p1 在几轮高危 CVE 通报里都是点名对象,其中一个与权限绕过相关的问题甚至不需要认证就能触发拒绝服务。手动升级又是个麻烦事:直接源码编译安装,rpm -qa 查不到记录,下次 yum update 可能被系统包覆盖回去,等保审计时也拿不出像样的变更记录。这套脚本解决的就是这个尴尬:在 Rocky Linux 9.5 x86_64 上,用 RPM 方式把 OpenSSH 从 8.7p1 升到 10.0p1,同时配套 OpenSSL 3.5.0,并在升级完成后自动完成 sshd 加固配置。适合生产环境运维、等保整改负责人,以及需要在多台机器上批量交付同样 SSH 版本的人。

2. 从源码到 RPM 包:构建链路上的三个关键决策

2.1 为什么必须走 RPM 而不是 make install 一条路

源码编译安装 OpenSSH 并不难,难的是升级之后系统和包管理器不知道你干了什么。yum 的依赖数据库里记录的仍然是 8.7p1,后续任何涉及 openssh 的依赖检查都会基于这个错误前提,某次安全更新就可能把新的二进制覆盖掉,或者更糟——直接出现 libcrypto 版本对不上的半损坏状态。RPM 方式的好处是变更可追踪、可回滚、批量分发一致,这也是生产环境唯一稳妥的选择。

脚本构建链路的核心是 rpmbuild,先看一个最小可用的构建流程:

# 安装构建依赖 dnf install -y gcc make rpm-build openssl-devel krb5-devel \ pam-devel zlib-devel libselinux-devel audit-libs-devel # 准备目录结构 mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS} # 从官方源拉取 tarball cd ~/rpmbuild/SOURCES curl -O https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-10.0p1.tar.gz # 生成 spec 文件后执行构建 rpmbuild -ba ~/rpmbuild/SPECS/openssh.spec

这里有个关键点:spec 文件决定了一切。-ba参数会同时产出二进制 RPM 和源码 SRPM,前者用于安装,后者留档作为变更记录。构建目录的五个子目录各司其职:SOURCES 放 tarball 和补丁,SPECS 放 spec 文件,BUILD 是解压编译的工作区,RPMS 是最终产物,SRPMS 是源码包。这个结构是 rpmbuild 的硬约定,脚本里也是按这套流程组织的。

构建完成后检查 RPMS 目录,应该能看到 openssh-10.0p1-1.x86_64.rpm、openssh-server、openssh-clients 三个主包。如果只看到 openssh 主包而缺少 server 和 clients,说明 spec 里没有拆包,这种情况安装后 sshd 可能都起不来。

2.2 OpenSSL 3.5.0 的依赖处理:升级系统库还是捆绑进包

OpenSSH 10.0p1 对 OpenSSL 版本有硬性要求,Rocky Linux 9.5 默认带的是 OpenSSL 3.0.7,直接装新 SSH 会在运行时报出versionOPENSSL_3_5_0' not found` 一类的符号缺失错误。这是升级 SSH 时最常见的翻车点。

常见做法有两种。第一种是单独把 OpenSSL 也做一份 RPM 升级到 3.5.0,然后在 spec 的 Requires 字段里写明依赖版本。优点是干净,缺点是系统级 OpenSSL 升级风险面大,其他调用了 libcrypto 的服务都可能受影响,比如某些版本的 Python、curl、nginx 模块。我一般不建议在生产环境直接替换系统 OpenSSL,除非你有充足的时间做全量回归。

第二种更稳妥:OpenSSH 编译时链接到独立安装的 OpenSSL 3.5.0 路径,用 rpath 或 wrapper 脚本指向自定义安装目录,系统自带的 OpenSSL 完全不动。以 OpenSSL 3.5.0 为例:

# 自定义路径安装 OpenSSL ./config --prefix=/usr/local/ssl shared zlib make -j$(nproc) && make install # 编译 OpenSSH 时指定该路径 ./configure --prefix=/usr \ --with-ssl-dir=/usr/local/ssl \ --with-pam --with-zlib --with-md5-passwords

--with-ssl-dir参数是这里的核心,编译时 OpenSSH 会从这个目录找头文件和库文件。代价是运行时要确保动态链接器找得到这个路径,需要在 /etc/ld.so.conf.d/ 里加一行并执行 ldconfig。这套组合拳在脚本里是以打包后的 RPM 形式落地的,spec 里写死了构建期的--with-ssl-dir,装完 RPM 后自动装载依赖,审计时也能说清楚用的是哪份 OpenSSL。

2.3 spec 文件里容易被审计盯上的三个细节

RPM 打包的新手容易把 spec 写得太粗糙,装完能用就算完事,但安全审计会逐行看 %pre、%post、%preun 脚本。三个细节我每次都会检查。

第一是 %config 标记。sshd_config 属于配置文件,应该在 spec 里声明为%config(noreplace) /etc/ssh/sshd_config,这样后续升级包时如果管理员改过配置,rpm 不会直接覆盖而是生成 .rpmsave 文件。少了 noreplace,升级时本地配置会被静默冲掉,这是很多加固配置"莫名其妙消失"的元凶。

第二是 %post 脚本里的 host key 处理。OpenSSH 升级后如果系统里已经存在 /etc/ssh/ssh_host_* 密钥,不应该重新生成,否则所有已分发的公钥全部失效,需要重新同步 authorized_keys。正确逻辑是先判断密钥是否存在再决定是否生成:

if [ ! -f /etc/ssh/ssh_host_rsa_key ]; then /usr/sbin/sshd-keygen fi

第三是服务重启时机。%post 里不能用简单的systemctl restart sshd,因为升级过程中如果有远程会话在跑,重启会断开当前连接。常规做法是systemctl try-restart sshd或在 %preun 里记录服务状态,升级完成后再恢复。脚本里这一段的写法是:%preun 阶段停止服务前先判断是否开机自启,%post 阶段用systemctl enable --now sshd恢复。

3. 跑一遍升级加固脚本:预检、执行与配置落点

3.1 先把这几项查完再动手:预检清单

任何 SSH 升级都有把远程连接搞断的风险,所以预检不是走形式。我会在跑脚本前手动执行一遍下面的检查,确认当前系统状态足够安全。

# 确认系统版本与架构 cat /etc/rocky-release uname -m # 确认当前 OpenSSH 与 OpenSSL 版本 ssh -V openssl version # 确认磁盘空间,rpmbuild 过程中需要至少 3GB df -h /root # 确认没有正在运行的 SSH 会话以外关键任务 w

有一条隐蔽的血泪经验:如果服务器是通过 SSH 密钥登录的,升级前请确认当前 shell 里ssh-add -l能看到密钥,或者至少保证还有另一个登录途径(比如物理控制台或带外管理)。因为 OpenSSH 10.0p1 默认禁用了部分旧算法,升级后如果你本地的客户端太老,可能出现密钥协商失败、完全登不进去的情况。这不是脚本的问题,但脚本不会替你判断本地客户端版本,所以我都会在预检时把这句话写进执行文档里。

磁盘空间这一项容易被忽略。rpmbuild 编译 OpenSSH 和 OpenSSL 时,BUILD 目录里会有两套完整的源码树,加上编译中间产物,3GB 只是保守估计。之前在一台磁盘快满的机器上跑构建,最后阶段报No space left on device,RPMS 目录里躺着一个只有 200KB 的残缺包,装上去 sshd 直接起不来,只能重新构建。

3.2 脚本分四段跑:每段执行时你在终端会看到什么

脚本不像一键安装器那样黑匣子式执行,它是按阶段推进的,每个阶段有明确输出和退出码检查。拆解来看,核心流程是依赖安装、源码拉取与构建、RPM 安装、加固配置四段。

#!/bin/bash set -e # 阶段1:安装编译依赖 dnf groupinstall -y "Development Tools" dnf install -y openssl-devel zlib-devel pam-devel \ krb5-devel libselinux-devel audit-libs-devel # 阶段2:构建 OpenSSH RPM rpmbuild -ba /root/rpmbuild/SPECS/openssh.spec if [ $? -ne 0 ]; then echo "RPM build failed, check /root/rpmbuild/SPECS/openssh.spec" exit 1 fi # 阶段3:安装 RPM 包 rpm -Uvh /root/rpmbuild/RPMS/x86_64/openssh-*.rpm # 阶段4:加固配置 /usr/local/bin/ssh_hardening.sh

逐段说明一下。阶段1的set -e保证任何一步失败就终止,不会带着残缺依赖继续。阶段2的退出码检查是防呆,rpmbuild 有时候报了 warning 但退出码还是 0,这个检查至少能拦截构建失败的情况。阶段3 用-Uvh而不是-ivh,区别在于前者是升级语义,会正确处理 spec 里的 %pre/%post 脚本,后者遇到已安装的同名包会报错退出。

值得留意的是阶段4,加固配置脚本在 RPM 安装完成后单独执行,这意味着即使加固脚本本身写得有问题,SSH 服务本身已经处于可登录状态,不会出现因为加固脚本 bug 导致服务完全断开的极端情况。

3.3 加固配置自动写入的 sshd_config 项目与参数含义

加固脚本写的是安全基线,不是定制需求。它做的是把 sshd_config 里默认宽松的项收紧,每一项的参数都是经过考量的。以下是脚本实际落地的关键项:

配置项写入值说明
PermitRootLoginno禁止 root 直接登录,管理员必须通过普通用户 su 切换
PasswordAuthenticationno关闭密码登录,只允许密钥
PubkeyAuthenticationyes显式开启公钥认证,避免关闭密码后误伤
MaxAuthTries3单次连接最大认证尝试次数,防暴力破解
ClientAliveInterval300每300秒向客户端发送存活探测包
ClientAliveCountMax2连续2次探测无响应即断开连接
Protocol2显式限定仅 SSHv2
X11Forwardingno关闭 X11 转发,减少攻击面
AllowTcpForwardingno关闭 TCP 转发,按需可改 yes

这些项的取舍核心:PasswordAuthentication 设成 no 是加固力度最大的单项,但前提是公钥已经在目标机器上布置好。如果你在跑脚本前没有把公钥推到 authorized_keys 里,这条配置会让你瞬间失联。脚本对这个风险的处理方式是在执行前自动检查 root 的 authorized_keys 是否存在,不存在则警告并中止加固段,而不是盲目写入。MaxAuthTries 设 3 是平衡点,设成 1 在自动化工具批量重连时会经常触发熔断。

另外 ClientAliveInterval 和 ClientAliveCountMax 组合起来的效果是:一个闲置或已断开的会话最长存活 600 秒就会被服务端清理,这对防止孤儿进程占用连接数很有用。如果你有长时间跑任务的场景,这两项需要按实际情况放宽,否则任务还没跑完会话就被掐了。

4. 升级之后:验证系统状态与周边组件的连带排查

4.1 新版本装完后第一件事:确认 sshd 真正跑起来的是新二进制

rpm -Uvh 成功后,很多人直接敲ssh -V看到 10.0p1 就以为完事了。但 ssh -V 显示的是客户端版本,服务端是否真的用新版运行需要单独确认。而且系统里可能存在多个 sshd 二进制,比如 /usr/sbin/sshd 和 /usr/local/sbin/sshd,升级脚本装到 /usr 前缀后,旧的源码编译版可能还在 /usr/local 下躺着,systemd 启动的到底是哪个,得看单元文件。

# 确认服务端版本 sshd -V # 查看 systemd 单元文件里写的启动路径 systemctl cat sshd # 确认实际运行的进程路径 ps -ef | grep sshd # 确认监听端口状态 ss -tlnp | grep :22

ps 输出里如果有多个 sshd 进程,注意看主进程的路径。systemctl cat sshd显示的 ExecStart 行是权威来源,脚本封装后的 RPM 会通过 %post 覆盖这个单元文件指向新二进制路径。如果发现还有进程跑在旧的 /usr/local 路径下,说明系统里存在残留的源码安装版本,需要手动停掉旧进程并 kill 掉,否则 22 端口会被旧进程占着,新 sshd 起不来只会报 Address already in use。

这里有个值得养成的习惯:升级后第一次连接新会话时,故意用ssh -v(小写 v)看客户端握手阶段的版本交换信息,服务端回显的版本字符串里包含 OpenSSH_10.0p1 才算真正生效。这是最快也最直接的确认方式,比看任何配置文件都可靠。

4.2 对 sftp、scp、rsync 的连带影响与配置对齐

OpenSSH 10.0p1 的子系统路径和默认行为跟 8.7p1 有差异,特别是 sftp-server 的路径可能变化。升级后首先检查 sshd_config 里的 Subsystem 行:

grep -i subsystem /etc/ssh/sshd_config

如果显示的是sftp-server而没有绝对路径,需要确认这个二进制能被找到。在新 RPM 包里,sftp-server 通常位于 /usr/libexec/openssh/sftp-server,与旧版的 /usr/libexec/sftp-server 不同。路径写错会导致客户端连上后立即断开,报subsystem request failed on channel 0。这类问题在客户端工具里表现为 sftp 和 scp 全部失效,但 SSH 本身正常,排查时容易绕弯路。

scp 的兼容性坑更隐蔽。新版 OpenSSH 默认启用了 SFTP 协议做 scp 传输(scp 命令底层调用 sftp 子系统),如果对端还是老版本且子系统配置有问题,scp 会卡在握手阶段。我在某次升级后就见过这种情况:SSH 登录正常,但 scp 从新版本服务器往老版本服务器传文件时直接失败。当时的解决办法是在 scp 命令加-O参数强制走老式 SCP 协议,后续等对端升级后再去掉。这个参数在新版手册里有明确说明,如果你维护的机器阵列版本不齐,需要在自动化脚本里按目标端版本动态决定是否加 -O。

rsync 走 SSH 通道时同样受影响,具体表现是 rsync 进程可以启动但传输中途断开,日志里能看到rsync: connection unexpectedly closed。这种问题一般不是 rsync 本身的 bug,而是 SSH 层在传输大文件时触发了自动断开逻辑。对照第 3 章的 ClientAlive 参数,传输大文件时如果停顿超过空闲判定阈值,服务端会主动断开。遇到这种情况,需要单独给 rsync 走的会话配置更宽松的空闲策略,最直接的办法是为某类用户单独写 Match 块,或者用 sftp 的限速参数平滑传输节奏。

4.3 SELinux 与防火墙策略的连带检查

Rocky Linux 9.5 的 SELinux 默认 enforcing,OpenSSH 升级后 sshd 的类型标签如果不对,会直接拒绝绑定 22 端口或读取密钥文件。判断方式是按顺序执行下面三步检查。

# 查看 sshd 进程的 SELinux 上下文 ps -eZ | grep sshd # 确认端口上下文 semanage port -l | grep ssh # 检查是否有相关 AVC 拒绝日志 ausearch -m avc -ts recent | grep sshd

按 RPM 方式安装后,文件上下文一般会自动正确,因为 spec 里 %post 会调用 restorecon 或 setsebool 修正。但如果你的部署环境做过自定义策略,升级后可能出现 sshd_t 域无法绑定自定义端口的情况,报错通常是Permission denied但在日志里却看不到明显异常。这时候ausearch是唯一能定位问题的工具。注意 semanage 需要安装 policycoreutils-python-utils 包,否则命令会提示找不到。

防火墙的策略一般不用动,因为端口没变。但如果你的 sshd_config 里监听了非 22 端口,而 firewalld 规则是按端口 zone 划分的,升级本身不会影响防火墙,但你要确认放行规则仍然存在。常见的坑是有人为了加固把 ssh 端口从 22 改成 2222,却忘了在 firewalld 里加规则,升级重启后 SSH 无法连接,屏幕上的报错还会误导你往 sshd 配置方向排查。

5. 加固中的常见问题:五条血泪经验与排查手段

5.1 远程连接中断,且无法再连上

现象:执行 rpm -Uvh 或加固脚本后,当前 SSH 会话无响应,断开后尝试重连被拒绝。

原因:最常见的是升级过程中 sshd 服务被重启,现有连接被杀;其次是 PasswordAuthentication 被设为 no 但公钥没有正确写入目标是 authorized_keys;还有一种情况是 sshd_config 里有语法错误导致服务起不来。

解决:先通过带外管理或现场控制台登录,执行systemctl status sshd -l查看具体报错。如果起不来,用sshd -t检查配置文件语法,它会精确指出哪一行有问题。公钥问题则检查 /etc/ssh/authorized_keys 的属主和权限,必须是 root:root 且权限为 600 或 644,属主不对 sshd 会拒绝读取。这条问题我至少遇见过三次,每次都出在不同机器的目录迁移上。

5.2 RPM 安装提示 openssl 版本冲突

现象:rpm -Uvh 报错openssh-10.0p1 requires libcrypto.so.3()(64bit)或类似符号缺失,安装中断。

原因:OpenSSH 10.0p1 链接了 OpenSSL 3.5.0 的库文件,但系统 OpenSSL 还在 3.0.7,动态链接器找不到需要的符号。这正对应当初为什么在 spec 里选择捆绑方案而非升级系统 OpenSSL——如果直接升级了系统 OpenSSL,报错不会出现在这里,而是出现在其他依赖旧版本库的服务上。

解决:确认编译时确实使用了--with-ssl-dir指向的独立 OpenSSL 路径,然后检查 /etc/ld.so.conf.d/ 下是否有对应配置文件。执行ldconfig -p | grep libcrypto看解析路径是否指向 /usr/local/ssl。如果指向的仍然是系统库路径,说明 ldconfig 缓存没刷新,手动执行echo "/usr/local/ssl/lib64" > /etc/ld.so.conf.d/ssl3.conf && ldconfig再重试安装。

5.3 sshd 服务起不来:Syntax error 或 Bad SSH2 cipher spec

现象:加固脚本执行后 sshd -t 报语法错误,或者启动时提示某个 cipher 或 MAC 算法无法识别。

原因:新版 OpenSSH 10.0p1 移除了一批不再安全的算法,比如 diffie-hellman-group1-sha1 这种已经在编译期被弃用的密钥交换算法。如果你在旧版本里手动配置过这些算法名称,新版会直接拒绝启动而不是默默忽略。还有一种情况是配置项本身在新版里改名了,比如某些 Rhosts 相关的指令。

解决:逐个检查 sshd_config 里与算法相关的行,把不可识别项删掉。用ssh -Q cipher、ssh -Q mac、ssh -Q kex三个命令列出现版支持的全部算法清单,然后与配置文件里的限定项做对比。你可能会发现旧配置里的aes128-cbc已经被移除,需要用aes128-gcm@openssh.com这类替代。排查时最快的办法是二分法注释:注释掉一半可疑配置,重启看是否恢复正常,再逐步缩小范围。

5.4 编译阶段报错:找不到头文件或符号

现象:rpmbuild 执行到 configure 或 make 阶段报fatal error: openssl/ssl.h: No such file or directory,或者链接阶段报找不到-lcrypto。

原因:构建依赖没装全。openssl-devel 这个包是必须的,但它提供的头文件版本可能与你--with-ssl-dir指向的路径不一致。另一种情况是安装了 openssl-devel 但pkg-config环境变量没有指向新版本路径,configure 脚本误用了系统默认的头文件位置。

解决:先确认 openssl-devel 已安装,然后检查 /usr/local/ssl/include 下是否有 openssl/ssl.h 头文件。configure 阶段加上PKG_CONFIG_PATH=/usr/local/ssl/lib64/pkgconfig环境变量,强制 pkg-config 找到正确版本信息。如果还报错,可以直接在 configure 参数里加CPPFLAGS=-I/usr/local/ssl/include LDFLAGS=-L/usr/local/ssl/lib64,手动指定头文件和库路径。这个问题的本质是构建环境的 include 路径优先级没理清,属于编译基础问题,但新手很容易卡在这里。

5.5 加固后控制台登录也变慢,每次连接等十几秒

现象:打开 SSH 连接后要停顿 5 到 15 秒才出现密码或密钥提示,但连接一旦建立后一切正常。

原因:这是反向 DNS 解析导致的。sshd 默认会对客户端 IP 做 PTR 反查,如果客户端 IP 没有配置反向解析记录,或者 DNS 服务器响应超时,sshd 会一直等到超时才会继续。加固配置里如果打开了 UseDNS 相关的参数或在日志里开了高 verbosity,这个问题会特别明显。

解决:在 sshd_config 里加UseDNS no并重启 sshd。新版 OpenSSH 默认已经不启用 UseDNS,但如果配置文件里有显式UseDNS yes,它会覆盖默认行为。检查/etc/resolv.conf里的 DNS 配置,确认没有配置不可达的 nameserver。这是 SSH 连接慢排查里最经典的一项,却经常被忽视。修完之后连接速度应该立即恢复到 1 秒以内。

6. 用 ssh-audit 量化加固结果:验证与回滚的完整闭环

升级加固做完不等于安全,你得能拿出证据。ssh-audit 是命令行审计工具,它连接目标 SSH 服务后自动探测协议版本、密钥交换算法、主机密钥类型、加密算法和 MAC 算法,并按安全等级打分。跑一遍就知道你的加固配置到底漏了什么。

# 安装 ssh-audit pip3 install ssh-audit # 对本地加固后的 sshd 做一次完整审计 ssh-audit 127.0.0.1

执行后重点看两处:算法列表里是否还有ssh-rsa(SHA1 签名的旧版)和diffie-hellman-group14-sha1之类的弱算法;以及最后的评分是否达到Overall: pass。如果仍然出现弱算法,说明 sshd_config 里的算法限定不够严格,需要在配置里用KexAlgorithms、Ciphers、MACs三项显式列出白名单。版本升级能解决系统二进制层面的漏洞,但算法策略必须靠配置控制,这是两个独立维度。

回滚方案也要一起准备好。脚本的 RPM 安装方式让回滚比源码编译简单得多,但前提是升级前把 RPM 包备份到独立目录,而不是构建完立刻删掉。回滚命令是rpm -Uvh --oldpackage,它会把包回退到之前记录的版本:

# 备份当前 RPM 包 mkdir -p /var/backup/ssh-rpm && cp /root/rpmbuild/RPMS/x86_64/openssh-*.rpm /var/backup/ssh-rpm/ # 回滚到旧版本(需要旧版 RPM 在手) rpm -Uvh --oldpackage /var/backup/ssh-rpm/openssh-8.7p1-1.x86_64.rpm

回滚后同样要跑一遍 sshd -t 检查配置兼容性,因为新版的 sshd_config 里可能有旧版不认识的指令,比如某些新版新增的算法配置项。如果旧版无法解析,会直接拒绝启动。

从那以后,我每次升级 SSH 都强制走一遍完整闭环:预检、构建、安装、验证、审计、备份回滚包。这套流程保证了即使新版本出了幺蛾子,也能在五分钟内恢复原样,不至于半夜被叫起来救火。这份脚本把前四步自动化了,剩下两步你自己掌控就行。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进