ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Nginx国密改造实战:源码编译OpenSSL与双栈HTTPS配置

Nginx国密改造实战:源码编译OpenSSL与双栈HTTPS配置

1. 为什么要在编译层面折腾国密支持

很多人第一次接触国密改造,第一反应是找个现成的二进制包直接替换,或者装个模块了事。但真到了生产环境,尤其是金融、政务、能源这类对传输安全有硬性要求的场景,你会发现光靠动态加载模块往往不够——要么版本对不上,要么编译参数缺斤少两,要么运行起来握手直接失败。所以“编译配置”这四个字,才是国密改造里真正见功力的地方。

这篇是系列的第二篇,上一篇我们聊了国密算法的基础概念和整体改造思路,这一篇直接进入硬核环节:从源码编译入手,把 Nginx 改造成一个能同时跑国际算法和国密算法的双栈服务。说白了,就是让同一台服务器既能接普通浏览器的 HTTPS 请求,也能接国密客户端的加密连接,两边互不打架。

这篇文章适合谁看?如果你已经会基本的 Linux 编译操作,懂 Nginx 配置文件的大致结构,但一碰到国密就不知道从哪下手,那这篇就是写给你的。我会把每一步为什么这么做、参数怎么算、坑在哪里,全部摊开讲。全程不依赖任何特定厂商的闭源包,纯靠开源组件和标准编译流程走通。

先明确一个核心认知:Nginx 本身并不原生支持国密算法,它依赖 OpenSSL 提供的密码学能力。所以“编译 Nginx 支持国密”这件事,本质上拆成两步——第一步是编译一个支持国密的 OpenSSL,第二步是让 Nginx 链接这个 OpenSSL 重新编译。两步缺一不可,顺序也不能反。

注意:国密改造涉及密码学组件的替换,任何一步编译参数出错都可能导致握手失败或性能骤降。建议在测试环境完整走通后再上生产。

2. 编译前的整体设计与组件选型

2.1 为什么选源码编译而不是包管理安装

系统自带的 OpenSSL 和 Nginx 包,编译时用的都是默认参数,默认参数里不包含国密算法套件。你就算把配置文件写上天,底层没有对应的算法实现,照样握手失败。包管理安装的另一个问题是版本锁定——你想换 OpenSSL 版本,系统包管理器会告诉你依赖冲突,动不了。

源码编译的好处是彻底可控。OpenSSL 编译时可以指定启用国密算法相关的编译开关,Nginx 编译时可以指定链接我们自己编译出来的 OpenSSL 路径。整条链路从底层到上层都是自己掌控的,出了问题也好排查。

代价就是编译过程比较长,OpenSSL 全量编译大概十几分钟,Nginx 快一些,几分钟。但这是一次性的工作,编译好之后可以打包分发到同架构的其他机器上,不用每台都重新编。

2.2 组件版本选择与兼容性考量

版本选择这块我踩过不少坑,直接说结论。OpenSSL 建议选 1.1.1 系列的一个较新小版本,这个系列对国密算法的支持比较成熟,社区补丁也全。太老的版本缺少必要的算法接口,太新的 3.x 系列虽然也支持,但部分国密相关的编译开关和 API 有变动,和某些 Nginx 版本的配合需要额外打补丁,新手容易卡住。

Nginx 建议选 1.20 以上的稳定版。1.20 之后的版本对 TLS 1.3 和双证书配置的支持更完善,而国密双栈恰恰需要双证书机制——一张国际算法证书、一张国密证书,根据客户端支持的算法自动选择。

还有一个容易被忽略的组件是国密算法引擎或补丁。OpenSSL 官方源码里对 SM2、SM3、SM4 的支持是有的,但默认编译不一定全部启用。你需要确认编译配置里开启了对应的开关。有些场景还需要额外的国密引擎来提供硬件加速或特定实现,这个看具体需求。

组件推荐版本区间选择理由注意事项
OpenSSL1.1.1 系列较新小版本国密支持成熟,API 稳定避免过老版本缺接口
Nginx1.20 以上稳定版双证书和 TLS 1.3 支持完善确认与 OpenSSL 兼容
国密引擎按需硬件加速或特定实现非必需,视场景而定

2.3 双栈架构的核心思路

双栈的意思是同一套 Nginx 同时支持国际算法和国密算法的 TLS 握手。实现方式不是跑两个 Nginx 实例,而是在一个 server 块里配置两张证书,Nginx 根据客户端 ClientHello 里支持的算法套件自动选择用哪张证书完成握手。

