OpenSSH 8.6p1源码手动打包RPM:aarch64架构下完整流程与踩坑实录 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环境。先说aarch64OpenSSH本身是高度可移植的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.19CPU架构是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。它们各自的用途如下BUILDrpmbuild解压源码并执行编译的临时工作目录。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-passwordsno \ --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 21; 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 youexample.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-passwordsno则是明确禁止老的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. 完整打包流程从源码到RPMspec文件写好后打包过程本身其实很简单就是一条命令的事。但命令背后的阶段执行机制和每个阶段可能会出的问题值得展开讲。4.1 执行rpmbuildcd ~/rpmbuild/SPECS rpmbuild -ba openssh.spec-ba参数表示同时构建二进制RPMbinary rpm和源码RPMsource 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自动设置为-jNN是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里。排查步骤依次是清理buildrootrpmbuild默认会自动清理但如果你用了--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识别。打包的底层方法论是一样的但每次编译新版本都要在测试环境完整走一遍“升级-登录-回滚”三个动作再考虑生产分发。