简介:面向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.9:OpenSSH 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,选择rpmbuild
2.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/secure:
tail -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 2>&1 || 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和少量基础项,这样便于以后回溯。
| 配置项 | 值 | 说明 |
|---|---|---|
| PermitRootLogin | no | 禁止root直接SSH登录,必须用普通用户+sudo |
| PasswordAuthentication | no | 关闭密码登录,只允许密钥登录 |
| PubkeyAuthentication | yes | 开启密钥认证 |
| PermitEmptyPasswords | no | 禁止空密码账号登录 |
| MaxAuthTries | 3 | 单连接最大认证尝试次数 |
| LoginGraceTime | 30 | 登录超时时间,单位秒 |
| ClientAliveInterval | 300 | 服务端每300秒向客户端发送心跳 |
| ClientAliveCountMax | 2 | 连续2次心跳无回复则断开 |
| X11Forwarding | no | 关闭X11转发 |
| GSSAPIAuthentication | no | 关闭GSSAPI认证,减少延迟和枚举风险 |
| UseDNS | no | 不进行DNS反向解析,提升连接速度 |
| AllowGroups | wheel ops | 只允许wheel和ops组的用户登录 |
这里要特别提一句,执行PasswordAuthentication no之前,必须确认已经有一个可用用户的公钥写进了authorized_keys,并且用密钥方式测试过一次登录。如果直接一改就重启,而公钥没放对,轻则锁在门外,重则只能带外管理口去救。我见过不止一次同事在电池阀值上翻车。
4.2 OpenSSL 3.5.1的弱算法策略修复CVE-2016-2183
OpenSSL独立的安装路径下有一个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:@SECLEVEL=2:!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 PreferredAuthentications=publickey -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包要留着。很多人升级完觉得旧包没用了,删掉之后一旦新版出了问题,只能重新上网找匹配的依赖包,时间和风险都不可控。
最后再说一条我自己的习惯:这套升级脚本在正式跑生产之前,我会先在一台仿真机或者同配置测试机上连续跑两遍,第一遍观察输出,第二遍验证幂等性。确认第二次执行不会重复装包、不会重复覆盖配置之后,才批量上生产。能做到这一点的脚本,才配叫"升级加固脚本",而不是一次性手工操作记录。
本文还有配套的精品资源,点击获取