这个机制的底层依赖是 OpenSSL 的多证书支持能力。当客户端发起连接时,ClientHello 里会带上自己支持的密码套件列表。如果列表里有国密套件,Nginx 就走国密证书和国密算法;如果只有国际套件,就走国际证书。整个过程对用户透明,不需要手动切换端口或域名。

这种设计的好处是迁移平滑。你不需要一次性把所有客户端都换成国密,可以国际和国密并行跑一段时间,逐步切换。对于面向公众的服务,这一点尤其重要,因为你不确定访问者用的是什么浏览器或客户端。

3. 编译 OpenSSL 的关键参数与实操

3.1 环境准备与依赖检查

动手之前先把编译工具链装齐。不同发行版的包名不一样,但核心就是编译器、make、perl 这几样。Perl 是必须的,OpenSSL 的编译配置脚本是 Perl 写的,没有它连 configure 都跑不起来。

# 以常见发行版为例,安装编译依赖 # 编译器与构建工具 gcc make perl # 如果系统里已有旧版 OpenSSL 开发包,建议先确认路径 which openssl openssl version

这里有个细节要注意:系统里通常已经装了一个 OpenSSL,编译新版本时不要覆盖系统自带的,否则可能影响系统里其他依赖 OpenSSL 的服务。正确做法是把新版本编译安装到一个独立目录,比如/usr/local/openssl-gm,然后通过编译参数让 Nginx 去链接这个目录。

提示:编译前用ldd --version确认一下系统的基础库版本,太老的系统可能需要先升级基础库,否则编译过程中会出现奇怪的链接错误。

3.2 configure 参数逐项拆解

OpenSSL 的 configure 参数决定了最终产物包含哪些能力。国密支持相关的关键参数需要重点关注。

./config \ --prefix=/usr/local/openssl-gm \ --openssldir=/usr/local/openssl-gm/ssl \ --shared \ enable-sm2 enable-sm3 enable-sm4 \ no-weak-ssl-ciphers

逐项解释一下。--prefix指定安装目录,独立目录避免污染系统。--openssldir指定配置文件目录。--shared生成动态链接库,Nginx 链接时用动态库比静态库灵活,后续升级 OpenSSL 不用重新编译 Nginx。

enable-sm2 enable-sm3 enable-sm4这三个是国密算法的核心开关。SM2 是非对称算法,用于证书和密钥交换;SM3 是哈希算法,用于摘要和签名;SM4 是对称算法,用于数据加密。三个都要开,缺一个国密套件就跑不起来。

no-weak-ssl-ciphers是安全加固选项,禁用已知的弱加密套件。这个不是国密必需的,但生产环境建议加上,减少攻击面。

参数写完之后执行 config 脚本,它会检查环境并生成 Makefile。如果这一步报错,通常是缺依赖或者参数拼写错误,仔细看报错信息,一般都能定位到具体原因。

3.3 编译安装与验证

configure 通过之后就是标准的编译安装流程。

make -j$(nproc) make install

-j$(nproc)是并行编译,用满 CPU 核心数加快速度。OpenSSL 编译比较吃 CPU,并行能省不少时间。编译过程中如果出现警告不用太紧张,只要不是 error 就继续。但如果出现 error,一定要停下来解决,不要强行跳过。

安装完成后验证一下国密算法是否可用。

/usr/local/openssl-gm/bin/openssl version /usr/local/openssl-gm/bin/openssl ecparam -list_curves | grep SM2

第一条命令看版本号,确认是新编译的版本。第二条命令列出支持的椭圆曲线,如果能看到 SM2,说明国密非对称算法已经编译进去了。再验证一下 SM3 和 SM4。

/usr/local/openssl-gm/bin/openssl dgst -sm3 /dev/null /usr/local/openssl-gm/bin/openssl enc -sm4-cbc -help

SM3 那条命令会对空输入做一次摘要,能输出结果就说明算法可用。SM4 那条会打印帮助信息,能看到 sm4-cbc 选项就说明对称算法也正常。

注意:验证时一定要用新编译的 openssl 完整路径,不要直接敲openssl,因为系统 PATH 里可能还是旧版本,验证结果会误导你。

4. 编译 Nginx 并链接国密 OpenSSL

4.1 Nginx 编译参数配置

Nginx 的编译配置比 OpenSSL 简单一些,核心就是告诉它去哪里找我们刚编译好的 OpenSSL。

