
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-optenable-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-gmNginx 源码在/opt/nginx-src。第一步编译国密 OpenSSLcd /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这类参数按你的分支实际支持情况调整。第二步编译 Nginxcd /opt/nginx-src ./configure \ --prefix/usr/local/nginx-gm \ --with-http_ssl_module \ --with-openssl/opt/openssl-gm \ --with-openssl-optenable-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_VERyour-gm-version NGINX_VERyour-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-optenable-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 的编译配置难点不在命令本身而在版本搭配和验证方法。命令网上到处都是但版本不匹配、验证不到位编出来的东西就是跑不起来。所以每次动手前先把版本对应关系理清楚编译后先验证算法开启再验证握手一层层来比一股脑配完再排查要快得多。