1. 为什么非要把OpenSSH源码手动打成RPM包
先说一个我最近做的事:把OpenSSH 8.6p1的源码,在aarch64架构、4.19内核的机器上手动编译,最后打包成一个可安装、可回滚的RPM。整个过程踩了不少坑,尤其是spec文件里的细节,稍微写错一点,rpmbuild就直接报错,或者生成出来的包装上去sshd都起不来。这篇就把完整流程和关键教训整理出来,给打算自己维护OpenSSH包的同学一个参考。
1.1 直接make install的问题
有些人可能会问:既然源码包都拿到了,为什么不直接./configure && make && make install,非要绕一圈去打RPM包?
如果你只在本地一两台机器上用,那确实可以直接编译安装。但一旦涉及批量部署、版本统一、安全基线扫描,直接make install就是给自己埋雷。举个实际场景:某内网环境里有几十台aarch64服务器,安全扫描报告说当前OpenSSH版本存在多处已知漏洞,整改要求是统一升级到8.6p1以上。如果一台台手搓编译,第一是耗时,第二是每台机器的编译环境未必一样,编出来的二进制行为可能有细微差异,第三是卸载极其痛苦——make install把文件散落在各个目录,完全没有反安装机制。
还有一个更隐蔽的问题:Linux系统的RPM数据库里记录的OpenSSH还是旧版本。很多安全扫描工具是直接读RPM数据库的,你一厢情愿地把/usr/bin/ssh换成了新版本,数据库里还是老版本,扫描结果照样报漏洞。只有把RPM包做好,用rpm -Uvh安装,数据库和实际文件才能保持一致。
1.2 用RPM方式能解决什么
RPM打包带来的收益是实打实的:
- 可追溯:每个RPM包都有版本、Release号、构建时间,出了问题能快速确认到底装的是哪一次构建的产物。
- 可回滚:升级之前留一个旧版本RPM,出问题直接rpm -Uvh --oldpackage降回去,或者用yum/dnf自带的回滚机制。
- 批量分发:生成一个RPM文件,拷到其他相同架构、相似系统的机器上直接装,不依赖目标机有编译工具链。
- 依赖控制:可以把openssl、pam、zlib等依赖关系写进spec,包管理器自动检查,避免装完缺库。
这些优点在开发环境、测试环境、生产环境分层管理的场景下尤其明显。我个人一直认为,只要是跑Linux服务器的地方,自定义编译软件就应该走“打成包”的路线,哪怕内部用,也要让安装、升级、卸载成为一个标准行为,而不是把服务器当成个人开发机随便make install。
1.3 aarch64 + 4.19内核这个组合的特殊性
标题里特别标注了aarch64和4.19内核,说明这不是普通的x86_64环境。先说aarch64,OpenSSH本身是高度可移植的C项目,编译层面基本不需要针对ARM做什么额外适配,configure脚本会自动识别架构。但要注意一点:构建机的架构必须和目标机一致,也就是原生rpmbuild,而不是在x86_64上交叉编译。OpenSSH里有大量运行时探测和编译期宏判断,交叉编译很容易在ssh-keygen生成密钥、sshd权限测试这些环节出幺蛾子。
4.19内核是老牌LTS内核,本身没什么问题,但它与新版OpenSSH的兼容性值得留意。比如OpenSSH 8.6已经支持在系统调用层面使用某些新特性,这些特性在4.19内核上可能不存在,configure脚本会通过弱符号或运行时检测来做退避。真遇到编译错误时不要慌,大概率是某个开发头文件版本不对,而不是内核不支持。还有一点,4.19内核对应的glibc版本如果偏老,某些新版本OpenSSL链接时可能会有符号问题,所以后面我才会强调openssl-devel版本的选择。
2. 环境准备与关键物料清单
动手之前先把环境捋清楚。我这边构建机用的是某款RPM系的Linux发行版,内核正好是4.19,CPU架构是aarch64,也就是ARMv8的64位模式。OpenSSH 8.6p1源码包从官方发布渠道下载,那边提供的是tar.gz和tar.gz.asc签名文件,建议都下下来,至少做一次SHA256校验。
2.1 构建机依赖安装
rpmbuild不是系统默认装好的,需要手动安装工具链和依赖。下面是我实际跑的安装命令,根据你的发行版包管理器略作调整:
# 安装rpmbuild基本工具 yum install -y rpm-build rpmdevtools # 编译工具链 yum install -y gcc make # OpenSSH编译需要的核心依赖 yum install -y openssl-devel pam-devel zlib-devel # 打包和测试辅助工具 yum install -y rpm-devel这里有个容易踩的坑:openssl-devel的版本会直接影响OpenSSH的configure结果。OpenSSH 8.6p1官方要求的OpenSSL版本是1.1.1以上,但如果你系统里装的是OpenSSL 3.x的devel包,configure阶段可能会提示头文件版本过于激进,需要加--without-openssl-header-check来跳过检查。我这边环境比较保守,系统自带的是OpenSSL 1.1.1系列,所以没这个问题。
PAM依赖也别忘了。OpenSSH的--with-pam选项需要pam-devel头文件,缺了它会在make阶段报错说找不到pam_*.h。Zlib是必选的,除非你打算用--without-zlib,但那样会禁用掉压缩支持,一般没人这么干。
2.2 准备rpmbuild目录结构
RPM打包有固定的目录约定,用rpmdev-setuptree命令一条龙建好:
rpmdev-setuptree执行后会在当前用户家目录生成rpmbuild目录,包含五个子目录:BUILD、RPMS、SOURCES、SPECS、SRPMS。它们各自的用途如下:
- BUILD:rpmbuild解压源码并执行编译的临时工作目录。
- RPMS:打包生成的二进制RPM输出目录,里面会按架构分子目录,比如aarch64。
- SOURCES:放置源码压缩包、补丁文件、配置文件等原物料。
- SPECS:放置spec文件,这是打包的“图纸”。
- SRPMS:生成的源码RPM输出目录。
把下载好的openssh-8.6p1.tar.gz丢进SOURCES目录,然后开始写spec。
2.3 源码校验
这一步很多人跳过,但我不建议。源码包在网络上传输是有可能被篡改的,尤其内网环境如果经过多个镜像转发,谁也不能保证文件完整性。校验方式很简单:
sha256sum openssh-8.6p1.tar.gz把输出的哈希值和官方发布的SHA256值比对。如果对得上再继续,对不上就重新下载。这既是安全习惯,也能避免一个很尴尬的打包错误:源码包损坏,rpmbuild执行%prep时解压失败,然后你排查半天以为spec写错了,实际是压缩包缺字节。
3. 编写spec文件:核心细节与参数选择
spec文件是RPM打包的核心工程图纸。写得好不好,直接决定了包能不能装上、能不能用、能不能维护。我把完整spec贴出来,然后一行一行解释关键部分。
Name: openssh Version: 8.6p1 Release: 1%{?dist} Summary: An open source implementation of SSH protocol version 2 License: BSD URL: http://www.openssh.com/ Source0: openssh-%{version}.tar.gz BuildRequires: gcc make openssl-devel pam-devel zlib-devel Requires: openssl-libs pam zlib %description OpenSSH is a complete implementation of the SSH protocol version 2. %prep %setup -q %build %configure \ --prefix=/usr \ --sysconfdir=/etc/ssh \ --mandir=/usr/share/man \ --with-pam \ --with-privsep-path=/var/empty/sshd \ --with-md5-passwords=no \ --disable-static make %{?_smp_mflags} %install rm -rf %{buildroot} make install DESTDIR=%{buildroot} # 清理不需要的静态库 find %{buildroot} -name '*.a' -delete %files %{_bindir}/ssh %{_bindir}/scp %{_bindir}/sftp %{_bindir}/sftp-server %{_bindir}/ssh-add %{_bindir}/ssh-agent %{_bindir}/ssh-keygen %{_bindir}/ssh-keyscan %{_sbindir}/sshd %{_mandir}/man1/*.1.gz %{_mandir}/man5/*.5.gz %{_mandir}/man8/*.8.gz %config(noreplace) %{_sysconfdir}/ssh/ssh_config %config(noreplace) %{_sysconfdir}/ssh/sshd_config %pre # 升级前备份配置文件(如果之前是rpm安装且被改过) if [ -f /etc/ssh/sshd_config ]; then cp -f /etc/ssh/sshd_config /etc/ssh/sshd_config.rpmsave.$(date +%Y%m%d) fi %post # 如果sshd服务在运行则重启,否则不启动 if systemctl is-active sshd.service >/dev/null 2>&1; then systemctl restart sshd.service fi %preun # 卸载时如果服务在运行则停止 if [ "$1" -eq 0 ]; then systemctl stop sshd.service fi %postun if [ "$1" -ge 1 ]; then systemctl restart sshd.service 2>/dev/null || : fi %changelog * Tue Jan 1 2024 Your Name <you@example.com> - 8.6p1-1 - Build OpenSSH 8.6p1 from source3.1 Name、Version、Release这三个字段的工程意义
Name、Version、Release组合起来就是RPM的完整标识符(NVR),格式是Name-Version-Release。这里Version必须是源码包里的版本字符串,也就是8.6p1,不能改。如果你自己在源码基础上打了补丁,可以在Release字段体现,比如1%{?dist},意思是基准Release为1,再加上发行版后缀。千万不要动Version,否则以后升级时RPM的版本比较逻辑会错乱,导致新包被认为比旧包老,拒绝安装。
另外Version字段里不能带横杠(-),这是RPM的硬性规定。8.6p1这个版本号里没横杠,刚好合规。如果哪天你手里的源码版本号是类似8.6-p1这种,要记得把横杠替换掉。
3.2 configure参数详解
%configure宏会自动填充大量常用参数,它会从rpmbuild环境变量里推导--prefix、--exec-prefix这些值。但OpenSSH这种软件比较特殊,默认值未必符合Linux发行版的约定,所以我在%configure后面显式叠加了关键参数。
--prefix=/usr 是让二进制装到/usr/bin和/usr/sbin下,符合常规PATH。--sysconfdir=/etc/ssh是指定配置文件目录,这是Linux上OpenSSH的传统位置,不能改,否则sshd启动时找不到配置。--with-pam启用PAM认证支持,这个强烈建议开启,现代Linux账号认证都走PAM,不开的话密码认证会变成直接读shadow文件,容易出权限问题。--with-privsep-path=/var/empty/sshd是权限分离机制的隔离目录,sshd会把特权子进程降权到这个目录里运行,这是安全设计的一部分,不能省,而且这个目录默认应该由包安装脚本创建。--with-md5-passwords=no则是明确禁止老的MD5密码哈希,现在没有理由再用MD5存密码了。--disable-static是为了不生成非必要的静态库,减少包体积和攻击面。
值得单独说一句的是,我没有加--with-ssl-dir。这是因为%configure宏已经通过CFLAGS和LDFLAGS指向了系统的OpenSSL头文件和库目录,再加反而可能干扰。如果你的构建机上有多套OpenSSL路径,才需要考虑显式指定。
3.3 %files清单和%config(noreplace)
写%files是打包过程中最容易被忽略出错的一步。RPM的机制是:只有列在%files里的文件才会被打进包,漏掉任何文件都会在安装后发现缺斤少两。我列表里把ssh、scp、sftp、sshd这些主二进制放前面,然后是一系列man手册页,最后是两个配置文件。
配置文件中用了%config(noreplace),这是什么意思?RPM在升级时遇到配置文件有两种处理逻辑:普通%config遇到系统里已有同名文件时会直接覆盖;%config(noreplace)则更保守,如果原文件被本地管理员改过,就保留原文件,把新配置写成一个.rpmnew后缀文件。对sshd_config这种大概率会被管理员改动的文件,用noreplace是绝对正确的选择,否则升级一次就丢一次自定义配置。
我在%pre脚本里又加了一层备份逻辑:每次升级前把当前sshd_config复制一份带时间戳的备份。这种双保险的做法在系统服务类软件里很推荐,虽然%config(noreplace)能防止覆盖,但如果你想直接对比新旧差异,手动备份会更方便。
4. 完整打包流程:从源码到RPM
spec文件写好后,打包过程本身其实很简单,就是一条命令的事。但命令背后的阶段执行机制,和每个阶段可能会出的问题,值得展开讲。
4.1 执行rpmbuild
cd ~/rpmbuild/SPECS rpmbuild -ba openssh.spec-ba参数表示同时构建二进制RPM(binary rpm)和源码RPM(source rpm)。构建过程会依次执行spec里的%prep、%build、%install、%files检查、%post/%preun等脚本段。
执行时的输出会很长,核心观察点是每个阶段是否以exit 0结束。我遇到过一个情况:make阶段跑完了,但make install阶段因为DESTDIR路径里有空格导致安装路径错乱,检查输出时看到install: cannot create regular file这类信息才发现。所以不要只看最后的error summary,中间阶段的warning也要扫一眼。
4.2 构建过程的四个阶段
%prep阶段最主要的工作是%setup -q,它会把SOURCES目录下的openssh-8.6p1.tar.gz解压到BUILD目录,并切换到解压后的源码目录。-q是quiet模式,减少冗余输出。如果你有补丁要打,在这个阶段用%patch指令应用。
%build阶段执行configure和编译。前面spec里的%configure宏和一堆参数在这里生效。编译耗时根据机器性能不同,快则几分钟,慢则十几分钟。make %{?_smp_mflags}会自动启用多核并行编译,_smp_mflags宏通常会被rpmbuild自动设置为-jN(N是CPU核数),不用手动指定。
%install阶段是关键,make install DESTDIR=%{buildroot}把文件装到一个临时目录里,这个目录就是将来RPM安装时的根目录。RPM打包里的一切都是基于DESTDIR的,不要在BuildRoot之外写任何文件。
最后rpmbuild会检查%files清单里列出的文件是否真的存在于buildroot里,缺一个就报错“File not found: /usr/bin/foo”,这时候需要回spec里把路径改对。
4.3 生成的包与本地验证
构建成功后,在~/rpmbuild/RPMS/aarch64/目录下会生成openssh-8.6p1-1.某个发行版.aarch64.rpm文件,在SRPMS目录下生成对应的源码RPM。源码RPM很重要,建议留存,以后想重新构建或者增补补丁时,直接基于SRPM操作,不需要重新写spec。
本地验证分几步走,先看包信息:
rpm -qip openssh-8.6p1-1.*.aarch64.rpm rpm -qlp openssh-8.6p1-1.*.aarch64.rpm第一条命令查看基本信息,第二条查看包里会安装哪些文件。重点确认一下/usr/bin/ssh、/usr/sbin/sshd、/etc/ssh/sshd_config这些核心路径是否齐全。因为我是做的升级安装,先做了快照备份,然后:
rpm -Uvh openssh-8.6p1-1.*.aarch64.rpm-Uvh表示升级安装,会保留原配置文件(因为noreplace),如果旧版本是由发行版RPM安装的,包管理器会自动完成文件替换。装完之后立刻验证:
ssh -V sshd -tssh -V看版本,sshd -t会做一次配置语法检查,如果输出没有报错,说明配置没问题。然后再重启sshd服务,最后另开一个终端测试能不能正常登录,确认没问题再关掉当前连接。这一步极其重要,我见过有人升级sshd后直接断了自己连接,然后就再也进不去了,只能在带外控制台上处理。
4.4 升级与回滚测试
验证通过后,测试回滚也很有必要。用旧版本的RPM包执行:
rpm -Uvh --oldpackage openssh-旧版本.aarch64.rpm确认能回到旧版本,再装回新版本。这套“先备、再装、验证、回滚”的流程做完,包才敢拿到生产环境分发。在内网环境里批量分发时,可以用:
yum install -y ./openssh-8.6p1-1.*.aarch64.rpm本地文件安装,yum会自动处理依赖。
5. 常见报错与排查实录
这部分是这次打包花费时间最多的环节,把典型的报错和排查思路整理成速查表,遇到问题可以直接对着查。
5.1 configure时报OpenSSL头文件版本不匹配
如果系统OpenSSL是3.x版本,执行configure时可能出现类似“OpenSSL header check failed”的报错。这是因为OpenSSH编译时会对头文件和库文件版本做一致性检查,防止头文件版本和链接库版本打架。我排查时先确认了系统里openssl和openssl-devel版本一致:
openssl version rpm -q openssl-devel如果确实一致,但OpenSSH仍然拒绝编译,可以在configure参数里加--without-openssl-header-check跳过这个检查。需要说明的是,跳过检查后编译产物在SSH运行时也依赖系统的OpenSSL库,只要系统库本身版本正常,不会引入额外风险。
5.2 make阶段报错缺PAM头文件
报错信息类似于:
pam_*.h: No such file or directory原因是系统里没装pam-devel。PAM的库文件通常在,但头文件在pam-devel包里。安装一下再重新rpmbuild即可。这类依赖缺失问题在rpmbuild时其实可以通过BuildRequires字段提前预防,包管理器在执行构建前会检查构建依赖,缺了就提示安装,这比自己瞎猜要高效得多。
5.3 打包时提示文件已存在于其他包中
这是重复打包时的经典问题。执行rpmbuild后报错:
File /usr/sbin/sshd is already packaged in ...很可能系统里其他包(比如某个发行版自带的旧openssh包)也声明拥有这个文件,或者你重复打包时上一个构建产物残留在buildroot里。排查步骤依次是:清理buildroot(rpmbuild默认会自动清理,但如果你用了--noclean或临时改过脚本则可能残留);检查是不是系统的openssh-server包已经提供了这个路径;用rpm -qf确认文件归属。
5.4 升级后sshd服务启动失败
这个问题最棘手,症状是升级完服务起不来,排查时先看日志:
journalctl -u sshd.service -n 50常见原因有几个:
- 配置文件权限不对,sshd要求sshd_config权限不能太宽,通常是0600或0644,且属主必须是对的。
- 主机私钥权限太大,sshd会拒绝启动。
- 配置里指定的算法与OpenSSL 1.1.1不完全兼容,导致加载密钥时报错。
- 旧的PAM配置里引用了不存在的模块文件。
我遇到过一次,新版本的sshd对配置文件里某些过时命令不再支持,sshd -t会明确提示不认识的指令。解决办法是注释掉或改成新版支持的等价写法。由于我在%pre脚本里做了配置文件备份,直接拿备份对比,定位就很快。
5.5 安装后ssh命令能执行但sshd服务没自动启动
有些环境里的服务管理脚本不会自动处理sshd的启用。RPM安装后sshd可能处于未启动状态,需要手动:
systemctl enable --now sshd.service或者如果系统用的是传统的SysV init,则使用service sshd start。在%post脚本里我做了“如果原本是启动的,重启它;如果原本没启动,就不动”的逻辑,这样既不会因为升级把服务搞挂,也不会无缘无故在别人机器上拉起一个新服务。
6. 一些经验总结和后续扩展思路
打包这件事做到能生成一个可安装的RPM,其实只是第一步。我实际用下来还有几点体会。
第一,spec文件一定要纳入版本管理。把它放在和源码、补丁一起维护的目录里,每次构建都明确记录哪个版本对应哪个spec。我自己的做法是给每个spec文件加注释,说明基于哪个根环境构建、用了什么特殊参数,避免两个月后自己都忘了当初为什么那么写。
第二,构建环境要尽量干净。我专门用一个虚拟化的aarch64环境来构建,里面除了编译工具链和依赖库,不装多余的东西。这样能保证rpmbuild过程可重复,不会因为构建机上某个自定义库导致产物带有不确定性。
第三,注意密钥和配置文件的生命周期。升级OpenSSH时,主机密钥千万不要删除。旧版本生成过的/etc/ssh/ssh_host_*_key文件要原样保留,否则客户端会报host key verification failed,要求用户重新确认指纹。我碰到过有同事图省事直接删了密钥目录,结果整个内网所有机器都要改known_hosts,相当折腾。
以后如果要从8.6p1继续升级,比如升到9.x系列,这套spec可以继续复用,主要是核对一下新版本有没有移除或新增configure选项,以及检查一些老旧的算法指令是否还能被新版sshd识别。打包的底层方法论是一样的,但每次编译新版本,都要在测试环境完整走一遍“升级-登录-回滚”三个动作,再考虑生产分发。