./configure \ --prefix=/usr/local/nginx-gm \ --with-http_ssl_module \ --with-http_v2_module \ --with-openssl=/path/to/openssl-source \ --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"

这里有几个关键点。--with-http_ssl_module是必须的,没有它 Nginx 根本不支持 HTTPS。--with-http_v2_module建议加上,HTTP/2 在现代服务里基本是标配。

--with-openssl指向 OpenSSL 源码目录,注意是源码目录不是安装目录。Nginx 编译时会自己再去编译一遍 OpenSSL,所以需要源码。--with-openssl-opt把国密开关传给 OpenSSL 的编译过程,确保 Nginx 链接的那个 OpenSSL 也带国密能力。

--with-cc-opt和--with-ld-opt分别指定头文件路径和库文件路径。-Wl,-rpath这个参数很重要,它把库路径写进可执行文件的运行时搜索路径,这样 Nginx 启动时才能找到我们自定义目录下的 OpenSSL 库,而不是系统默认的。

4.2 编译过程中的常见报错处理

编译 Nginx 时最常见的报错是找不到 OpenSSL 头文件或者链接不到库。如果报openssl/ssl.h: No such file or directory,说明--with-cc-opt里的 include 路径不对,检查一下/usr/local/openssl-gm/include下是不是真的有 ssl.h。

如果报链接错误,类似undefined reference to SSL_CTX_set_cipher_list,通常是--with-ld-opt的库路径不对,或者库文件没生成。回去检查 OpenSSL 编译安装那一步是否成功。

还有一种情况是编译能过,但 Nginx 启动时报error while loading shared libraries。这就是 rpath 没设对,Nginx 运行时找不到动态库。解决办法要么重新编译加上正确的 rpath,要么把库路径加到系统库配置里。

# 临时验证可以用这个方式指定库路径 LD_LIBRARY_PATH=/usr/local/openssl-gm/lib /usr/local/nginx-gm/sbin/nginx -t

但生产环境不建议依赖环境变量,还是编译时把 rpath 写死更可靠。

4.3 验证 Nginx 的国密能力

编译安装完成后,先别急着配证书,先确认 Nginx 链接的 OpenSSL 版本对不对。

/usr/local/nginx-gm/sbin/nginx -V

输出里会显示编译参数和 OpenSSL 版本。确认版本号和你编译的一致,并且编译参数里能看到国密相关的选项。

再进一步验证,可以用 Nginx 自带的测试命令配合国密套件。

/usr/local/openssl-gm/bin/openssl ciphers -v | grep -i sm

这条命令列出所有包含 SM 的密码套件。如果能看到类似ECDHE-SM2-SM4-CBC-SM3这样的套件,说明整条链路都通了。这个套件的含义是:密钥交换用 SM2,数据加密用 SM4-CBC,完整性校验用 SM3。这是国密 TLS 的典型套件组合。

5. 双栈证书配置与握手流程解析

5.1 证书准备与格式要求

国密证书和国际证书在格式上有区别。国际证书通常是 RSA 或 ECC 密钥,国密证书用的是 SM2 密钥。生成国密证书需要用到支持 SM2 的工具链。

# 生成 SM2 私钥 /usr/local/openssl-gm/bin/openssl ecparam -genkey -name SM2 -out sm2.key # 生成证书签名请求 /usr/local/openssl-gm/bin/openssl req -new -key sm2.key -out sm2.csr -sm3 # 自签名生成证书(测试用,生产环境应走正规 CA) /usr/local/openssl-gm/bin/openssl x509 -req -in sm2.csr -signkey sm2.key -out sm2.crt -sm3 -days 365

注意签名时指定-sm3,表示用 SM3 做摘要。生产环境的国密证书需要向支持国密的 CA 机构申请,自签名证书只适合测试。

国际证书的生成流程不变,用常规的 RSA 或 ECC 流程即可。两张证书准备好之后,放到 Nginx 能读取的目录,权限设置成只有 Nginx 工作进程用户可读。

5.2 Nginx 双证书配置写法

双栈配置的核心是在 server 块里同时指定两张证书。Nginx 从某个版本开始支持ssl_certificate指令出现多次,分别对应不同的算法类型。

