CentOS 7.9 OpenSSH/OpenSSL升级加固实战:RPM打包与回滚方案 简介面向CentOS 7.9运维与安全人员的OpenSSH/SSL升级加固资源针对系统自带版本存在安全漏洞的场景提供脚本化一键升级方案。整体包含4个文件其中3个为openssh-server、openssh-clients等RPM安装包1个为自动化升级加固脚本压缩包大小20.42MB。RPM包对应OpenSSH 10.0p1版本并将SSL库更新至3.5.1脚本覆盖依赖检查、安装升级、配置优化等环节并支持常见安全项调整例如限制空密码登录、关闭root远程登录、设置空闲超时等。通过该资源用户可避免手动编译的繁琐流程快速完成SSH服务与SSL库的更新同时了解版本升级中的关键配置与排错思路为后续自主维护提供参考。目前已有138人学习下载适合需要快速收敛高危漏洞并加固SSH服务的中高级系统管理员。 临近下班漏扫平台又弹出一批告警盯着一台跑了好几年的CentOS 7.9OpenSSH 7.4p1用户名枚举、OpenSSL 1.0.2k的CVE-2016-2183整改时限三天。这台机器上跑着好几个不能停的应用重启窗口只能在凌晨领导只给一句话别搞挂了。这就是我写centos7.9-ssh10.0p1-ssl3.5.1-rpm-x86-64升级加固脚本这套东西的起因。标题看着像一串版本号拼起来的文件名实际上它要解决的是三件事把SSH从7.4p1升到10.0p1把OpenSSL从1.0.2k升到3.5.1再在升级过程中完成安全加固并且整个过程要可回滚、可复用、不搞挂存量业务。这篇文章把整个思路、打包方式、执行过程中最磨人的坑和最终落地配置都写出来适合准备做漏扫整改、等保加固或者想给一批CentOS 7.9机器批量升级SSH/SSL的运维朋友参考。在动笔前先说清楚一个定位问题这不是随手跑两条yum update就能完成的CentOS 7.9的软件源里根本没有OpenSSH 10.0p1和OpenSSL 3.5.1。要拿到这套版本组合必须自己用RPM方式构建然后写一个健壮的脚本去分布执行。接下来我按自己实际执行的顺序把过程拆开讲。1. 升级动因漏扫告警背后老的SSH/SSL已经撑不住合规要求1.1 旧版本暴露出来的问题到底有多实际网上到处能搜到CentOS 7.9默认自带的OpenSSH 7.4p1、OpenSSL 1.0.2k的漏洞清单真正把我逼到动手的是下面几类漏扫直接报出CVE-2016-2183也就是SSL/TLS协议信息泄露漏洞根源是OpenSSL里对3DES这类弱套件的支持。OpenSSL 1.0.2k默认还带着3DES相关密码套件所以只要端口上跑着依赖OpenSSL的服务扫描器基本一打一个准。OpenSSH 7.4p1存在用户枚举类问题扫描器能通过SSH登录流程的差异判断系统里是否存在某个用户名这对生产环境来说很危险。等保和护网期间的检查项里弱算法、旧协议版本都算硬指标。即便我在sshd_config里把弱算法全部禁掉只要sshd主程序版本还是7.4p1扫描器照样会报OpenSSH版本过低。所以结论很直接只在配置层打补丁没办法彻底过检必须升级版本。这是最根本的原因。1.2 版本跳跃式升级的取舍从7.4直接跳到10.0p1从1.0.2k跳到3.5.1中间隔了多个大版本。好处很明显新版本默认策略收紧了很多比如默认禁用了更多弱算法、默认对密钥交换算法做了裁剪升级完成后后续整改项会少很多。代价是兼容性风险集中爆发。最典型的就是OpenSSL从1.x跳到3.x之后动态库从libcrypto.so.1.0.0变成了libcrypto.so.3系统里大量旧程序如果还在引用老的so文件升级后直接启动失败。另一个是OpenSSH 10.0p1对算法协商更加严格老客户端、老运维工具、部分旧设备可能连不上。所以我在一开始就定了两个原则第一新版OpenSSL不能覆盖系统原有的/lib64/libcrypto.so.1.0.0等文件必须装到独立目录第二升级脚本必须具备回滚能力而且回滚路径要在升级前就验证过。这两条原则贯穿了后面所有设计。2. RPM打包与脚本组织为什么绕开make install选择rpmbuild2.1 直接编译安装的隐患很多教程会让你./configure make make install我在测试环境试过效果很差。主要问题是编译默认装到/usr/local系统里同时存在两套sshd、两套配置目录service命令启动的、ps看到的、ss -tlnp查到的可能不是同一个东西排查问题时精神分裂。无法用rpm -q、rpm -ql来管理文件清单等保检查时说不清楚系统里装了哪些关键软件。升级容易回滚很难。编译安装的版本卸载不干净残留文件有时候比不升级还麻烦。所以我选择了rpmbuild。用RPM包的好处是文件的安装路径、依赖关系、配置模板都写死在spec里安装时留下准确的rpm数据库记录回滚时rpm -e就能干净移除。对生产环境来说可审计性和可回滚性比跑通一次重要得多。2.2 spec文件里最关键的几个设置我构建OpenSSH 10.0p1的spec时核心关注点不在那些默认的configure参数而在安装路径和动态库依赖。经验证下面这套配置能减少后面90%的麻烦安装路径固定在/usr/local/openssh和/usr/local/openssl3不用系统默认的/usr目录。configure时给OpenSSH加--with-ssl-dir/usr/local/openssl3并配合LDFLAGS-Wl,-rpath,/usr/local/openssl3/lib让sshd运行时优先找到自己配套的libcrypto.so.3。BuildRequires至少包含gcc、make、pam-devel、zlib-devel、krb5-devel、libselinux-devel、openssl-devel、rpm-build缺一个都可能让configure阶段静默跳过某些特性。构建OpenSSL 3.5.1时也需要加shared否则不会生成libcrypto.so和libssl.so的动态库后面所有依赖它的程序都会挂在加载阶段。这里有个小经验构建机一定要用一台干净的CentOS 7.9并且不要在这台机器上先装各种乱七八糟的编译器版本。我之前在一台装过多个gcc版本的机器上build出来的二进制在标准机器上跑起来总有些奇怪行为花了一天排查最后换干净构建机重编一次通过。2.3 升级脚本的模块划分与幂等设计整套升级脚本我没有写成一个几百行的面条脚本而是按阶段拆成函数。大体结构如下#!/bin/bash # centos7.9-ssh10.0p1-ssl3.5.1-rpm-x86-64升级加固脚本主入口 set -e BASE_DIR$(cd $(dirname $0) pwd) PKG_DIR$BASE_DIR/packages BACKUP_DIR/data/backup/ssh_ssl_upgrade_$(date %Y%m%d%H%M%S) # 检查系统版本、架构 check_env() { [ -f /etc/redhat-release ] || { echo 仅支持RHEL系; exit 1; } uname -m | grep -q x86_64 || { echo 仅支持x86_64; exit 1; } echo 环境检查通过 } # 备份关键路径 backup_config() { mkdir -p $BACKUP_DIR cp -a /etc/ssh $BACKUP_DIR/ 2/dev/null || true cp -a /etc/pam.d/sshd $BACKUP_DIR/ 2/dev/null || true echo 配置备份完成: $BACKUP_DIR } # 安装OpenSSL 3.5.1 install_ssl() { local ssl_installed$(rpm -q openssl351 2/dev/null || echo not_installed) if [ $ssl_installed not_installed ]; then rpm -Uvh $PKG_DIR/openssl-3.5.1-1.x86_64.rpm fi ldconfig } # 安装OpenSSH 10.0p1 install_ssh() { local ssh_installed$(rpm -q openssh10 2/dev/null || echo not_installed) if [ $ssh_installed not_installed ]; then rpm -e --nodeps openssh-server openssh-clients openssh 2/dev/null || true rpm -Uvh $PKG_DIR/openssh-10.0p1-1.x86_64.rpm fi } # 写入加固配置 apply_hardening() { source $BASE_DIR/hardening_sshd_config.sh source $BASE_DIR/hardening_openssl_config.sh } # 验证 verify_upgrade() { /usr/local/openssh/sbin/sshd -V /usr/local/openssl3/bin/openssl version -a } # 回滚入口 rollback() { echo 执行回滚... }脚本里最值得注意的设计就是幂等。每次执行前先查目标RPM包是否已经安装装了就直接跳过安装阶段进入配置阶段。这样批量跑多台机器时中间某台失败了修复后重新执行脚本不会乱。还有一个细节脚本开头加了一个--dry-run参数只打印将要执行的命令不实际执行。我第一次在生产上跑之前就是靠dry-run模式把要执行的命令逐条过了一遍避免了低级错误。3. 升级实施中必踩的三类故障与完整排查链路这一章如果只说装包重启就成功了那对读者没有任何价值。我实际踩过的坑基本可以归纳为三类下面按故障现象、排查过程、根因、解决办法的顺序讲。3.1 sshd服务起不来提示找不到libcrypto.so.3现象rpm包装完后执行systemctl restart sshd返回失败systemctl status sshd看到进程退出了日志里没有明显的PAM报错看起来像是静默崩溃。排查链路systemctl status sshd journalctl -u sshd --no-pager | tail -100 /usr/local/openssh/sbin/sshd -t ldd /usr/local/openssh/sbin/sshd | grep not found我在测试时第一次就卡在这里。ldd结果出来发现libcrypto.so.3 not found根因是编译OpenSSH时只指定了--with-ssl-dir没有把运行时搜索路径打进去。程序知道去哪里找头文件但运行时不带RPATH就找不到新库。解决办法有两个推荐第一个重编包在OpenSSH的configure里加LDFLAGS-Wl,-rpath,/usr/local/openssl3/lib。临时方案export LD_LIBRARY_PATH/usr/local/openssl3/lib再启动sshd但这种方式对systemd管理不友好重置环境后失效只适合临时排错。这个坑给我最大的教训是构建阶段就必须把运行时的库搜索路径考虑进去而不是装完了再靠系统库路径找补。3.2 老客户端连不上报no matching key exchange method现象升级后本机用新版ssh客户端连接正常但Windows下的旧版Xshell、部分内网跳板机连接时报no matching key exchange method或者Unable to negotiate。排查链路在客户端执行ssh -vvv观察协商过程在服务端看/var/log/securetail -100 /var/log/secure | grep -i Unable to negotiate根因是OpenSSH 10.0p1的默认KexAlgorithms和Ciphers裁剪掉了老版本客户端支持的算法比如diffie-hellman-group14-sha1这类SHA-1体系算法两边都找不到共同项就断开。解决思路不是把服务端所有算法全部放开而是做一个折中对正常的办公网段和内网运维网段在sshd_config里单独加一个匹配块开放必要的旧算法对外网或核心生产网段仍然使用严格算法集。具体配置# /etc/ssh/sshd_config.d/compat.conf Match Address 10.0.0.0/8,172.16.0.0/12 KexAlgorithms diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1 HostKeyAlgorithms ssh-rsa PubkeyAcceptedKeyTypes ssh-rsa这种按源地址放行的做法既解决了老客户端兼容问题又不至于把攻击面暴露在整个网络上。3.3 卸载旧OpenSSH包时当前SSH连接直接断掉现象脚本在执行rpm -e对旧openssh-server卸载时当前正在跑安装命令的SSH会话瞬间断线安装过程中断机器处于sshd被卸载但新包还没装上的窗口期。这个坑很隐蔽尤其在批量远程执行脚本时最容易触发。因为rpm -e openssh-server会停掉sshd服务而我们的操作会话恰恰就是通过sshd进来的。排查链路一开始我被断线搞得有点慌连上带外管理口之后检查发现sshd服务确实没有了新包也没装上。根因很明确就是卸载顺序问题。解决方法是加一个死亡开关保护在卸载旧包之前先在crontab里写入一个定时任务每两分钟检测一次sshd是否存活如果检测不到就自动恢复旧版服务并停止继续安装。实际上更稳妥的做法是先用screen或nohup把安装脚本放进去再用at在5分钟后执行安装这样即使当前会话断了安装脚本还能继续跑完。我在脚本里加了这样一段# 在screen会话中后台执行升级避免卸载旧sshd导致当前连接断开 if [ -z $TMUX ] [ -z $STY ]; then yum install -y screen /dev/null 21 || true screen -dmS ssh_upgrade bash $0 --run exit 0 fi如果当前不在screen会话里脚本会自己弹出一个screen会话去执行原有的SSH连接断开也不影响升级流程。这个细节看起来小批量跑几十台机器时能救命的。4. 安全加固项的落地细节从sshd_config到OpenSSL策略版本升上去只是第一步漏扫里很多项还要靠配置加固来消除。下面把我在脚本里实际使用的加固要点列出来。4.1 sshd_config加固项逐条说明加固配置我基本都放在/etc/ssh/sshd_config.d/hardening.conf里主配置文件只保留Include和少量基础项这样便于以后回溯。配置项值说明PermitRootLoginno禁止root直接SSH登录必须用普通用户sudoPasswordAuthenticationno关闭密码登录只允许密钥登录PubkeyAuthenticationyes开启密钥认证PermitEmptyPasswordsno禁止空密码账号登录MaxAuthTries3单连接最大认证尝试次数LoginGraceTime30登录超时时间单位秒ClientAliveInterval300服务端每300秒向客户端发送心跳ClientAliveCountMax2连续2次心跳无回复则断开X11Forwardingno关闭X11转发GSSAPIAuthenticationno关闭GSSAPI认证减少延迟和枚举风险UseDNSno不进行DNS反向解析提升连接速度AllowGroupswheel ops只允许wheel和ops组的用户登录这里要特别提一句执行PasswordAuthentication no之前必须确认已经有一个可用用户的公钥写进了authorized_keys并且用密钥方式测试过一次登录。如果直接一改就重启而公钥没放对轻则锁在门外重则只能带外管理口去救。我见过不止一次同事在电池阀值上翻车。4.2 OpenSSL 3.5.1的弱算法策略修复CVE-2016-2183OpenSSL独立的安装路径下有一个openssl.cnf我在里面显式指定了密码套件策略目标是彻底关闭3DES类弱套件# /usr/local/openssl3/ssl/openssl.cnf 中相关配置 openssl_conf openssl_init [openssl_init] ssl_conf ssl_sect [ssl_sect] system_default system_default_sect [system_default_sect] CipherString DEFAULT:SECLEVEL2:!3DES CipherSuites TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256加上!3DES之后依赖OpenSSL的应用就不会再协商出3DES套件CVE-2016-2183在扫描结果里就能消除。注意这里说的不只是SSH服务因为SSH本身不使用OpenSSL的密码套件真正受影响的往往是同一台机器上的Nginx、MySQL、自研Java服务等。所以升级完OpenSSL后这些业务进程要逐个重启并回归。4.3 升级后常见业务场景的影响这台机器上还跑着Nginx用的阿里云SSL证书。升级OpenSSL到3.5.1后Nginx加载证书时有时会报no required ssl certificate was sent或者证书链校验失败多数原因不是证书真的有问题而是OpenSSL 3.x默认安全级别提高后对证书链中某些中间证书的算法强度判定更严格。处理思路是先用openssl s_client检查证书链是否完整再确认证书本身是RSA 2048以上、SHA-256签名中间证书有没有缺失。如果证书链不完整把中间证书补进fullchain.pem重新加载即可。另外如果你在群晖上配置了SSH密钥平时用VSCode远程连接服务器或者在GitLab CI里通过SSH拉代码升级后突然报KEX错误或Invalid SSL certificate大概率也是算法协商问题。优先排查客户端和服务端之间的KexAlgorithms、Ciphers、MACs交集而不是先怀疑密码输错了。4.4 密钥登录的权限和SELinux上下文既然关闭了密码登录authorized_keys的权限和上下文就必须正确否则哪怕文件内容是对的SSH也会拒绝读取。三件套检查chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys restorecon -Rv ~/.sshSELinux上下文这条特别容易漏。CentOS 7.9默认SELinux是Enforcing如果把/home目录从别的地方挂载进来或者用rsync从其他机器同步过.ssh目录SSH进程可能因为上下文策略问题读不到公钥。遇到密钥登录失败、但日志里没有明显提示时跑一下ausearch -m avc -ts recent看有没有SELinux拒绝记录能少走很多弯路。5. 升级后的验证清单与回滚预案5.1 验证命令与预期结果升级加固完成后我建议按以下顺序验证顺序不能乱每一层都过了再进下一层# 1. 版本是否正确 /usr/local/openssh/sbin/sshd -V /usr/local/openssl3/bin/openssl version -a # 2. 动态库依赖是否正常 ldd /usr/local/openssh/sbin/sshd | grep -E ssl|crypto # 3. 服务状态 systemctl status sshd ss -tlnp | grep :22 # 4. 新开一个SSH会话实测登录确认密钥登录正常 ssh -o PreferredAuthenticationspublickey -i ~/.ssh/xxx_key ops_audit目标IP # 5. 模拟漏扫检查弱套件是否已经关闭 nmap --script ssl-enum-ciphers -p 22 目标IP除了端口验证还要验证系统自身的yum功能没坏。因为OpenSSL升级如果污染了系统库路径yum、curl、wget这些依赖底层库的工具全都会报错。我在测试机上升级后第一时间就执行yum clean all yum makecache确认没报libcrypto相关错误才敢继续。5.2 回滚细节与备份策略回滚预案要写在脚本里并且要提前验证一次而不是出了问题再临时想。我的备份策略是升级前把以下路径完整打包备份/etc/ssh/etc/pam.d/sshd/etc/ld.so.conf.d/usr/lib/systemd/system/sshd.service如果有覆盖旧版RPM包文件本身保存到单独目录禁止升级后随手删除回滚命令大致如下# 停止新版服务 systemctl stop sshd # 卸载新版包 rpm -e openssh10 openssl351 # 安装回旧版包 rpm -ivh openssh-7.4p1-*.x86_64.rpm openssl-1.0.2k-*.x86_64.rpm # 恢复配置 cp -a $BACKUP_DIR/ssh /etc/ # 启动服务 systemctl start sshd systemctl status sshd整个回滚过程里最核心的一点是rpm包要留着。很多人升级完觉得旧包没用了删掉之后一旦新版出了问题只能重新上网找匹配的依赖包时间和风险都不可控。最后再说一条我自己的习惯这套升级脚本在正式跑生产之前我会先在一台仿真机或者同配置测试机上连续跑两遍第一遍观察输出第二遍验证幂等性。确认第二次执行不会重复装包、不会重复覆盖配置之后才批量上生产。能做到这一点的脚本才配叫升级加固脚本而不是一次性手工操作记录。本文还有配套的精品资源点击获取