Rocky Linux 9.5 OpenSSH 10.0p1升级加固与回滚
简介:面向 Rocky Linux 9.5(x86_64)运维人员的 OpenSSH 与 OpenSSL 升级加固套件,用于把系统自带的旧版 SSH 服务平滑替换为 OpenSSH 10.0p1 与 OpenSSL 3.5.0,并完成服务加固与开机自启配置;适合需要应对等保合规、漏洞整改或安全基线检查的中高级 Linux 管理员使用。压缩包共 6 个文件,约 10.96MB,由 5 个 rpm 安装包与 1 个 sh 脚本组成:rpm 覆盖 openssh 服务端、客户端及若干系统依赖组件,脚本把依赖校验、升级安装、配置调整与服务重启等流程串联起来,减少手工操作遗漏。资源已有 210 人学习下载。读者可据此获得一套可直接在 Rocky Linux 9.5 上验证的升级路径,理解由 SYSV 方式托管的 sshd 服务如何在新版本下正常启动,并参考脚本中的加固项与回退思路,快速完成从旧版本到 10.0p1 的迁移与安全加固。
1. 一台 Rocky Linux 9.5 被扫出 OpenSSH 老版本,需求只有两句话
机器是跑了两三年的 Rocky Linux 9.5 业务机,扫描报告点名的是系统自带的 OpenSSH 8.7p1 那一套算法与协议的老问题。给出的需求很直接:升到 OpenSSH 10.0p1,依赖链上的 OpenSSL 一起带到 3.5.0,交付物必须是能直接rpm -Uvh的 x86_64 包,另外附一份 sshd 加固配置。难点不在编译,而在“换心脏”这件事:系统里一堆东西挂在老的 libcrypto 上,升级完还得保证 SSH 能进来、能重启、能回滚。这套升级加固脚本解决的就是这个场景——把源码编译、RPM 重打包、sshd 加固、失败回滚串成一条能重复执行的路径,适合需要在内网做基线整改、又不愿意整机重装的运维和开发。
2. 脚本骨架拆解:从 vars.env 到 99-rollback.sh 的执行链
先把“这是什么东西”讲清楚。它不是一堆散落的命令,而是一个以vars.env为单一变量入口、按编号顺序执行的分阶段脚本集。你把它丢到任意一台 Rocky Linux 9.5 x86_64 机器上,改两个变量、跑一遍预检,就能知道这台机器能不能升;跑完全流程,得到的是两个 rpm 加上一份已经生效的加固配置。下面从依赖关系、目录结构、执行顺序三块拆开。
2.1 OpenSSL 3.5.0 与 OpenSSH 10.0p1 的依赖链,决定了能不能直接覆盖
很多人第一反应是“下载 tar 包,./configure && make install完事”。在 Rocky Linux 9 上这么干会翻车,原因在 soname。系统自带的 openssl 是 3.0.x 分支,产出的是libcrypto.so.3和libssl.so.3;OpenSSL 3.5.0 编译出来的库文件名同样是libcrypto.so.3。如果你把 3.5.0 装进/usr/lib64,等于用新库顶掉系统包里的同名文件,dnf 下次校验会报文件冲突,更麻烦的是所有链接libcrypto.so.3的组件(curl、python3、rpm 自己)都跟着换了 ABI,一旦哪个符号对不上,连锁反应就是系统级故障。
所以脚本里的设计是:OpenSSL 3.5.0 装进独立前缀/opt/openssl-3.5.0,不碰/usr/lib64;OpenSSH 编译时用--with-ssl-dir显式指向这个前缀;跑 sshd 之前通过/etc/ld.so.conf.d让动态链接器优先找到新路径。这样系统包不动,新 sshd 只依赖/opt下的库,出问题把/opt目录一删、rpm 回滚旧包就恢复。
至于为什么不干脆只升 OpenSSH、保留系统 OpenSSL 3.0.x:技术上可行,OpenSSH 10.x 对 OpenSSL 1.1.1 及以上都支持。但需求里点名要 3.5.0,而且 3.5 是 LTS 分支,后续几年不用再折腾一次。代价就是上面这套隔离方案,以及多一个 ld.so 配置项要维护。
2.2 目录结构:每个文件对应一个动作
解开脚本包,看到的目录长这样,文件数量不多,但职责切得很干净:
openssh-10.0p1-upgrade/ ├── vars.env # 全局变量:版本号、路径、端口、保留天数 ├── build.sh # 总入口,按参数调用下面各阶段 ├── scripts/ │ ├── 00-checkos.sh # 校验发行版、架构、磁盘余量 │ ├── 01-precheck.sh # 依赖包、现有版本、sshd 配置快照 │ ├── 02-build-openssl.sh # 编译安装 OpenSSL 3.5.0 到 /opt │ ├── 03-build-openssh.sh # 编译 OpenSSH 10.0p1,指向新 ssl │ ├── 04-rpmbuild.sh # 改 spec、rpmbuild 出包 │ ├── 05-install-swap.sh # 备份旧包、安装新包、重启 sshd │ ├── 06-harden.sh # 写入加固配置并校验 │ └── 99-rollback.sh # 从 backup/ 回滚到旧版本 ├── conf/ │ ├── sshd_config.hardened # 加固后的完整配置模板 │ └── openssh.spec.patch # 对发行版 spec 的版本与依赖修改 ├── src/ │ ├── openssl-3.5.0.tar.gz │ └── openssh-10.0p1.tar.gz └── backup/ # 运行时生成:旧 rpm、旧配置、旧二进制| 文件 | 作用 | 改它的时机 |
|---|---|---|
vars.env | 版本号、安装前缀、SSH 端口、备份保留数 | 换版本或换端口时 |
01-precheck.sh | 检查 gcc、rpm-build、pam-devel 等是否齐全 | 基本不改,报错时按提示补包 |
04-rpmbuild.sh | 修改 spec 的 Version 与 BuildRequires | 换 OpenSSH 版本时改版本号 |
06-harden.sh | 覆盖/etc/ssh/sshd_config.d/下的加固片段 | 增删加固项时 |
backup/目录是全流程的安全网。脚本在 05 阶段做的第一件事不是安装,而是把当前openssh、openssh-server、openssh-clients三个包的版本号和 rpm 文件记下来,把/etc/ssh整个打包成 tar。后面任何一步失败,99 脚本能凭这两份东西回到原点。
2.3 执行顺序:为什么编译必须排在校验之后
build.sh是一个很薄的分发器,核心就是把阶段串起来,并且在每一步之间做前置判断:
#!/usr/bin/env bash # build.sh —— 总入口,按阶段执行,支持断点续跑 set -euo pipefail source "$(dirname "$0")/vars.env" # 所有路径、版本号都来自这里 STAGE="${1:-all}" # all | precheck | build | pack | install | harden run_stage() { local name="$1" script="$2" echo "==> [$(date '+%F %T')] 开始阶段: ${name}" bash "${SCRIPT_DIR}/scripts/${script}" || { echo "!! 阶段 ${name} 失败,保留现场,可执行 99-rollback.sh 回滚" exit 1 } echo "==> [$(date '+%F %T')] 阶段 ${name} 完成" } case "${STAGE}" in precheck) run_stage "环境预检" "01-precheck.sh" ;; build) run_stage "编译 OpenSSL" "02-build-openssl.sh" run_stage "编译 OpenSSH" "03-build-openssh.sh" ;; pack) run_stage "RPM 打包" "04-rpmbuild.sh" ;; install) run_stage "安装替换" "05-install-swap.sh" ;; harden) run_stage "加固收敛" "06-harden.sh" ;; all) # 顺序不能乱:先确认环境,再编译,再打包,最后才动线上 run_stage "系统校验" "00-checkos.sh" run_stage "环境预检" "01-precheck.sh" run_stage "编译 OpenSSL" "02-build-openssl.sh" run_stage "编译 OpenSSH" "03-build-openssh.sh" run_stage "RPM 打包" "04-rpmbuild.sh" run_stage "安装替换" "05-install-swap.sh" run_stage "加固收敛" "06-harden.sh" ;; *) echo "用法: $0 [all|precheck|build|pack|install|harden]"; exit 2 ;; esac这段分发器的逻辑很朴素,但有两个细节值得说。一是set -euo pipefail配合每个子脚本独立退出码,任何一步非零就直接终止,不会出现“编译失败了还继续装包”的雪崩。二是STAGE参数支持分段执行——真实场景里编译和打包通常在一台构建机上做,装包在目标机上做,没必要每次从头跑一遍。
vars.env里的变量至少要包含这些,改版本号只动这一个文件:
# vars.env —— 单一变量入口 OPENSSH_VER="10.0p1" # OpenSSH 源码版本,注意带 p 的小版本写法 OPENSSL_VER="3.5.0" # OpenSSL 源码版本 SSL_PREFIX="/opt/openssl-${OPENSSL_VER}" # 独立前缀,绝不装进 /usr/lib64 SRC_DIR="/root/build/openssh-10.0p1-upgrade/src" BACKUP_DIR="/root/build/openssh-10.0p1-upgrade/backup" SSH_PORT="22" # 改动端口要同步 SELinux 与 firewalld KEEP_BACKUP_DAYS="30" # 备份保留天数,回滚窗口期变量说明上,SSL_PREFIX是整个方案的地基,所有编译参数和 ld.so 配置都引用它;SSH_PORT单独拎出来是因为改端口不只是改配置,还要动 SELinux 的ssh_port_t和 firewalld 的放行规则,散落在脚本里迟早漏一处。预检脚本会先读这两个值,把磁盘余量(编译 OpenSSH + 打包大约要 2GB 临时空间)、gcc 版本、是否装了rpm-build/pam-devel/libselinux-devel/audit-libs-devel逐条核对,缺什么直接打印安装命令,让人工补完再跑。
3. 编译与打包:从两个 tar 包到 ssh-10.0p1-1.el9.x86_64.rpm
这一章是整份脚本里最容易失败的部分,也是最能体现设计取舍的部分。三种做法摆在这里:装到/usr/local改 systemd 单元、直接make install覆盖系统文件、用 rpmbuild 重打包替换系统包。脚本选第三种,因为它让rpm -q openssh能查到正确版本,让 dnf 知道这个包的存在,也让回滚有标准动作可走。
3.1 编译 OpenSSL 3.5.0:独立前缀加 shared
第一步是把 OpenSSL 编成一个共享库形态、装在/opt下的产物:
#!/usr/bin/env bash # 02-build-openssl.sh —— 编译 OpenSSL 到独立前缀,不污染系统库目录 set -euo pipefail source "$(dirname "$0")/../vars.env" cd "${SRC_DIR}" tar -xzf "openssl-${OPENSSL_VER}.tar.gz" cd "openssl-${OPENSSL_VER}" # shared 生成 .so,zlib 启用压缩;--openssldir 决定默认配置与证书目录 ./Configure --prefix="${SSL_PREFIX}" \ --openssldir="${SSL_PREFIX}/ssl" \ --libdir="lib64" \ shared zlib \ -fPIC make -j"$(nproc)" make install_sw # 只装库和头文件,跳过文档,省时间也省空间 # 让动态链接器认识新路径,排在已有路径之前 echo "${SSL_PREFIX}/lib64" > /etc/ld.so.conf.d/openssl-${OPENSSL_VER}.conf ldconfig "${SSL_PREFIX}/bin/openssl" version -a # 自检:应输出 3.5.0几个参数值得掰开说。--libdir="lib64"是必须的,Rocky Linux 9 的 64 位库约定在lib64下,用默认的lib会让后面 ld.so 配置和 RPM 打包的路径对不上。shared生成动态库,如果编成静态库,sshd 会被链接成一个大二进制,后续想单独升级 OpenSSL 就得重编 sshd,失去隔离意义。make install_sw而不是make install,是因为后者会额外安装 man 手册和文档,几百 MB 下去对一台生产机毫无价值。
最后的ldconfig和自检不能省。我见过有环境因为/etc/ld.so.conf.d里残留了一条更早的相对路径,导致ldconfig之后找到的还是系统 3.0.x 的库,openssl version输出的却是新版本——因为直接执行的是/opt下的二进制。判断依据要改成ldd ${SSL_PREFIX}/bin/openssl | grep libcrypto,看它指向哪。
3.2 编译 OpenSSH 10.0p1:把 ssl-dir 指对,把 PAM 和 SELinux 带上
OpenSSH 的 configure 选项决定了后面能不能无缝替换系统包。系统自带的 sshd 是带 PAM、带 SELinux 支持、带 audit 的,自己编的时候漏了任何一个,替换后就是“能登录但会话环境不对”或者“登录直接被 SELinux 拦掉”:
#!/usr/bin/env bash # 03-build-openssh.sh —— 编译 OpenSSH,显式绑定新 OpenSSL set -euo pipefail source "$(dirname "$0")/../vars.env" cd "${SRC_DIR}" tar -xzf "openssh-${OPENSSH_VER}.tar.gz" cd "openssh-${OPENSSH_VER}" ./configure --prefix=/usr \ --sysconfdir=/etc/ssh \ --with-ssl-dir="${SSL_PREFIX}" \ --with-pam \ --with-selinux \ --with-audit=linux \ --with-zlib \ --with-privsep-path=/var/lib/sshd \ --with-default-path=/usr/local/bin:/usr/bin:/usr/sbin \ --disable-strip make -j"$(nproc)" # 确认二进制真的链到 /opt 下的新库,而不是系统 3.0.x ldd ./sshd | grep -E 'libcrypto|libssl'--prefix=/usr是关键。如果不写,默认装到/usr/local,产出的 sshd 在/usr/local/sbin/sshd,systemd 的sshd.service找的还是/usr/sbin/sshd,结果就是“装完了但跑的还是老版本”,这是最典型的自欺欺人。--with-pam必须开,否则/etc/pam.d/sshd里的会话限制、pam_nologin、密码策略全部失效。--with-selinux决定 sshd 会不会去设置登录会话的安全上下文,关掉之后在 enforcing 模式下大概率出现登录后家目录不可写。
--with-audit=linux让 sshd 把登录事件投递给 auditd,配合后面加固里的LogLevel VERBOSE,做审计时才拿得到“谁用什么密钥登录”的记录。--disable-strip保留符号,出问题时能 gdb 看一眼,代价只是几十 MB。
编完先别急着装。用编译目录里的 sshd 做一次配置语法检查:./sshd -t -f /etc/ssh/sshd_config,这一步能提前暴露配置项在新版本里是否已被废弃——OpenSSH 10.x 删掉了一批老指令和 DSA 相关算法,直接拿旧配置启动会报 “Bad configuration option”。
3.3 rpmbuild 重打包:改 spec 而不是硬塞二进制
有了编译产物,打包这步要做的是把二进制、配置文件、systemd 单元、PAM 配置按 RPM 的规矩装进包里,让rpm -q和 dnf 都认得。脚本的做法是拿发行版自带的openssh.spec做基底,改版本号和依赖,而不是从零写一份 spec:
#!/usr/bin/env bash # 04-rpmbuild.sh —— 基于发行版 spec 改版本后重打包 set -euo pipefail source "$(dirname "$0")/../vars.env" TOPDIR=/root/rpmbuild # 准备构建树,把源码包放进 SOURCES mkdir -p ${TOPDIR}/{SOURCES,SPECS,BUILD,RPMS,SRPMS} cp "${SRC_DIR}/openssh-${OPENSSH_VER}.tar.gz" ${TOPDIR}/SOURCES/ # 取发行版 spec:优先用本机已装源码包,没有则从仓库拉 srpm if [ ! -f ${TOPDIR}/SPECS/openssh.spec ]; then dnf download --source openssh -y --destdir /tmp/sshsrc rpm -i /tmp/sshsrc/openssh-*.src.rpm fi cp ${TOPDIR}/SPECS/openssh.spec ${TOPDIR}/SPECS/openssh.spec.orig # 改版本号与依赖:Version 字段不允许包含短横线,10.0p1 可以直接写 sed -i "s/^Version:.*/Version: ${OPENSSH_VER}/" ${TOPDIR}/SPECS/openssh.spec sed -i "s/^Release:.*/Release: 1.el9/" ${TOPDIR}/SPECS/openssh.spec sed -i "/^Patch[0-9]*:/d" ${TOPDIR}/SPECS/openssh.spec # 老 patch 在新版本上多半打不上 sed -i "/^%patch/d" ${TOPDIR}/SPECS/openssh.spec # 装依赖后开始构建,只出二进制包,跳过 debuginfo dnf builddep -y ${TOPDIR}/SPECS/openssh.spec rpmbuild -bb ${TOPDIR}/SPECS/openssh.spec \ --define "_topdir ${TOPDIR}" \ --define "debug_package %{nil}"为什么是“删 patch”而不是“改 patch”。发行版 spec 里通常挂着十几个补丁,处理密钥读取、SELinux 上下文、sshd-keygen 的发行版定制。这些补丁大多是基于 8.7 源码写的,直接往 10.0p1 上打,rpmbuild会甩出一堆Hunk #1 FAILED at 12。稳妥的顺序是:先全删,看编译能不能过;能过之后,只把确有必要的(比如 SELinux 相关的上下文设置)挑回来手工适配。功能层面的补丁在上游新版本里基本已经合并。
打包完成后产物在${TOPDIR}/RPMS/x86_64/下,至少包含openssh-10.0p1-1.el9.x86_64.rpm、openssh-server-...、openssh-clients-...。安装前先rpm -qpl看一眼文件清单,确认/usr/sbin/sshd在里面、没有意外的/usr/lib64/libcrypto.so.3被塞进去——如果 spec 的%files里误包含了新 ssl 的库路径,装上去就变成覆盖系统库的灾难现场。
4. 加固落地:sshd_config 之外的三个系统层开关
包出来了只是第一步,真正决定这台机器能不能扛住基线扫描的是配置。加固脚本分两层:sshd_config 里的协议与认证收敛,和系统层的 SELinux、firewalld、PAM 配合。两层缺一层,扫描器都能挑出问题。
4.1 sshd_config 收敛:从算法白名单到认证次数
OpenSSH 10.x 本身已经默认关掉了 DSA 和 SHA-1 签名,但基线检查通常要求显式写出算法白名单。加固配置模板放在conf/sshd_config.hardened,由 06 脚本拷进/etc/ssh/sshd_config.d/00-hardening.conf,避免直接改主配置导致升级时被覆盖:
# /etc/ssh/sshd_config.d/00-hardening.conf Port 22 Protocol 2 AddressFamily inet # 认证:禁用密码,只留公钥;root 仅允许密钥登录 PermitRootLogin prohibit-password PasswordAuthentication no PermitEmptyPasswords no PubkeyAuthentication yes MaxAuthTries 3 MaxSessions 4 LoginGraceTime 30 # 算法白名单:只保留 curve25519 与 ecdh,MAC 只留 ETM 模式 KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384 Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256 PubkeyAcceptedAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256 # 会话与转发收敛 X11Forwarding no AllowAgentForwarding no AllowTcpForwarding no PermitTunnel no ClientAliveInterval 300 ClientAliveCountMax 2 LogLevel VERBOSE逐条说几个容易踩的点。PermitRootLogin prohibit-password和PasswordAuthentication no是绑定的,只关密码但留着yes,等于给自己留了个爆破入口。PubkeyAcceptedAlgorithms那行是升级后最容易出事的地方——很多存量机器的authorized_keys里躺的是ssh-rsa(SHA-1 签名),10.x 默认不再接受,日志会明确写signature algorithm ssh-rsa not in PubkeyAcceptedAlgorithms。正确做法是把用户密钥换成 ed25519,而不是把ssh-rsa加回白名单。
KexAlgorithms只留 curve25519 和 nistp256/384,会直接排除掉一批老客户端(CentOS 6 时代的 ssh 客户端只支持diffie-hellman-group14-sha1)。如果环境里确实有老设备要连,把它单独拎出来评估,别为了省事整段放开。AllowTcpForwarding no也要看业务,有些跳板场景依赖端口转发,关掉会直接把业务弄挂——加固项不是越严越好,得对着业务清单一条条确认。
4.2 系统层配合:SELinux 端口、firewalld 富规则、PAM 会话
配置改完,如果系统层不动,大概率是“配置看着对,登录就是不行”。06 脚本里对应的动作是这几条:
#!/usr/bin/env bash # 06-harden.sh 节选 —— 系统层配套动作 set -euo pipefail source "$(dirname "$0")/../vars.env" # 1. 配置落盘并做语法检查,检查不过直接回滚配置文件 install -m 600 -o root -g root conf/sshd_config.hardened \ /etc/ssh/sshd_config.d/00-hardening.conf /usr/sbin/sshd -t || { echo "配置语法错误,撤销"; rm -f /etc/ssh/sshd_config.d/00-hardening.conf; exit 1; } # 2. SELinux:非 22 端口必须打上 ssh_port_t,否则 sshd 起不来 if [ "${SSH_PORT}" != "22" ]; then semanage port -a -t ssh_port_t -p tcp "${SSH_PORT}" 2>/dev/null || \ semanage port -m -t ssh_port_t -p tcp "${SSH_PORT}" fi restorecon -Rv /etc/ssh /root/.ssh /home >/dev/null # 3. 防火墙:只放行 SSH,并对同 IP 做失败次数限制 firewall-cmd --permanent --add-service=ssh firewall-cmd --permanent --add-rich-rule=\ "rule family=ipv4 service name=ssh limit value=10/m accept" firewall-cmd --reload # 4. 重载而非重启:SIGHUP 让主进程重新 exec,已建立的会话不受影响 systemctl reload sshd sshd -T | grep -E 'permitrootlogin|passwordauthentication|kexalgorithms'semanage port那条是硬要求。Rocky Linux 9 默认 enforcing,sshd 监听一个没有ssh_port_t标记的端口,SELinux 会直接拒绝 bind,systemctl status sshd只给你一句Permission denied,不看 audit 日志根本猜不到原因。
systemctl reload sshd和restart的区别值得单独强调。sshd 收到 SIGHUP 时主进程会重新读取配置并 exec 自身,已经建立的连接由 fork 出来的子进程维持,不会断。用 restart 则会把监听套接字整个关掉重开,虽然现有连接也不一定断,但在升级替换二进制的场景下,reload 才是那个“把新二进制加载进来又不踢人”的动作。前提是你知道自己当前这条连接是独立会话,不是靠某个正在被 reload 的进程转发。
4.3 验证:五条命令确认加固真的生效
改完配置不看输出,等于没改。验证清单固定成五条,写进脚本末尾,每次执行完打印一遍:
# 1. 版本与包信息:确认跑的是新包,不是残留的旧二进制 rpm -q openssh openssh-server openssh-clients /usr/sbin/sshd -V 2>&1 | head -1 # 2. 依赖检查:确认链接到 /opt 下的 OpenSSL 3.5.0 ldd /usr/sbin/sshd | grep -E 'libcrypto|libssl' # 3. 生效配置:-T 输出的是解析后的最终值,包含 include 进来的片段 sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|maxauthtries|logLevel' # 4. 算法协商:分别列出服务端支持的 kex/cipher/mac ssh -Q kex | head -5 ssh -Q cipher | head -5 # 5. 真实连接:用另一台机器带 -v 连一次,看协商到的算法 ssh -vv -p "${SSH_PORT}" user@127.0.0.1 exit 2>&1 | grep -E 'kex:|Server host key|Authenticated'第 3 条容易被忽略:sshd -T会把所有 include 展开后的最终配置打出来,是判断“我改的到底生效没有”的唯一可靠依据。直接cat /etc/ssh/sshd_config看到的可能只是主文件,加固片段在sshd_config.d/里,顺序不对还会被后面的文件覆盖。
5. 避坑与排查:升级过程中最容易翻车的五个现场
这套流程我在几台机器上跑过,失败点高度集中。下面五条按“现象 → 原因 → 解决”写,遇到问题先对号入座,能省掉大半排查时间。
5.1 五条高频故障的现象、原因与处理
第一条:sshd 起不来,日志报error while loading shared libraries: libcrypto.so.3: cannot open shared object file。现象是systemctl start sshd直接失败,ldd /usr/sbin/sshd显示libcrypto.so.3 => not found。原因是编译时链接到了/opt/openssl-3.5.0/lib64,但运行环境的 ld.so 缓存里没有这条路径——make install之后没跑ldconfig,或者/etc/ld.so.conf.d里的文件被误删。解决:确认/etc/ld.so.conf.d/openssl-3.5.0.conf内容为/opt/openssl-3.5.0/lib64,执行ldconfig,再ldd复核。更彻底的办法是编译时加-Wl,-rpath,/opt/openssl-3.5.0/lib64,把路径写进二进制的 RPATH,不依赖全局缓存。
第二条:升级完成,客户端连上来报no matching key exchange method found。现象是服务端日志里出现Unable to negotiate with x.x.x.x port 50022: no matching key exchange method found。原因是加固模板把KexAlgorithms收得太窄,对端是老版本客户端,双方没有交集。解决:先用ssh -Q kex确认服务端支持列表,再在对端执行ssh -vv看客户端提议了哪些,找交集后补进白名单。老设备的场景下更推荐单独开一个Match Address段落放行,而不是全局放宽。
第三条:升级后过几天,版本自己变回去了。现象是rpm -q openssh又显示 8.7p1。原因是 dnf 在一次常规更新里,从仓库拉了版本号更高的openssh-8.7p1-xx.el9(发行版的 Release 号一直在涨),把你的本地包顶掉了。解决:在/etc/dnf/dnf.conf或对应 repo 里加exclude=openssh* openssl*,或者用dnf versionlock add openssh。更规范的做法是把自建包放进本地仓库并设置 priority,让 dnf 认为它是合法更新源。这条属于血泪经验,不看日志根本发现不了。
第四条:rpmbuild中途失败,报一堆Hunk #N FAILED。现象是补丁打不上。原因是发行版 spec 里的 patch 针对的是旧源码。解决:把^Patch和%patch行全部注释掉,先跑通一次裸编译,再逐个把确有必要的补丁(通常是 SELinux 与 sshd-keygen 相关)挑回来手工 rebase。别硬改 patch 文件里的行号,上下文对不上时那样只是把问题往后推。
第五条:新配置生效后,密钥登录突然不通,日志只有一句Permission denied (publickey)。现象是密码能登(如果没关)、密钥不行,/var/log/secure里没有更多信息。原因通常是三个:家目录或.ssh权限过宽(group/other 有写权限 sshd 会直接拒绝)、SELinux 上下文不对、或者密钥类型是ssh-rsa被新版本拒了。解决:chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys && chmod 700 ~,然后restorecon -Rv ~/.ssh,最后把LogLevel调到VERBOSE重启再看日志,它会明确告诉你签名算法是否被拒。
5.2 出问题时的三条应急命令
真出事了别慌,按这个顺序来。第一步看服务状态和最近日志:systemctl status sshd -l --no-pager加journalctl -u sshd -n 100 --no-pager,90% 的启动失败在这里能看到明确原因。第二步做配置语法检查:/usr/sbin/sshd -t -f /etc/ssh/sshd_config,它会把具体哪一行有问题指出来,包括 include 进来的片段。第三步才是回滚:
#!/usr/bin/env bash # 99-rollback.sh —— 从 backup/ 恢复到升级前状态 set -euo pipefail source "$(dirname "$0")/../vars.env" OLD_VER=$(cat "${BACKUP_DIR}/old_version.txt") # 升级前记录的版本号 echo "准备回滚到 openssh-${OLD_VER}" # 1. 恢复配置并做语法检查,避免回滚后仍然起不来 tar -xzf "${BACKUP_DIR}/etc-ssh.tar.gz" -C / /usr/sbin/sshd -t || { echo "旧配置校验失败,人工介入"; exit 1; } # 2. 卸载新包、装回旧包(--oldpackage 允许降级) rpm -e --nodeps openssh-server openssh-clients openssh 2>/dev/null || true rpm -Uvh --oldpackage --nodeps "${BACKUP_DIR}"/openssh-${OLD_VER}*.rpm # 3. 重新生成主机密钥、恢复上下文、重启服务 ssh-keygen -A restorecon -Rv /etc/ssh >/dev/null systemctl restart sshd && systemctl status sshd --no-pager--oldpackage这个参数是回滚能成立的前提,RPM 默认拒绝安装比已装版本低的包。ssh-keygen -A补生成主机密钥,因为卸载包时/etc/ssh/ssh_host_*可能被%postun清掉,少了这个步骤回滚完 sshd 会因为找不到主机密钥起不来。
6. 二次开发:把一次性脚本改成可复用、可回滚的加固流水线
这套脚本跑通一次不难,难的是让它在你手上十几台机器上都能复现。我的改造思路是把“一条命令跑到底”拆成“状态可查、步骤可跳、失败可退”的三件事,落地成三层结构。
第一层是状态文件。在vars.env旁边加一个state/目录,每个阶段开始和结束时写一个标记文件,内容是时间戳和关键产物路径:
# 节选:阶段状态写入与读取,用于断点续跑 mark_done() { echo "$(date '+%F %T') $2" > "${STATE_DIR}/$1.done"; } is_done() { [ -f "${STATE_DIR}/$1.done" ]; } if ! is_done "openssl_build"; then bash scripts/02-build-openssl.sh && mark_done "openssl_build" "${SSL_PREFIX}" fi有了这层,编译失败重启后不会把 OpenSSL 从头编一遍,build.sh all变成幂等的。第二层是 precheck 的输出结构化,把每项检查结果写成key=value打到state/precheck.env,后面的阶段直接 source 它做判断——比如没装pam-devel就不该走到编译 OpenSSH,而不是编到一半失败。第三层是回滚点前置,05-install-swap.sh在动手之前先把备份做完并校验备份完整性(tar -tzf能读、rpm 文件能rpm -qpi),备份不成立就不许继续。
想再往上走一步,可以把每个阶段做成 systemd 的 oneshot 单元或者用 ansible 的serial: 1滚动执行,把“单机脚本”变成“批量流水线”。
| 阶段 | 幂等判据 | 失败后的处理 |
|---|---|---|
| 编译 OpenSSL | /opt/openssl-3.5.0/bin/openssl version输出匹配 | 删/opt前缀重编,不影响系统 |
| 编译 OpenSSH | src/openssh-10.0p1/sshd -V可执行 | 保留编译目录,只重跑 make |
| RPM 打包 | RPMS 目录下存在目标版本 rpm | 清理 BUILD 目录重打,不动已装系统 |
| 安装替换 | rpm -q openssh版本匹配 | 走 99 脚本回滚 |
| 加固收敛 | sshd -T输出与模板一致 | 删掉加固片段,恢复主配置 |
还有一个具体技巧:把加固模板做成“差异对比”而不是“整段覆盖”。06 脚本先用sshd -T导出当前生效配置,与模板逐项 diff,只对差异项做修改并打印变更清单,人工确认后落盘。这样在已有的生产机上做二次加固时,不会被一份通用模板把业务相关配置冲掉——比如某台机器有Match User的特殊段落,整段覆盖就没了。
我印象最深的一次,是图省事直接在唯一一条 SSH 会话里跑替换,reload 那一刻配置写错了一个参数,连接还在但新连接全部被拒,只能让人跑去机房接显示器。从那以后我每次动 sshd 之前都强制走一遍“先在 2222 端口起一个临时实例、用新配置跑sshd -t、再开第二条会话验证”的流程,哪怕多花五分钟。这套脚本里的预检和临时实例校验,就是那次教训固化下来的产物。希望帮到你。
本文还有配套的精品资源,点击获取