server { listen 443 ssl; server_name example.local; # 国际算法证书 ssl_certificate /etc/nginx/certs/intl.crt; ssl_certificate_key /etc/nginx/certs/intl.key; # 国密算法证书 ssl_certificate /etc/nginx/certs/sm2.crt; ssl_certificate_key /etc/nginx/certs/sm2.key; # 密码套件配置,同时包含国际和国密套件 ssl_ciphers 'ECDHE-SM2-SM4-CBC-SM3:ECDHE-SM2-SM4-GCM-SM3:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384'; ssl_prefer_server_ciphers on; ssl_protocols TLSv1.2 TLSv1.3; location / { root /usr/share/nginx/html; index index.html; } }

ssl_ciphers里把国密套件放在前面,国际套件放在后面。ssl_prefer_server_ciphers on表示优先使用服务端指定的顺序。这样配置之后,支持国密的客户端会优先协商到国密套件,不支持的客户端会自动落到国际套件。

提示:国密套件的名称在不同 OpenSSL 版本里可能有细微差异,配置前先用openssl ciphers -v确认实际支持的套件名称,直接复制命令输出里的名称最稳妥。

5.3 握手过程与算法协商机制

客户端发起连接时,ClientHello 里带着自己支持的密码套件列表。Nginx 收到后,结合服务端配置的套件列表,选出一个双方都支持的最优套件。

如果客户端支持国密套件,且服务端配置了国密证书,协商结果就是国密套件,后续的密钥交换、数据加密、摘要校验全部走国密算法。如果客户端不支持国密,协商结果就是国际套件,走常规的 RSA 或 ECC 流程。

这个协商过程完全由 TLS 协议层自动完成,应用层无感知。你可以用抓包工具看握手过程,ClientHello 和 ServerHello 里的 CipherSuite 字段能清楚看到最终选了什么套件。

有一个细节值得注意:国密 TLS 的握手流程和国际 TLS 略有不同。国密标准里定义了双证书体系,签名证书和加密证书是分开的。但在实际部署中,很多场景用的是单证书简化模式。具体用哪种模式,取决于你的客户端要求和服务端实现。这一点在配置前要和客户端侧确认清楚,否则可能出现服务端配好了但客户端不认的情况。

6. 常见问题排查与性能调优实录

6.1 握手失败的典型原因与定位方法

握手失败是国密改造里最常见的问题,表现是客户端连不上或者连接被重置。排查的第一步是看 Nginx 错误日志。

tail -f /usr/local/nginx-gm/logs/error.log

如果日志里出现no shared cipher,说明客户端和服务端没有共同支持的密码套件。原因可能是服务端没配国密套件,或者客户端不支持国密,或者套件名称写错了。

如果出现certificate verify failed,说明证书验证没过。可能是证书链不完整,或者客户端不信任服务端的 CA。国密证书的信任链和国际证书是两套体系,客户端需要预置对应的根证书。

如果出现unsupported protocol,检查ssl_protocols配置,确认协议版本双方都支持。

用 openssl 命令行工具可以模拟握手,快速定位问题。

# 模拟国密握手 /usr/local/openssl-gm/bin/openssl s_client -connect example.local:443 -cipher 'ECDHE-SM2-SM4-CBC-SM3' # 模拟国际握手 openssl s_client -connect example.local:443

第一条命令如果握手成功,会打印出证书信息和协商的套件。失败的话会给出具体错误,根据错误信息对症下药。

6.2 性能影响评估与优化建议

国密算法的性能和国际算法有差异。SM2 在密钥交换阶段的计算量比 RSA 小,但比 ECC 略大。SM4 的加解密速度和 AES 接近,在支持硬件加速的平台上差距更小。SM3 的哈希速度和 SHA256 相当。

整体来看,国密 TLS 的性能损耗在可接受范围内,但有几个优化点值得注意。

第一,开启会话复用。TLS 握手是性能开销的大头,会话复用能避免重复的完整握手。配置ssl_session_cache和ssl_session_timeout即可。

ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m;

第二,如果平台支持国密硬件加速,编译 OpenSSL 时启用对应的引擎。硬件加速能把 SM2 和 SM4 的计算卸载到专用硬件,性能提升明显。但硬件加速依赖特定平台,通用服务器上不一定有。

第三,合理设置ssl_buffer_size。默认值对大多数场景够用,但如果传输大量小文件,适当调小可以减少延迟。

优化项配置指令预期效果适用场景
会话复用ssl_session_cache减少完整握手次数所有场景
硬件加速编译时启用引擎提升国密计算速度支持加速的平台
缓冲区调优ssl_buffer_size降低小文件延迟小文件传输多

6.3 独家避坑经验汇总

说几个我在实际操作中踩过的坑,都是文档里不会写的。

第一个坑是 OpenSSL 编译时的enable-sm2开关。有些小版本的 OpenSSL 里,SM2 的支持是默认开启的,不需要显式指定。但如果你显式指定了而该版本又不认这个参数,configure 会直接报错。解决办法是先跑一次./config --help看看支持哪些参数,按实际支持的来写。

第二个坑是 Nginx 的--with-openssl参数。这个参数指向的是 OpenSSL 源码目录,Nginx 编译时会自己再编译一遍 OpenSSL。但如果你之前已经单独编译安装过 OpenSSL,Nginx 编译时可能会混淆,用了错误的版本。稳妥的做法是 Nginx 编译时用一个干净的 OpenSSL 源码副本,不要和之前编译过的目录混用。

第三个坑是证书链的顺序。国密证书的证书链里,根证书和中间证书的顺序有讲究。顺序错了,某些客户端会验证失败。一般来说,服务端证书在前,中间证书在后,根证书通常不需要放在服务端。具体顺序参考 CA 机构提供的说明。

第四个坑是双栈配置下的套件优先级。如果你把国际套件放在国密套件前面,支持国密的客户端也会优先协商到国际套件,国密能力就浪费了。所以国密套件一定要放在前面,配合ssl_prefer_server_ciphers on使用。

第五个坑是日志级别。排查握手问题时,把 Nginx 日志级别调到info或debug,能看到详细的握手过程。但生产环境不要长期开 debug,日志量太大,影响性能。排查完记得调回去。

error_log /usr/local/nginx-gm/logs/error.log info;

这个配置在排查阶段很有用,能看到每一步握手协商的细节。调完之后改回error或warn级别。

7. 编译产物的打包与迁移部署

7.1 制作可分发的编译产物

编译一次不容易,如果有多台同架构的机器要部署,没必要每台都重新编译。把编译产物打包,直接分发过去就行。

需要打包的内容包括:OpenSSL 安装目录、Nginx 安装目录、以及必要的配置文件模板。打包前先确认目标机器的架构和基础库版本一致,否则动态库可能不兼容。

# 打包 OpenSSL 和 Nginx 安装目录 tar czf nginx-gm-package.tar.gz /usr/local/openssl-gm /usr/local/nginx-gm

分发到目标机器后解压到相同路径,然后验证动态库依赖是否满足。

ldd /usr/local/nginx-gm/sbin/nginx | grep "not found"

如果有not found的库,说明目标机器缺依赖,需要先补上。常见的是 libpcre、zlib 这些基础库。

7.2 迁移后的配置调整与验证

迁移到新机器后,证书路径、日志路径、用户权限这些可能需要调整。检查 Nginx 配置文件里的路径是否和新环境一致。

启动前先用nginx -t测试配置语法。

/usr/local/nginx-gm/sbin/nginx -t

测试通过后再启动。启动后立即验证国密握手是否正常,用前面提到的 openssl s_client 命令测一遍。确认无误后再把流量切过来。

注意:迁移部署时最容易忽略的是文件权限。Nginx 工作进程通常以非 root 用户运行,如果证书文件权限设置过严,工作进程读不到证书,握手会失败。确保证书文件对 Nginx 工作进程用户可读。

7.3 版本升级与回滚策略

国密组件后续可能需要升级,比如 OpenSSL 出了安全补丁,或者 Nginx 有新版本。升级策略建议是先在测试环境验证,确认没问题再上生产。

回滚方案也要提前准备好。最简单的回滚就是保留旧版本的安装目录,出问题时把软链接或启动脚本指回旧版本。所以升级时不要直接覆盖旧目录,而是新装一个目录,通过切换启动路径来实现版本切换。

# 假设旧版本在 /usr/local/nginx-gm-old # 新版本在 /usr/local/nginx-gm-new # 通过软链接切换 ln -sfn /usr/local/nginx-gm-new /usr/local/nginx-gm

这样切换和回滚都只是一条命令的事,风险可控。

我个人在实际操作中的体会是,国密改造这件事,编译环节虽然繁琐,但恰恰是最值得投入时间的地方。把编译参数吃透,把依赖关系理顺,后面配置和运维阶段会省很多事。反过来,如果编译阶段图省事用了来路不明的二进制包,后面出问题排查起来会非常痛苦,因为你根本不知道那个包是用什么参数编出来的。所以宁可多花半小时自己编译,也不要在这件事上走捷径。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进