最近在做一批 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 2>&1 sshd -V 2>&1这里要注意ssh -V和sshd -V默认输出到 stderr,所以用2>&1重定向一下。正常情况下 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] name=CentOS-7.9-vault baseurl=https://mirrors.aliyun.com/centos-vault/7.9.2009/os/x86_64/ gpgcheck=0 enabled=1 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 传输层依赖 zlib;
- openssl-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-audit=linux --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] name=OpenSSH 10.0p1 Local Repo baseurl=http://build-server-ip/data/repo/openssh-10.0p1 gpgcheck=0 enabled=1 EOF这里我把 gpgcheck 暂时设置为 0,因为是内网小道,包是自己构建的,安全性可控。如果公司有 RPM 签名体系,建议给包打上 GPG 签名后再发布,客户端 gpgcheck=1,避免包在传输链路中被篡改。后面如果要做规范化,可以用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 downgrade:
yum 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 配置文件里保留了 gpgcheck=1 的默认值。
既然是内网仓库,我直接把本地 repo 写成 gpgcheck=0,一次性绕开。如果公司安全规范要求签名,就得额外引入 rpmsign 流程,把私钥放到构建机上给每个 RPM 签名,并在客户端注入公钥。这个倒不算坑,只是流程规范问题,提前统一就不会在分发时卡住。
回到这次适配本身,我认为最核心的收获是:在 CentOS 7.9 这种老系统上引入新版本 OpenSSH,RPM 化不是可有可无的加分项,而是控制风险的基础。把 spec 文件、依赖、编译参数和回滚流程固定下来以后,后续版本升级就只是重复操作,不会再像第一次这样需要反复试错。如果你现在也在做类似的事,建议先把 spec 文件版本管理起来,至少存到 git 里,以后每次升级都能留痕。最后想提醒一句,CentOS 7 终究是有服务生命周期的,RPM 包解决的是存量机器上的问题,系统整体迁移的计划还是趁早排上日程比较好。