
最近在做一批 CentOS 7.9 服务器的安全整改等保漏洞扫描报告里OpenSSH 相关的 CVE 占了很大篇幅。系统自带的 OpenSSH 7.4p1 服役多年面对 OpenSSH 10.0p1 这一版本号的整改目标最稳妥的交付方式就是打成 RPM 安装包然后批量分发。这篇文章是我这次 centos7.9 openssh10.0p1-rpm 适配过程的全记录从为什么选 RPM 方案、怎么改 spec 文件到安装替换和回滚兜底按实际操作顺序过一遍。你要是也在做着同样的事可以直接拿这些步骤做参考。CentOS 7.9 的官方生命周期已经结束但生产环境里它还在大量运行短时间内不可能全部迁移。对运维来说能用 RPM 管理起来的东西才是可控的包可以查询、可以回滚、可以用 yum 仓库分发。OpenSSH 升级这种事最怕的就是一时手快在一台服务器上直接 make install等出了问题连“当初装了什么”都说不清楚。下面内容完全围绕这个场景展开读者对象是负责系统安全、业务服务器维护的运维工程师也包括准备做自研 RPM 包的同学。1. 决定打 RPM 而不是二进制编译的几个现实原因1.1 编译安装的包管理盲区大部分人拿到 OpenSSH 新版源码第一反应就是解压然后三条命令./configure、make、make install。单台机器临时用没问题但放到几十台甚至上百台的服务器里痛点会立刻暴露rpm -qa 查不到你的 OpenSSH 版本yum update 可能直接覆盖掉你手工装的文件卸载的时候只能自己手动清理根本不知道哪些文件是编译产生的。更麻烦的是OpenSSH 这类系统级服务还牵扯到 PAM 模块、SELinux 安全上下文、systemd 服务文件手工编译很容易把 /usr/libexec/openssh/sftp-server 这类辅助二进制放错位置导致升级后 SFTP 服务时好时坏。还有一个隐蔽问题很多 CentOS 7.9 服务器为了过等保基线检查会跑 rpm -qa 核验软件版本和补丁状态。如果 OpenSSH 是通过编译安装的检查脚本可能直接判定为“不符合”因为你根本没进入 RPM 体系。为了安全和合规之间的对齐把 OpenSSH 10.0p1 做成 RPM 安装包不是可选项而是必须项。1.2 “el7 通用 RPM”最好别乱下有些人喜欢去网上找现成的 openssh-10.0p1-xxx.el7.x86_64.rpm 直接安装这个做法我其实是反对的。首先别人打的包你并不知道他的编译参数是什么比如没有启用 PAM 支持那么 sshd 在登录验证时就会绕过系统 PAM 配置轻则密码登录异常重则影响账号锁定的安全策略。其次很多第三方仓库编译环境并不是干净的 CentOS 7.9他可能用的是高版本 glibc 或 OpenSSL 动态库放到你的机器上启动 sshd 时会直接报 missing symbol 或 version not found。拿这个场景来说网上下载的 RPM 包如果依赖了当前系统没有的 libcrypto.so.1.1你在 CentOS 7.9 上就会看到 sshd 起不来的现象。与其去猜别人怎么编译的不如自己从头构建一个能追溯、能复现的 RPM 包。自己打出来的包放进内网 yum 仓库后面所有机器都是同一条链路出现问题时能快速定位是编译选项问题还是系统环境问题。1.3 RPM 打包其实没有想象中复杂很多运维对 rpmbuild 有畏惧感总觉得写 spec 文件是发行版工程师才能干的事。实际用下来OpenSSH 这个项目在源码包里带了完整的 spec 模板路径CentOS 7 自身也有开源的 openssh.spec 可以参考。我们要做的不是从零写一份规范而是在已有规范基础上做版本升级和依赖适配核心工作主要集中在版本号、Source 地址、编译选项、文件清单这几块。另外打成 RPM 之后还有一个优势可以用 rpmbuild 的构建结果直接做验证。rpm -qpl 可以列出包内所有文件路径rpm -qpR 可以查看运行时依赖rpm -qpc 能确认哪些是配置文件。这些信息在出现故障时都是排查的抓手。相比编译安装之后只能靠 find 命令猜测文件位置RPM 体系的可观察性强太多了。2. 准备构建环境镜像源、依赖与源码校验2.1 先确认系统现状构建之前先把系统底数摸清楚避免打到一半发现环境太干净或者版本不对。我在构建机上执行的第一组命令是cat /etc/redhat-release uname -r rpm -qa | grep openssh ssh -V 21 sshd -V 21这里要注意ssh -V和sshd -V默认输出到 stderr所以用21重定向一下。正常情况下 CentOS 7.9 会返回 OpenSSH_7.4p1 这样的版本信息。另外确认一下架构我这批服务器全是 x86_64所以后续包名都按 x86_64 处理。如果系统特别精简连 rpm 命令都提示 not found那就要先用 yum 安装 rpm 基础包。正常情况下 CentOS 7.9 不会缺 rpm但某些容器镜像或裁剪系统确实会有这种极端情况提前确认一下不浪费时间。2.2 配置可用的 yum 源因为 CentOS 7 已经结束维护默认的 mirrors.centos.org 路径可能已经失效或不稳定建议把 yum 源切到阿里云的开源镜像站。这里有个细节CentOS 7 停更后源路径从/centos/变成了/centos-vault/一定要用 vault 路径否则 yum 会报 404。简单配置一个 base 源cat /etc/yum.repos.d/CentOS-Base.repo EOF [base] nameCentOS-7.9-vault baseurlhttps://mirrors.aliyun.com/centos-vault/7.9.2009/os/x86_64/ gpgcheck0 enabled1 EOF yum clean all yum makecache fast如果还要装 EPEL 包可以再把 epel-release 装一下。不过 OpenSSH 构建所需的依赖基本都在 base 源里EPEL 不是必须的。这一步做好之后后面 yum 安装工具链才能顺畅。2.3 安装 rpmbuild 工具链与关键依赖构建 RPM 需要一套完整的工具链直接一条 yum 命令全部装齐yum install -y rpm-build rpmdevtools gcc make autoconf automake libtool yum install -y zlib-devel openssl-devel pam-devel krb5-devel audit-libs-devel这里重点讲一下为什么需要这些依赖rpm-build 提供 rpmbuild 命令本体rpmdevtools 提供 rpmdev-setuptree 之类的辅助脚本gcc、make、autoconf、automake、libtool 是编译源码的基本工具链zlib-devel 提供压缩库头文件OpenSSH 传输层依赖 zlibopenssl-devel 提供加密库头文件新版 OpenSSH 虽然在某些版本后对 OpenSSL 的依赖有所减弱但 configure 检测时最好还是要保证头文件存在pam-devel 是 PAM 认证支持的关键依赖如果缺少configure 会直接提示找不到 pam 相关头文件krb5-devel 和 audit-libs-devel 分别对应 GSSAPI 认证和 Linux 审计模块等保环境下这两项一般都要开启。很多人只装了 gcc 就开始 ./configure结果卡在 PAM headers not found非常浪费时间。在 CentOS 7 上依赖最好一次装全后续 rpmbuild 才能一气呵成。2.4 下载源码并校验完整性OpenSSH 官方源码发布在 OpenBSD 的可移植版发布目录下。我用 wget 下载 openssh-10.0p1.tar.gz然后立刻做 sha256 校验wget https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-10.0p1.tar.gz sha256sum openssh-10.0p1.tar.gz校验值建议和官网上发布的校验信息核对这一步不能省。源码包被篡改这种事虽然概率低但 OpenSSH 是直接暴露在公网的服务任何供应链风险都可能导致严重后果。下载完源码后把它放到 rpmbuild 的 SOURCES 目录这个目录在后续的 spec 构建中会被自动引用。如果你所在环境的服务器出网受限也可以找一台能出网的机器先下载好再通过内网传输到构建机。只要源码包完整性和路径没问题构建结果是一致的。3. 改造 openssh.spec适配老系统是核心工作量3.1 从旧 SRPM 提炼 spec而不是从零写OpenSSH 这种复杂组件的 spec 文件涉及太多细节从零写会踩很多坑。我的做法是先把 CentOS 7.9 自带的 OpenSSH SRPM 拉下来从里面提取旧 spec 作为基础然后在此基础上修改。获取旧 SRPM 的命令yum install -y yum-utils yumdownloader --source openssh rpm -ivh openssh-7.4p1-*.el7.src.rpm执行完 rpm -ivh 之后源码包会解压到~/rpmbuild/SOURCES和~/rpmbuild/SPECS其中~/rpmbuild/SPECS/openssh.spec就是我们要改的起点。这里要注意CentOS 7 的 spec 里带了一堆针对旧版本的补丁比如 CVE 修复补丁、GSSAPI 兼容补丁等。到了 OpenSSH 10.0p1很多修复已经合入上游源码这些旧补丁要么直接删除要么会打不上需要逐个确认。3.2 版本号、Source0 与构建依赖的调整spec 文件里最核心的几处修改必须准确。先看版本相关字段Version: 10.0p1 Release: 1.el7 Source0: https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-%{version}.tar.gz我保留了%{version}变量这样以后升级到 10.1 之类的新版时只需要改 Version 字段Source0 的 URL 会自动变化省得手动改一串地址。BuildRequires 需要根据实际情况调整。CentOS 7 默认的 spec 里已经写了 openssl-devel、zlib-devel、pam-devel 等基本不用大动。但要注意如果新版本源码的 configure 增加了新的检测项可能还需要补充对应开发包。我这次构建时没有新增依赖所以这部分改动最少。3.3 configure 参数里藏着 SELinux / PAM 兼容性spec 中%configure或显式 configure 参数是决定 RPM 行为的关键。我这次比较关注的是下面这几个参数--sysconfdir/etc/ssh --with-pam --with-selinux --with-kerberos5 --with-auditlinux --with-privsep-path/var/empty/sshd --with-md5-passwords--sysconfdir/etc/ssh保证配置文件仍然放在老位置--with-pam必须开否则 CentOS 的密码认证、账号锁定策略全部失效--with-selinux开启后 sshd 会主动设置正确的文件安全上下文避免 SSH 私钥目录被 SELinux 拦截--with-kerberos5是给 GSSAPI 认证留的口子企业环境里跳板机认证经常要用。--with-md5-passwords这个选项在 OpenSSH 5.4 以后的版本中默认就不太推荐了但 CentOS 7 的用户数据库里如果有老哈希格式的密码开这个选项能减少升级后的认证兼容性问题。按实际安全策略决定如果你们已经强制 sha256 以上哈希可以不开。构建服务器默认不会自动生成这些 configure 参数我建议直接改动 spec 中的%configure段把上面的参数按需写入这样构建出的 RPM 才真正适合生产环境。3.4 补丁维护与配置文件的 %config 声明旧 spec 的 Patch 列表是这次适配最容易翻车的地方。CentOS 7 的 openssh.spec 中有一长串Patch0:、Patch1:等定义并在%prep阶段用%patch应用。这些补丁是对应 7.4p1 源码的换到 10.0p1 之后很多 hunk 根本找不到上下文。我在处理时把所有不适用于新版本的补丁全部注释掉只保留了针对 SELinux 策略和 SSH 配置路径类的必要补丁。如果上游 10.0p1 已经合入了相同内容那这些补丁就是多余的留着反而会让%prep阶段直接构建失败。配置文件的%config声明也不要乱动。OpenSSH 的 RPM 里通常把/etc/ssh/ssh_config和/etc/ssh/sshd_config标记为%config(noreplace)这意味着升级安装时如果配置文件已经被本地修改过RPM 不会直接覆盖而是生成.rpmnew后缀文件。这个机制在生产环境中非常重要否则升级一次 SSH你手写的 AllowUsers 配置就被冲没了。3.5 执行 rpmbuild 并检查产物spec 改好后进入真正的构建环节cd ~/rpmbuild/SPECS rpmbuild -ba openssh.spec-ba表示同时构建二进制包和源码包。如果第一次构建报错不要慌大概率是缺依赖或者补丁没删干净。补充依赖后重新执行即可。构建成功后进入~/rpmbuild/RPMS/x86_64/目录产物应该是这几个包openssh-10.0p1-1.el7.x86_64.rpm openssh-clients-10.0p1-1.el7.x86_64.rpm openssh-server-10.0p1-1.el7.x86_64.rpm先用 rpm 查询命令验证一下包内容cd ~/rpmbuild/RPMS/x86_64/ rpm -qpl openssh-10.0p1-1.el7.x86_64.rpm rpm -qpl openssh-server-10.0p1-1.el7.x86_64.rpm rpm -qpR openssh-server-10.0p1-1.el7.x86_64.rpm重点看两个地方一是/usr/sbin/sshd、/usr/libexec/openssh/sftp-server这些关键二进制是否落在预期路径二是运行时依赖是否出现了当前系统无法满足的高版本 glibc 或 OpenSSL 动态库。如果rpm -qpR输出的依赖都很正常这个包才允许进入安装环节。4. 安装替换Uvh 背后的那些细节问题4.1 安装前必须完成的备份动作不管 RPM 包构建得多完美替换系统 SSH 服务时都必须先做备份。生产事故往往不是新版本不能用而是新旧交替时配置文件的迁移出了问题。安装前我习惯做这样几件事cp -a /etc/ssh /etc/ssh.bak.$(date %Y%m%d) cp /usr/sbin/sshd /usr/sbin/sshd.bak cp /etc/pam.d/sshd /etc/pam.d/sshd.bak这里最重要的备份是/etc/ssh目录。机器上现有的 SSH host key 如果丢了所有已知主机的指纹缓存都会失效影响大量客户端连接。RPM 安装包内的%post脚本如果没有检测到现有 host key可能会重新生成虽然技术上不影响功能但会导致很多客户机报警 host key 变更。还要确认一下/etc/pam.d/sshd的备份新版 OpenSSH 如果带了 PAM 配置有时会覆盖或新增配置文件一旦认证链路被破坏远程登录直接断掉。4.2 使用 rpm -Uvh 而不是 rpm -e新包下载好之后很多人会因为担心旧版本冲突先 rpm -e 卸载旧包再安装新包。在 OpenSSH 升级场景中这是非常危险的操作。因为 rpm -e openssh-server 时会同时删掉 sshd 服务、依赖和 systemd 配置如果在远程操作极大概率把自己锁在机器外面。正确的替换方式是用 rpm -Uvh 做原位升级rpm -Uvh ~/rpmbuild/RPMS/x86_64/openssh-10.0p1-1.el7.x86_64.rpm \ ~/rpmbuild/RPMS/x86_64/openssh-server-10.0p1-1.el7.x86_64.rpm \ ~/rpmbuild/RPMS/x86_64/openssh-clients-10.0p1-1.el7.x86_64.rpmUvh 会自动处理旧包卸载和新包安装的顺序并且会执行新包 spec 中的%post脚本。如果构建时报错提示某个文件已经被旧包占用不要用 rpm -e 硬删检查一下是否同时装了 openssh-server 和 openssh-server-sysvinit 之类的叠叠包一起升级掉就行。安装完成后先不要急着断开当前 SSH 会话。执行ssh -V确认客户端版本再执行sshd -V确认服务端版本有一个不对都说明包没装对。4.3 启动失败时的救援排查链路如果在另一台测试机上做了升级但 sshd 启动失败不要慌按固定链路排查。首先测试配置语法/usr/sbin/sshd -t -f /etc/ssh/sshd_config这个命令会直接输出配置文件的语法错误。常见的错误类型是老配置文件中使用了新版已经不支持的指令比如Protocol 2,1或者废弃的Ciphers选项。用编辑器把对应行处理掉或者干脆把/etc/ssh/sshd_config先改名让系统使用默认配置启动再逐步 diff 差异。如果语法测试通过但服务仍然起不来查看系统日志tail -100 /var/log/messages tail -100 /var/log/secure日志里经常出现的关键词是Bad owner or permissions这时检查/var/empty/sshd目录权限。这个目录被 sshd 用作权限分离的 chroot 环境必须归 root 所有且不建议开放写权限。很多管理员在排查时把它改成 777反而加剧了问题正确做法是chown root:root /var/empty/sshd chmod 0755 /var/empty/sshd另外别忘了 SELinux 上下文。CentOS 7 开启了 SELinux 后新装的二进制文件上下文可能不是 sshd 标准上下文导致 sshd 无法读取密钥或监听端口。执行一次恢复操作restorecon -Rv /etc/ssh /usr/sbin/sshd /usr/sbin/sftp-server /usr/libexec/openssh如果在测试机上确认了一切正常再回到生产机器上执行同样的升级流程。远程操作时一定要先开一个备用会话或者确保有带外管理通道否则 sshd 重启失败后你可能只能去物理机房。4.4 验证新版本和密钥管理替换完成后除了 version 验证还要确认 host key 没有被覆盖。如果你的备份里有/etc/ssh/ssh_host_rsa_key升级后应该还在我一般对比一下文件指纹ssh-keygen -lf /etc/ssh/ssh_host_rsa_key如果指纹和升级前一致说明密钥没有被重新生成客户端不会报 host key changed。如果发现密钥确实变了日志里会有 SSH 服务自己重新生成的记录这通常不是大问题但需要尽快通知相关维护人员更新 known_hosts。服务状态验证方面执行systemctl status sshd -l看到 active (running) 后再从另一台机器上做一次完整的 SSH 登录、scp 传输、sftp 登录测试。这三件事都通过安装替换才算真正成功。5. 批量分发把 RPM 做成内网 yum 仓库5.1 仓库搭建与客户端配置单台构建完成后接下来的问题是怎么把包干净地分发到几十台机器上。我用的是内网 yum 仓库方案。构建机上创建一个目录把 RPM 文件统一放进去然后使用 createrepo 生成仓库元数据yum install -y createrepo mkdir -p /data/repo/openssh-10.0p1 cp ~/rpmbuild/RPMS/x86_64/openssh*.rpm /data/repo/openssh-10.0p1/ createrepo /data/repo/openssh-10.0p1/客户端机器上新建一个 repo 文件cat /etc/yum.repos.d/openssh-local.repo EOF [openssh-local] nameOpenSSH 10.0p1 Local Repo baseurlhttp://build-server-ip/data/repo/openssh-10.0p1 gpgcheck0 enabled1 EOF这里我把 gpgcheck 暂时设置为 0因为是内网小道包是自己构建的安全性可控。如果公司有 RPM 签名体系建议给包打上 GPG 签名后再发布客户端 gpgcheck1避免包在传输链路中被篡改。后面如果要做规范化可以用rpmsign对包签名。5.2 灰度策略与脚本自保批量升级 SSH 这种高危操作绝对不能一下子全量执行。即使 RPM 包在测试机上验证过每台服务器的配置和运行状态也都不一样。我这次先选了 3 台非核心、非关键链路机器做灰度。灰度机上安装时最好专门写一个带自保机制的脚本。所谓自保是指脚本在升级完成并重启 sshd 后不要立即退出而是等待一段时间并检测 sshd 是否稳定运行。简单实现如下systemctl restart sshd sleep 20 if systemctl is-active sshd | grep -q active; then echo sshd upgrade ok on $(hostname) else echo sshd failed on $(hostname), please use console fi听起来简单但非常实用。没有这一步脚本跑完就断开 SSH等你想再登录时发现服务已经挂了只能用远程管理卡或者人工进机房。灰度 3 台全部正常后再扩大到 10 台、最后全量。5.3 回滚方案的两种兜底路径批量升级之前一定要把回滚路径想清楚。我的建议是把升级前的 RPM 包版本号记下来并在内网仓库中保留旧版本包# 从 yum 缓存或安装介质中找到旧版本 rpm rpm -Uvh --oldpackage openssh-7.4p1-22.el7_9.x86_64.rpm \ openssh-server-7.4p1-22.el7_9.x86_64.rpm \ openssh-clients-7.4p1-22.el7_9.x86_64.rpm--oldpackage参数允许高版本包被低版本包降级替换这是 RPM 体系里特有的回滚能力。如果机器已经接入了上面的 yum 仓库更省事的方式是用yum downgradeyum downgrade openssh openssh-server openssh-clients只要 yum 仓库里还留着旧版本它就会自动完成降级。但注意yum downgrade 时旧版本包的 spec 脚本也会执行因此在回滚后同样要用sshd -t检查配置并用新会话验证登录。6. 复盘这次适配过程中踩过的坑6.1 pam-devel 缺失引发的编译中断第一次构建时我在一台最小化安装的机器上执行 rpmbuild结果 configure 阶段直接报configure: error: *** Cannot find PAM headers.。原因很简单没装 pam-devel。这个错误很典型很多人以为只要系统有 PAM 运行时库就可以编译但编译需要的是开发头文件即 /usr/include/security/pam_appl.h 这一层的文件。后来我把 pam-devel 装上重新构建一路顺畅。这种事在 spec 里其实已经写了 BuildRequires: pam-devel但如果你用了系统的 gcc 从源码直接 configure就很容易漏掉。提醒一下别跳步依赖包装齐再动手。6.2 /var/empty/sshd 权限被误改测试过程中遇到过一回 sshd 启动报Missing privilege separation directory: /var/empty/sshd当时检查了一圈发现 /var/empty/sshd 目录被之前的加固脚本改成了 0700属主也不是 root。OpenSSH 的 privilege separation 机制要求这个目录归 root 所有且不能有 group / other 的写权限但改成只有 root 可进入有时候反而让 sshd 自己都进不去。用chown root:root /var/empty/sshd和chmod 0755恢复之后服务立刻正常。这个问题不大但很隐蔽特别是有些安全扫描工具会建议把系统目录权限收紧误伤到 /var/empty 就会牵连 sshd。6.3 老配置文件中的废弃参数升级后第一次启动测试时我遇到过sshd: error: sshd_config line 12: Deprecated option RSAAuthentication这类提示。CentOS 7.9 的老配置里有很多为老版本 OpenSSH 准备的参数例如RSAAuthentication yes、Protocol 2,1在新的 OpenSSH 10.0p1 中已经被彻底移除或不再需要。处理方法只有一种把废弃参数注释掉或直接删除。不要指望新版本兼容所有旧参数越大的跨版本升级越要做配置清理。稳妥做法是让新包先以默认配置启动然后逐项把必要的配置AllowUsers、PermitRootLogin 等添加回来每加一项sshd -t验证一次。6.4 gpgcheck 拦住了批量安装批量分发阶段还有一个有意思的问题。客户端 yum repo 配置好后执行yum install openssh时报Public key for openssh-10.0p1-1.el7.x86_64.rpm is not installed。这是因为我在构建机上没有给 RPM 做 GPG 签名但有些客户端 repo 配置文件里保留了 gpgcheck1 的默认值。既然是内网仓库我直接把本地 repo 写成 gpgcheck0一次性绕开。如果公司安全规范要求签名就得额外引入 rpmsign 流程把私钥放到构建机上给每个 RPM 签名并在客户端注入公钥。这个倒不算坑只是流程规范问题提前统一就不会在分发时卡住。回到这次适配本身我认为最核心的收获是在 CentOS 7.9 这种老系统上引入新版本 OpenSSHRPM 化不是可有可无的加分项而是控制风险的基础。把 spec 文件、依赖、编译参数和回滚流程固定下来以后后续版本升级就只是重复操作不会再像第一次这样需要反复试错。如果你现在也在做类似的事建议先把 spec 文件版本管理起来至少存到 git 里以后每次升级都能留痕。最后想提醒一句CentOS 7 终究是有服务生命周期的RPM 包解决的是存量机器上的问题系统整体迁移的计划还是趁早排上日程比较好。