1. 国密改造的起点:为什么编译配置是绕不开的坎
做国密改造这件事,很多人第一反应是找个现成的二进制包直接替换,但真到生产环境里,你会发现这条路基本走不通。原因很简单:国密算法套件(SM2/SM3/SM4)在标准 OpenSSL 里默认是不开启的,而 Nginx 又强依赖 OpenSSL 提供的 TLS 握手能力。所以只要你的业务要求走国密 TLS 通道,编译配置这一步就躲不掉。
这篇是"编译配置 Nginx 支持国密"系列的第二篇,第一篇我们聊了环境准备和依赖梳理,这一篇重点落在编译参数怎么定、国密引擎怎么挂、配置怎么验证这三件事上。适合已经动手编译过一次、但卡在"编译过了却跑不起来"或者"跑起来了但抓包发现还是普通 TLS"的同行参考。如果你还没碰过编译,建议先把第一篇的环境部分过一遍,否则下面很多路径和依赖你会对不上。
我先说结论:国密 Nginx 的编译配置,核心不是 Nginx 本身,而是OpenSSL 的国密分支 + 国密引擎的加载方式 + Nginx 的 ssl 模块编译参数这三者的配合。任何一环没对齐,最后都会表现为"编译成功但握手失败"或者"握手成功但用的不是国密套件"。下面我按实际操作的顺序,把每一步的取舍和坑都摊开讲。
2. 编译前的整体设计思路与方案选型
2.1 三条技术路线的取舍
在动手之前,先明确你要走哪条路。目前国密 Nginx 的落地方式主要有三种,我列个表对比一下,方便你对号入座。
| 方案 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 国密分支 OpenSSL 静态编译 | 用支持国密的 OpenSSL 分支,Nginx 编译时静态链接 | 部署简单,无运行时依赖 | 升级 OpenSSL 需重新编译 Nginx | 中小规模、追求稳定 |
| 国密引擎动态加载 | 标准 OpenSSL + 国密引擎 so 文件 | Nginx 可复用系统 OpenSSL | 引擎加载配置繁琐,易出错 | 已有 OpenSSL 环境 |
| 国密网关代理 | Nginx 前置国密网关做卸载 | 业务无侵入 | 多一层转发,架构复杂 | 遗留系统改造 |
我个人的经验是:新项目直接走第一条路,静态编译虽然升级麻烦,但胜在可控。第二条路适合那些系统 OpenSSL 已经被其他组件强依赖、不能随便换的场景,但引擎加载的坑非常多,后面我会专门讲。第三条路属于架构层面的妥协,除非你确实动不了 Nginx,否则不建议。
2.2 为什么优先选静态编译
静态编译的核心逻辑是:把国密 OpenSSL 的库直接编进 Nginx 二进制里,运行时不再依赖系统的 libssl.so。这样做的好处是环境隔离——你编译出来的 Nginx 拿到同架构的另一台机器上,只要系统 glibc 版本兼容,基本能直接跑,不会出现"这台机器有国密引擎、那台没有"的问题。
代价也很明显:每次 OpenSSL 分支更新(比如修了某个国密相关的 bug),你都得重新编译一遍 Nginx。所以我的做法是把编译过程脚本化,参数固定下来,升级时改个版本号重跑脚本就行。这个脚本后面会给出来。
2.3 版本选择的经验
国密 OpenSSL 分支的版本选择有个坑:不是越新越好。新版本可能引入了对某些旧国密引擎的不兼容改动,而国密引擎本身更新往往滞后。我实测下来,选一个国密引擎明确声明支持的 OpenSSL 版本最稳妥,通常是某个 LTS 版本。Nginx 这边则建议选稳定版主线,不要用太老的版本,否则 ssl 模块的一些新参数用不了。
提示:版本搭配这件事没有万能答案,一定要以你用的国密引擎的官方说明为准。引擎说支持哪个 OpenSSL 版本区间,你就锁在那个区间里选,别自己往上跳。
3. 核心细节解析:编译参数与国密引擎挂载
3.1 Nginx 编译参数逐项拆解
Nginx 的./configure参数很多,但和国密相关的其实就几个关键项。我拿一份实际用过的配置举例:
./configure \ --prefix=/usr/local/nginx-gm \ --with-http_ssl_module \ --with-http_v2_module \ --with-openssl=/path/to/openssl-gm \ --with-openssl-opt="enable-sm2 enable-sm3 enable-sm4" \ --with-cc-opt="-I/path/to/openssl-gm/include" \ --with-ld-opt="-L/path/to/openssl-gm/lib -Wl,-rpath,/path/to/openssl-gm/lib"逐项说:
--with-http_ssl_module:这是国密 TLS 的载体,没有它 Nginx 根本不处理 TLS,必加。--with-openssl=:指向你的国密 OpenSSL 源码目录。注意这里是源码目录不是安装目录,Nginx 会自己调用它的编译流程。--with-openssl-opt=:传给 OpenSSL 编译的额外参数,用来开启国密算法支持。不同分支的参数名可能不一样,有的用enable-sm2,有的用sm2,以你的分支文档为准。--with-cc-opt和--with-ld-opt:指定头文件和库路径。如果你走静态编译,-Wl,-rpath这步其实可以省,但加上更保险,防止运行时找不到库。
这里有个容易忽略的点:--with-openssl和--with-ld-opt同时用时,如果路径不一致,会出现"编译时用 A 版本、链接时用 B 版本"的诡异问题,表现为编译通过但运行时报符号找不到。两个路径必须指向同一个 OpenSSL。
3.2 国密引擎的两种挂载方式
国密算法在 OpenSSL 里的实现,通常以引擎(engine)形式提供。挂载方式有两种:
方式一:编译进 OpenSSL。如果你的国密分支已经把 SM2/SM3/SM4 编进 libcrypto 了,那 Nginx 这边不用额外配置,直接在 nginx.conf 里用国密套件就行。这是最省心的方式。
方式二:动态加载引擎。OpenSSL 主库不含国密实现,需要运行时ENGINE_load加载一个 so 文件。这种方式下,Nginx 本身不感知引擎,得靠 OpenSSL 的配置文件(通常是 openssl.cnf)来指定默认引擎。
我踩过的坑是:方式二下,Nginx 启动时的工作目录和 OpenSSL 配置文件路径经常对不上,导致引擎加载失败但 Nginx 不报错,只是握手时静默回退到普通套件。排查方法是启动 Nginx 后用openssl s_client连一下,看协商出来的 cipher 是不是国密套件,如果不是,八成是引擎没加载上。
3.3 配置文件里的国密套件写法
编译搞定后,nginx.conf 里的 ssl 配置才是真正决定用不用国密的地方。关键几行:
server { listen 443 ssl; server_name your.domain; ssl_certificate /path/to/sm2_cert.pem; ssl_certificate_key /path/to/sm2_key.pem; ssl_ciphers ECDHE-SM2-WITH-SMS4-SM3:SM2-WITH-SMS4-SM3; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; }ssl_ciphers这行是核心,国密套件的命名规则是"密钥交换-认证-加密-摘要",比如ECDHE-SM2-WITH-SMS4-SM3就是 ECDHE 密钥交换、SM2 证书认证、SMS4 对称加密、SM3 摘要。不同 OpenSSL 分支的套件名可能略有差异,以你编译出来的openssl ciphers输出为准,别照抄网上的。
注意:国密证书和普通 RSA 证书不能混用。你的证书必须是 SM2 密钥对签发的,否则套件协商会失败。证书格式上,国密常用的是 PEM,但有些分支要求特定的封装,这个要提前确认。
4. 实操过程:从零编译到握手验证
4.1 完整编译流程记录
我把实际跑通的流程整理成步骤,你可以直接照着走。假设国密 OpenSSL 源码在/opt/openssl-gm,Nginx 源码在/opt/nginx-src。
第一步,编译国密 OpenSSL:
cd /opt/openssl-gm ./config --prefix=/usr/local/openssl-gm \ --openssldir=/usr/local/openssl-gm/ssl \ shared enable-sm2 enable-sm3 enable-sm4 make -j$(nproc) make install这里shared是生成动态库,如果你要静态编译 Nginx,可以去掉它生成静态库。enable-sm2这类参数按你的分支实际支持情况调整。
第二步,编译 Nginx:
cd /opt/nginx-src ./configure \ --prefix=/usr/local/nginx-gm \ --with-http_ssl_module \ --with-openssl=/opt/openssl-gm \ --with-openssl-opt="enable-sm2 enable-sm3 enable-sm4" \ --with-cc-opt="-I/usr/local/openssl-gm/include" \ --with-ld-opt="-L/usr/local/openssl-gm/lib -Wl,-rpath,/usr/local/openssl-gm/lib" make -j$(nproc) make install第三步,验证 Nginx 链接的 OpenSSL 版本:
/usr/local/nginx-gm/sbin/nginx -V输出里会显示built with OpenSSL x.x.x,确认这个版本是你编译的国密版本,而不是系统的。
4.2 参数计算与选择过程
编译参数里最需要动脑的是--with-openssl-opt和路径。我遇到过一种情况:国密分支的config脚本默认不开启 SM4,需要显式加参数。这时候如果你漏了,编译能过,但openssl ciphers里看不到国密套件。
判断方法很简单:编译完 OpenSSL 后,跑一句
/usr/local/openssl-gm/bin/openssl ciphers -v | grep -i sm如果输出里有 SM2/SM3/SM4 相关套件,说明算法开启成功;如果空的,回去检查enable-*参数。这一步我建议在编译 Nginx 之前就做,否则 Nginx 编完了才发现算法没开,白等半小时。
4.3 握手验证的实操
编译和配置都搞定后,验证是最后一道关。用openssl s_client连你的 Nginx:
/usr/local/openssl-gm/bin/openssl s_client \ -connect your.domain:443 \ -cipher ECDHE-SM2-WITH-SMS4-SM3 \ -tls1_2看输出里的Cipher字段,如果是国密套件,说明整条链路通了。如果显示handshake failure,按下面的顺序排查:
- 证书是不是 SM2 的?用
openssl x509 -in cert.pem -text看公钥算法。 - 套件名对不对?用
openssl ciphers确认。 - 引擎加载了吗?看 Nginx error log 有没有 engine 相关报错。
我实测下来,80% 的握手失败是证书类型不对,很多人拿 RSA 证书去配国密套件,必然失败。
5. 常见问题与排查技巧实录
5.1 编译期问题速查
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| configure 报找不到 openssl | 路径写错或源码目录不完整 | 确认--with-openssl指向含 config 脚本的目录 |
| make 报 SM2 符号未定义 | 国密算法没开启 | 检查--with-openssl-opt参数 |
| 编译通过但 nginx -V 显示系统 OpenSSL | 链接路径不对 | 检查--with-ld-opt的 rpath |
| 启动报 libssl 版本冲突 | 动态库路径混乱 | 用 ldd 看 nginx 实际链接的库 |
5.2 运行期问题排查
运行期最典型的问题是"握手成功但用的不是国密"。这种情况通常是客户端不支持国密套件,服务端回退到了普通套件。验证方法是在服务端配置里把普通套件全部去掉,只留国密套件,这样如果客户端不支持,握手会直接失败,而不是静默回退。
另一个坑是国密引擎的线程安全。有些国密引擎在多 worker 的 Nginx 下会出现随机崩溃,原因是引擎内部状态没做线程隔离。排查方法是把 worker_processes 设为 1,如果稳定了,基本就是这个问题。解决办法要么换引擎,要么在引擎初始化时加锁。
5.3 独家避坑经验
说几个文档里不会写、但实际会遇到的:
- 编译时间:国密 OpenSSL 全量编译比标准版慢不少,机器核少的话可能十几分钟,别以为卡死了。
- 证书链:国密证书的链验证和 RSA 不一样,中间证书的签名算法也必须是国密,否则链验证失败。
- 客户端兼容:不是所有客户端都支持国密 TLS,浏览器端尤其要注意,很多需要特定版本或插件。测试时优先用命令行工具,别一上来就用浏览器。
- 日志级别:排查握手问题时把 Nginx 的 error_log 调到 debug,能看到 OpenSSL 的详细握手过程,比猜快得多。
提示:每次改完 ssl 配置,记得
nginx -t先测语法,再 reload。国密配置写错时,reload 可能不报错但实际不生效,所以 reload 后一定要重新抓包或 s_client 验证。
6. 编译脚本化与后续维护
6.1 把流程固化成脚本
前面说过,静态编译的代价是升级要重编。所以我把整个流程写成了一个脚本,核心是把版本号和路径抽成变量:
#!/bin/bash set -e OPENSSL_VER="your-gm-version" NGINX_VER="your-nginx-version" OPENSSL_SRC="/opt/openssl-gm-${OPENSSL_VER}" NGINX_SRC="/opt/nginx-${NGINX_VER}" INSTALL_PREFIX="/usr/local/nginx-gm" # 编译 OpenSSL cd "${OPENSSL_SRC}" ./config --prefix=/usr/local/openssl-gm shared enable-sm2 enable-sm3 enable-sm4 make -j$(nproc) && make install # 编译 Nginx cd "${NGINX_SRC}" ./configure --prefix="${INSTALL_PREFIX}" \ --with-http_ssl_module \ --with-openssl="${OPENSSL_SRC}" \ --with-openssl-opt="enable-sm2 enable-sm3 enable-sm4" \ --with-cc-opt="-I/usr/local/openssl-gm/include" \ --with-ld-opt="-L/usr/local/openssl-gm/lib -Wl,-rpath,/usr/local/openssl-gm/lib" make -j$(nproc) && make install echo "build done"这样升级时只改版本号变量,重跑脚本即可。脚本里set -e很重要,任何一步失败就停,避免编出半成品。
6.2 升级时的注意事项
升级国密 OpenSSL 分支时,不要直接覆盖安装目录。我的做法是装到带版本号的目录,比如/usr/local/openssl-gm-1.1.1x,然后通过软链接切换。这样出问题能快速回滚。
Nginx 这边同理,新版本编到新 prefix,验证通过后再切流量。国密改造涉及 TLS 握手,一旦出问题影响面很大,灰度验证这一步不能省。
6.3 后续可以扩展的方向
编译配置搞定只是第一步。后面还可以做几件事:一是国密双证书配置,同时支持国密和普通证书,根据客户端能力自动选择;二是国密性能压测,SM2 的握手开销比 RSA 大,需要评估 QPS 影响;三是国密会话复用,减少重复握手开销。这些我在后续的实践里会陆续整理。
我个人在实际操作中的体会是,国密 Nginx 的编译配置难点不在命令本身,而在版本搭配和验证方法。命令网上到处都是,但版本不匹配、验证不到位,编出来的东西就是跑不起来。所以每次动手前,先把版本对应关系理清楚,编译后先验证算法开启,再验证握手,一层层来,比一股脑配完再排查要快得多。