前不久在客户现场碰到一个特别标准的内网项目:Linux 服务器完全隔离,数据库安装脚本一跑就报缺 zlib,OpenSSH 升级要新版 openssl,账号锁定策略依赖 pam 又一直不生效。离线安装 zlib、openssl、pam 这三个库,就这样变成躲不掉的必修课。这篇文章不准备讲太多虚的,就是把这套流程从头到尾捋一遍,每一个 configure 参数、每一段踩过的坑,都按实际操作顺序写清楚。往小了说是给部署 PostgreSQL、MySQL、Nginx、OpenSSH 补齐前置依赖;往大了说,摸透离线编译这套套路,以后在内网环境装任何源码包心里都有底。
1. 为什么偏偏是这三个库:一句话讲清三者关系和编译顺序
1.1 zlib、OpenSSL、PAM 各自解决什么问题
zlib 是通用的数据压缩库,体积不大但出镜率极高。PostgreSQL 需要它处理压缩,很多软件的 configure 检查阶段会去找 zlib.h 和 libz。你要是从来没在意过它,等 configure 报出zlib.h not found那一刻,才知道欠的债迟早要还。
OpenSSL 是加密通信的底层事实标准,提供 SSL/TLS 协议和各类加解密算法。OpenSSH、Nginx 的 HTTPS、各种编程语言的 ssl 模块,底层都在跟它打交道。CentOS 7 这类老系统自带的 OpenSSL 1.0.2 放到今天就是安全扫描报告上的常客,不升不行。
PAM 是可插拔认证模块,Linux 登录认证的抽象层。sshd、su、sudo、login 全走 PAM 接口。离线环境里频繁碰到的问题是:系统自带的 PAM 没有开发头文件,或者缺少某个特定模块(比如 pam_tally2、pam_cracklib),导致源码编译 OpenSSH 时开了--with-pam却找不到 pam_appl.h。
1.2 什么项目会同时依赖这三件套
典型场景是一条完整部署链:内网新装 PostgreSQL 或 MySQL → configure 检查缺 zlib 头文件和库 → 装完 zlib 又发现系统 OpenSSL 版本太低 → 升级 OpenSSL → 顺手编译 OpenSSH 时又需要 PAM 开发文件。三件套就是这条链路里最先要打通的地基。
另一个常见场景是国产化系统适配。我在银河麒麟这类基于 RPM 的国产发行版上部署 Nginx 时,经常遇到自带库版本老旧的问题,系统包管理器又没维护那么及时,最终都是靠离线源码编译解决。
1.3 编译顺序为什么是 zlib、openssl、pam
顺序很明确:先 zlib,再 openssl,最后 pam。
- OpenSSL 的编译参数里有 zlib 选项,编译前必须先把 zlib 装好,并把头文件和库路径告诉 OpenSSL,否则它只能退回"无 zlib 支持"模式。
- PAM 和 zlib、OpenSSL 之间没有编译期依赖,放第二或第三位都行。但因为它会被 OpenSSH 等软件引用,习惯上放在 zlib 和 OpenSSL 之后。
- 所有第三方软件(Nginx、PHP、PostgreSQL)最后装,统一引用这三个自定义路径下的库。
这个顺序不能乱。曾经有一次我先编 OpenSSL,忘了 zlib 还没装,configure 阶段就报错,回头补装 zlib 之后又得重新 configure 一遍 OpenSSL,白白浪费半小时。
还有个容易被忽略的底层逻辑:如果你所在环境能用 rpm 或 deb 离线包解决问题,优先用包管理器。源码编译是兜底方案,它最大的优势是能把新库放在独立前缀目录(比如 /usr/local/zlib、/usr/local/openssl),和系统自带版本共存,不破坏包管理器记录。
2. 离线前的准备:源码包下载、版本取舍与工具链自检
2.1 三个源码包从哪里下、怎么校验完整性
离线环境安装前最耗时间的不是编译,而是准备阶段的反复折腾。源码包来源要可靠,我一般固定用这几个官方渠道:
- zlib 官方站下载 zlib-1.2.13.tar.gz(zlib.net)
- OpenSSL 官方站下载 openssl-3.0.12.tar.gz(openssl.org/source)
- Linux-PAM 官方仓库下载 Linux-PAM-1.5.3.tar.xz(github.com/linux-pam/linux-pam/releases)
下载的同时务必把对应的.sha256或.asc签名文件一并拉下来。在能上网的工作机上下载后,先执行:
sha256sum zlib-1.2.13.tar.gz openssl-3.0.12.tar.gz Linux-PAM-1.5.3.tar.xz对比输出和官方页面公布的哈希值。离线环境这一步不能省。包已经传进内网再发现损坏或者校验值对不上,重新来一遍的沟通和传输成本远高于这几十秒的检查。
2.2 版本选择的三个判断标准
版本选择主要看三件事:支持周期、目标程序兼容性、系统架构。
OpenSSL 我的长期实践体会是优先选 3.0 系列的 LTS 版本,支持到 2026 年。1.1.1 系列虽然经典,但已经停止维护了,除非目标程序硬绑定 1.1.1 API,否则没必要选老版本。3.1 到 3.5 属于标准版本,支持周期比 LTS 短,做长时间运维的话不如 3.0.x 稳。
zlib 选最新稳定版即可,它 API 稳定,不用纠结。Linux-PAM 选 1.5.x。但这里有个坑:Linux-PAM 的压缩包是 .tar.xz 格式,解压需要 xz 工具,目标机器如果没有 xz,得提前把 xz 的离线包也准备好,否则解压这关就过不去。
2.3 动手前先检查的编译环境
离线环境最怕的不是缺这几个库,而是缺编译器。在目标机器上先执行:
gcc --version make --version perl --version三个命令都必须正常输出版本号。gcc 和 make 是编译的基础,perl 则是 OpenSSL 编译过程中生成脚本和符号文件的必需品。如果 gcc 都没有,那得先解决编译器离线安装的问题,这个工程量就大了。
还要用uname -m确认目标系统架构,x86_64、aarch64 还是其他。源码包本身不分架构,但 OpenSSL 的 Configure 会根据平台选择汇编优化实现,架构不匹配会直接影响优化代码能否启用。
另外建议动手前跑一次df -h看根分区剩余空间。OpenSSL 源码编译的中间文件能占 2~3GB,我踩过磁盘写满导致编译报错位置飘忽不定的坑,这个后面单独说。
2.4 源码包传进内网的常规做法
方法不复杂:工作机下载好源码包和校验文件后打成一个 tar 包,通过 U 盘或者内网文件服务器传到目标机器。公司有内部软件源的,也可以把源码包丢到内网 HTTP 服务器上,目标机 wget 拉取。
我自己的固定习惯是:所有源码包放在 /usr/local/src 目录下,解压后的目录名保留完整版本号,将来要复查版本或者重新编译,一眼就能看出当前环境用的什么版本。这个习惯在同时管理多台内网服务器时特别有用。
3. zlib 离线编译:小库也有大讲究
3.1 编译命令与关键输出
zlib 的编译流程很简洁,三条命令:
cd /usr/local/src tar zxf zlib-1.2.13.tar.gz cd zlib-1.2.13 ./configure --prefix=/usr/local/zlib make -j$(nproc) make installconfigure 执行完以后,注意看输出里有一行关键信息:
Checking for shared library support... yes如果是 yes,说明会同时生成 libz.so 和 libz.a;如果显示 no,说明系统环境生成不了动态库,后续用 zlib 的程序运行时可能找不到 libz.so。这种情况通常是因为缺少 binutils 相关组件,需要先解决工具链问题再回头编译。
3.2 动态库与静态库的取舍
zlib 编译默认会同时生成静态库和动态库,但装到 /usr/local/zlib 之后,很多人会困惑:为什么程序编译时找到了 libz.a,运行时却报找不到 libz.so?
这个问题的根源在于:程序链接时优先选择静态库还是动态库,取决于编译参数里有没有-static或者库路径下有哪些文件。如果你看到一个程序编译通过了但运行时报动态库缺失,基本可以断定它当初链接的是动态库版本,只是运行时没有把 /usr/local/zlib/lib 加入动态库搜索路径。
解决办法是提前把路径写进 ldconfig:
echo "/usr/local/zlib/lib" > /etc/ld.so.conf.d/zlib-local.conf ldconfig然后ldconfig -p | grep libz确认 libz.so.1 已经进入缓存。动态库搜索路径这件事,是整个离线编译过程中最值得花时间理解的概念,后面 OpenSSL 那节还会碰到。
3.3 OpenSSL 找不到 zlib 的两种解法
zlib 装到 /usr/local/zlib 之后,OpenSSL 的 config 默认不会去这个目录找头文件和库,需要显式告诉它:
./config --prefix=/usr/local/openssl --openssldir=/usr/local/openssl \ shared zlib \ --with-zlib-include=/usr/local/zlib/include \ --with-zlib-lib=/usr/local/zlib/lib如果你习惯用环境变量,也可以用这个方式:
export CPPFLAGS="-I/usr/local/zlib/include" export LDFLAGS="-L/usr/local/zlib/lib"两种方式实测都可以。用--with-zlib-include这种参数,路径会直接写进 Makefile,可读性更好,不用依赖 shell 环境变量,所以我更推荐第一种。
还要提醒一个 64 位系统的经典坑:zlib 默认把库装到 /usr/local/zlib/lib,不会自动区分 lib64。有些第三方软件 configure 检查时只去 lib64 目录找库,明明装了却提示找不到。遇到这种情况不用跟软件较劲,直接在 LDFLAGS 里把两个目录都加上:
export LDFLAGS="-L/usr/local/zlib/lib -L/usr/local/zlib/lib64"或者干脆做一个软链接:
ln -s /usr/local/zlib/lib /usr/local/zlib/lib64在银河麒麟这类按 lib64 规范组织库文件的系统上,这个软链接能省不少排查时间。
4. OpenSSL 离线编译:版本冲突是最大的坎
4.1 configure 参数逐项拆解
OpenSSL 的编译命令要稍微复杂一些:
cd /usr/local/src tar zxf openssl-3.0.12.tar.gz cd openssl-3.0.12 ./config --prefix=/usr/local/openssl \ --openssldir=/usr/local/openssl \ shared zlib \ --with-zlib-include=/usr/local/zlib/include \ --with-zlib-lib=/usr/local/zlib/lib make depend make -j$(nproc) make install参数含义逐条说:
--prefix:安装根目录,所有文件都会放到这个目录下。--openssldir:OpenSSL 运行时配置文件和证书目录的存放位置,通常和 prefix 保持一致。shared:生成动态库 .so,不加这个参数只生成静态库,很多程序运行时会找不到 libcrypto.so、libssl.so。zlib:启用 zlib 压缩支持,配合后面两个--with参数指定 zlib 路径。make depend:生成依赖关系。这个步骤容易被跳过去,一旦跳过,后续编译可能遇到莫名其妙的符号错误或者链接失败。
在我实际编译过程中,make depend这一步不是摆设。改过一次 Configure 参数之后没有执行它,结果 make 到一半报了一些找不到符号的错误,清掉重来加上这一步就好了。
4.2 "openssl version mismatch" 到底在吵什么
热搜词里有条报错很典型:
openssl version mismatch. built against 30000020, you have 30500060这个报错的本质是:某个程序在编译时,头文件拿到的 OpenSSL 版本是 3.0.2(30000020 是 OpenSSL 版本号的十六进制编码),但程序运行时实际加载的 libcrypto.so 是另一个版本,动态链接器发现版本对不上就直接拒绝启动。
出现这种错乱,常见原因有两个:
一是系统装了多个 OpenSSL。系统默认路径下有一个 libssl.so,自定义编译的 OpenSSL 又放在 /usr/local/openssl/lib,程序编译时头文件指向了自定义路径,但运行时 LD_LIBRARY_PATH 没配好,反而加载了系统路径下的老库。
二是新旧库的 soname 相同。OpenSSL 1.1.1 和 3.x 系列的 libcrypto.so、libssl.so 的 soname 都叫 libcrypto.so.3、libssl.so.3,路径一混,加载到哪个版本完全看运气。
排查手法很直接:编译完的程序用ldd看实际加载的库路径,再对照编译时指定的头文件路径,两者必须指向同一个 OpenSSL 安装目录。
4.3 ldconfig 与动态库查找顺序的博弈
Linux 下动态库的查找顺序,优先级从高到低大致是:LD_LIBRARY_PATH 环境变量、/etc/ld.so.cache(由 ldconfig 生成)、默认路径 /lib 和 /usr/lib。
也就是说,配置了 ldconfig 并不代表万事大吉。如果某个服务的启动脚本里手动 export 了 LD_LIBRARY_PATH 指向旧路径,ldconfig 里的新路径会被直接跳过。排查版本类问题时,第一反应应该是:
env | grep LD_LIBRARY_PATH确认有没有环境变量在抢优先级。然后再看 ldconfig 缓存:
ldconfig -p | grep ssl把这两步做完,动态库加载路径基本就能理清了。
4.4 绝不直接覆盖系统 OpenSSL 的理由
有不少人在内网编译完新 OpenSSL 以后,觉得直接替换 /usr/bin/openssl,或者删掉 /usr/lib64/libssl.so 让系统用新版,这样一劳永逸。我强烈不建议这么做。
系统自带的 OpenSSL 往往被 yum/rpm 的依赖关系绑死,wget、curl、rpm 本身都链接着它。直接替换或删除系统库,轻则 yum 瘫痪,重则整个系统包管理机制崩溃。我在一台 CentOS 7 上见过同事把系统 OpenSSL 换成 3.0 之后,yum 直接报libcrypto.so.10: cannot open shared object file,最后只能靠 rpm 离线包抢救。
正确做法是让新 OpenSSL 独立待在 /usr/local/openssl,有程序需要新版本时,通过编译期指定头文件和库路径、运行期配置 ldconfig 或 LD_LIBRARY_PATH 指向它。安全扫描要求版本号好看的话,做软链接到 /usr/local/bin:
ln -s /usr/local/openssl/bin/openssl /usr/local/bin/openssl让 /usr/local/bin 排在 PATH 前面即可,系统本身不被触碰。
5. PAM 离线编译:认证模块的接入与共存
5.1 编译流程与两个前置检查
Linux-PAM 的编译命令相对简单:
cd /usr/local/src tar Jxf Linux-PAM-1.5.3.tar.xz cd Linux-PAM-1.5.3 ./configure --prefix=/usr/local/pam make -j$(nproc) make install两个前置检查必须做:
第一,解压前确认 xz 是否安装。不带-J参数直接tar xvf会报错,用tar Jxf能提前暴露 xz 缺失的问题。
第二,configure 会检测一些可选依赖,比如 libcrack、libdb、libprelude。离线环境下这些检测失败不可怕,只会导致对应模块不被编译。真正要关心的是基础模块是否编译成功,编译完以后去 /usr/local/pam/lib/security/ 目录下看 .so 文件列表。如果 pam_unix.so、pam_permit.so、pam_deny.so 这些核心模块都在,说明主流程没问题。
5.2 与系统自带 PAM 的共存策略
大多数发行版本来就有 PAM,系统目录下有一堆 pam_*.so,还有完整的 /etc/pam.d/ 配置目录。我们自己源码编译的 PAM,应该待在 /usr/local/pam 前缀下,不要去覆盖系统的 /lib/security。
原因很简单:系统原有 PAM 配置和认证流程是经过发行版测试的,直接动它有风险。密码强度策略、sudo 认证、sshd 登录,全靠现有 PAM 模块在撑。自己编译的 PAM 主要用途是给 OpenSSH 这类自编译软件提供开发头文件(pam_appl.h、pam_misc.h),以及提供系统可能没有的新版模块。
编译 OpenSSH 时指定 PAM,基本是这个套路:
./configure --prefix=/usr/local/openssh \ --with-pam \ CPPFLAGS="-I/usr/local/pam/include" \ LDFLAGS="-L/usr/local/pam/lib"注意不同版本 OpenSSH 对 PAM 模块路径的处理不完全一样,拿不准时先./configure --help | grep pam确认选项。
5.3 用最小的 C 程序验证 PAM 可用
编译完 Linux-PAM,想确认安装没问题,最快的方式是写一个最小的 C 程序调用 PAM 接口:
#include <security/pam_appl.h> #include <stdio.h> int main(void) { pam_handle_t *pamh = NULL; struct pam_conv conv = { NULL, NULL }; int ret = pam_start("system-auth", "user", &conv, &pamh); if (ret == PAM_SUCCESS) { printf("pam_start OK, PAM works\n"); } else { printf("pam_start failed: %s\n", pam_strerror(pamh, ret)); } if (pamh) { pam_end(pamh, ret); } return 0; }编译执行:
gcc pamtest.c -o pamtest -I/usr/local/pam/include -L/usr/local/pam/lib -lpam LD_LIBRARY_PATH=/usr/local/pam/lib ./pamtest能输出 pam_start OK,说明开发环境和运行时库都可用。这一招排查 OpenSSH 编译后启动问题特别有效。
PAM 还有个经常遇到的坑:pam_unix.so 读取 /etc/shadow 需要 root 权限,如果程序以普通用户运行会提示 Authentication failure。这不是编译问题,是权限问题,别被误导去重编库。
6. 三库装齐后的验收清单:每一步都有明确输出
6.1 OpenSSL:版本、算法、协议三步走
装完不能只看 openssl version 输出个版本号就算完,要验证它实际能不能干活。
/usr/local/openssl/bin/openssl version -a这个命令输出里能看到完整的编译配置信息,包括编译器版本、配置参数,确认 zlib 支持是否真的编进去了:
/usr/local/openssl/bin/openssl version -a | grep -i zlib验证随机数功能:
/usr/local/openssl/bin/openssl rand -hex 32能输出 64 位十六进制字符串,说明随机数引擎正常。验证协议栈用 s_client:
/usr/local/openssl/bin/openssl s_client -connect 192.168.x.x:443 -tls1_2能完成 TLS 握手并输出证书链,说明协议栈没有问题。证书转换是另一个高频用法:
/usr/local/openssl/bin/openssl x509 -in cert.pem -outform DER -out cert.der这一步主要是验证 x509 相关功能可用,内网部署 HTTPS 的时候早晚用得上。
6.2 zlib:ldconfig 与 ldd 双验证
zlib 验收分两个层面。先看动态库缓存:
ldconfig -p | grep libz能看到 /usr/local/zlib/lib/libz.so.1 被收录,说明 ldconfig 配好了。再看 OpenSSL 实际链接的库:
ldd /usr/local/openssl/bin/openssl | grep libz如果输出是 libz.so.1 指向 /usr/local/zlib/lib/libz.so.1,说明新 zlib 生效了。如果指向系统路径,也不必惊慌,至少说明链接器找到了一个可用的 zlib,功能不受影响,只是没有优先用我们编译的那份。
6.3 PAM:模块文件与头文件核查
PAM 验收从两个目录下手。模块文件:
ls -l /usr/local/pam/lib/security/重点确认 pam_unix.so、pam_permit.so、pam_deny.so、pam_rootok.so 这些核心模块都在。头文件:
ls -l /usr/local/pam/include/security/pam_appl.h、pam_misc.h、pam_modules.h 必须存在,缺一个后面编译 OpenSSH 都会遇到头文件找不到的问题。
6.4 用真实服务编译做一次联动演练
把三库串起来的最高效方式,是编译一个同时依赖它们的东西。比如编译 Nginx:
./configure --prefix=/usr/local/nginx \ --with-zlib=/usr/local/src/zlib-1.2.13 \ --with-openssl=/usr/local/src/openssl-3.0.12这里有个必须特别指出的坑:Nginx 的--with-zlib和--with-openssl后面跟的是源码目录,不是安装目录。Nginx 的设计是把它内置编译一份 zlib 和 OpenSSL,而不是链接 /usr/local 下装好的那份。所以这个演练验证的是"源码包里这些库能不能正常协同编译",不代表 Nginx 动态链接了 /usr/local/openssl 下的库。
如果你希望改造一个直接链接自定义 OpenSSL 的程序,编译 PHP 或者 Python 更直观,它们的 configure 就是直接找系统里的库路径。到底用哪种思路,取决于项目需求。想统一管理版本,就尽量让软件直接链接 /usr/local/openssl;想省事,用 Nginx 内置编译的模式也能满足大部分场景。
7. 离线安装高频报错与排查思路
7.1 六类常见报错对照与处理
| 报错信息 | 根因 | 处理方法 |
|---|---|---|
configure: error: zlib.h not found | 找不到 zlib 头文件 | CPPFLAGS 加 -I/usr/local/zlib/include,或者给 configure 传对应参数 |
error while loading shared libraries: libz.so.1: cannot open shared object file | 运行期找不到 zlib 动态库 | 把 /usr/local/zlib/lib 写入 ld.so.conf.d 并 ldconfig |
openssl version mismatch. built against ... | 编译期头文件版本与运行期库版本不一致 | 检查 LD_LIBRARY_PATH 和 ldd 实际加载路径,统一到同一版本 |
No rule to make target 'libcrypto.so' | OpenSSL 编译状态混乱或依赖未生成 | make clean 后重新执行 make depend && make |
pam_appl.h: No such file or directory | 缺 PAM 开发头文件 | 确认 /usr/local/pam/include/security/pam_appl.h 存在,编译参数加 -I/usr/local/pam/include |
cannot find -lz | 链接阶段找不到 libz 库文件 | LDFLAGS 或 LIBRARY_PATH 加 -L/usr/local/zlib/lib |
这几类报错里,最磨人的是版本 mismatch 那类,因为报错信息不一定直接,往往是程序启动时崩溃,错误信息才带出 OpenSSL 版本号。排查思路固定:先env | grep LD_LIBRARY_PATH,再ldd看实际加载路径,最后对比编译时用的头文件路径。
7.2 编译中断后的现场恢复习惯
离线编译最常遇到的情况是编译到一半发现 configure 参数写错了。这时候千万别在当前目录里反复 make clean 试验,我的习惯是直接删掉整个解压目录,重新解压一份干净的源码包。
原因在于 configure 生成的文件遍布各个子目录,make clean 不一定清得干净,残留的 config 缓存可能让下一次 configure 无声地沿用错误的旧参数。源码包本身才几 MB,重新解压的成本远低于排查残留文件的时间成本。
改参数的过程建议脚本化。把 configure、make、make install 写成一个 build.sh,每次改参数就是改一行再跑一遍,既减少手误,也方便留下记录供后续维护参考。
7.3 磁盘空间和交叉编译两个隐藏坑
编译中间文件占用的空间远超想象。OpenSSL 编译时生成的 .o 目标文件、asm 汇编产物、测试程序,加起来能占 2~3GB。如果 /usr/local/src 所在的根分区剩余空间不够,编译会在随机位置报No space left on device。而且报错位置不固定,一会儿在汇编阶段,一会儿在生成文档阶段,很迷惑人。动手前df -h瞄一眼,能省下大量排查时间。
交叉编译是另一个容易被忽略的场景。如果你是在 x86 构建机上交叉编译给 ARM 机器用,三个库都不能直接跑常规 configure:
# OpenSSL ./Configure linux-aarch64 --prefix=/usr/local/openssl shared zlib # zlib CC=aarch64-linux-gnu-gcc ./configure --prefix=/usr/local/zlib # Linux-PAM CC=aarch64-linux-gnu-gcc ./configure --prefix=/usr/local/pam不同架构的 OpenSSL 需要指定对应的 Configure target,zlib 和 PAM 则通过 CC 环境变量指定交叉编译器。国产化适配和嵌入式设备上这个场景很常见,如果你不需要交叉编译,这段可以跳过,但真要搞的时候,务必先确认交叉工具链版本和新库的兼容性,避免编译产物在目标平台上跑不起来。
离线编译这件事,流程本身不复杂,复杂的是各种环境差异。我现在的固定套路是:所有源码包统一放 /usr/local/src,编译脚本 build.sh 和源码放一起,安装前缀统一 /usr/local/库名,ldconfig 配置单独写进 /etc/ld.so.conf.d 下的独立文件,绝不手动追加到 /etc/ld.so.conf。这套习惯坚持下来,就算换一个完全陌生的内网环境,也能在半小时内把三件套装完。你在实际安装中如果碰到其他奇怪报错,顺着"头文件缺没缺、库路径对不对、加载顺序对不对"这三个方向查,大概率能定位到根因。