
最近几年做政企项目、金融类系统的朋友基本都会碰到同一个需求客户要求网站全链路支持国密算法浏览器访问不再走传统的RSA体系而是用SM2做密钥协商、SM3做摘要、SM4做数据加密。这时候你的第一反应大概率是“把nginx的ssl证书换成国密证书”但真动手就会发现普通nginx根本不认SM2证书直接报unsupported protocol或证书解析失败。原因很简单nginx的SSL能力全部来自OpenSSL而社区版OpenSSL至今没有国密套件的原生实现你能做的就是换一套带国密能力的底层库重新编译nginx。这篇文章我从头到尾梳理一遍“国密nginx服务搭建”的完整链路包括算法背景、工具链选型、GmSSL安装、自建CA签发SM2证书、打补丁编译nginx、配置双证书、常见坑位排查全部是我自己实测过的方案。适合两类人看一类是刚接国密改造需求、连SM2和RSA的区别都还没搞清的运维同学另一类是已经编译过nginx、但卡在证书或兼容性环节的进阶选手。文章里的命令都能直接抄编译参数我也会逐个解释为什么这么写。1. 国密nginx到底是怎么一回事1.1 国密算法三件套SM2、SM3、SM4国密不是某一个算法而是一整套商用密码算法体系常用的是三个SM2是非对称算法负责密钥协商和数字签名相当于RSA或ECC的角色SM3是摘要算法输出256位哈希值对标SHA-256SM4是分组对称加密算法分组长度128位对标AES。它们之间是协同工作的HTTPS握手阶段用SM2协商出会话密钥握手完成后用SM4加密业务数据中间所有需要校验完整性的地方用SM3做摘要。和RSA体系相比SM2最明显的区别有两个。第一SM2基于椭圆曲线256位密钥长度就能达到128位对称密钥的安全强度而RSA通常要2048位以上所以SM2的密钥生成、签名验签速度更快证书体积也更小。第二SM2的签名算法是SM2-with-SM3证书里的Signature Algorithm字段写的是SM2-with-SM3而RSA证书写的是sha256WithRSAEncryption这个字段直接决定了普通OpenSSL能否解析这张证书。1.2 普通nginx为什么不能直接支持国密nginx本身并不实现SSL协议它把SSL/TLS层的所有工作都委托给了libssl和libcrypto。判断一台nginx能不能用国密证书看的不是nginx版本而是它编译时链接的OpenSSL是否支持国密套件。标准OpenSSL从1.1.1到3.x都只包含RSA、ECDSA、AES等国际算法SM2/SM3/SM4相关接口虽然在1.1.1之后以“不可用于TLS”的形式存在但套件表里根本没有ECC-SM2-SM4-CBC-SM3这种东西。所以在普通nginx上配置国密证书你会在日志里看到类似ngx_ssl_handshake_handler: SSL_do_handshake() failed或者no suitable key share之类的错误。本质上是握手阶段客户端发来的cipher suite列表里没有任何一个服务端能匹配的套件属于套件协商失败而不是证书格式问题。明白了这一层你就知道正确思路只有两条要么给nginx换一个支持国密的TLS库要么在nginx前面再加一层国密网关做协议转换。1.3 当前主流改造方案怎么选目前业内做国密nginx改造主流方案有三类。第一类是直接用现成的国密SSL库替换OpenSSL比如GmSSLOpenSSL的国密分支、铜锁Tongsuo原BabaSSL的升级版、以及一些企业自研的国密SDK然后重新编译nginx挂上这些库。第二类是给nginx打第三方补丁最典型的是磐云科技那套nginx-gmssl补丁但这套补丁比较老对新版nginx兼容性一般。第三类是完全不加nginx用专门的国密负载均衡网关比如部分国产硬件WAF和SLB但这种通常价格不低而且运维链路又多了一层。我自己的选择是第一类具体是用GmSSL作为nginx的SSL底层。原因很直接GmSSL是国内开源社区维护最活跃的国密实现接口兼容OpenSSL编译nginx时只需要--with-openssl指向GmSSL源码目录不用改nginx本体代码后续升级nginx版本也只是重新编译一次而已。铜锁Tongsuo我也试过它的API更现代对TLS1.3的部分扩展支持比GmSSL更好但如果你只是要“国内浏览器国密证书能正常访问”GmSSL的方案成熟案例更多网上踩坑记录也全出问题好查。2. 动手前的环境准备与工具链2.1 编译环境的硬性要求国密nginx的编译过程本质上和普通nginx编译一致但有一个额外门槛你的编译环境里必须能构建GmSSL而GmSSL依赖CMake。所以操作系统层面我建议用一个干净的环境推荐AlmaLinux 9、Rocky Linux 9或者Ubuntu 22.04 LTS。如果你用的是CentOS 7这种比较老的系统GmSSL 3.x系列编译会比较折腾glibc版本太低会碰到一堆兼容性问题。基础依赖按下面准备# Debian/Ubuntu系 apt update apt install -y build-essential libpcre3-dev libssl-dev zlib1g-dev cmake gcc g make wget # RHEL/AlmaLinux/Rocky系 dnf install -y gcc gcc-c make cmake pcre-devel zlib-devel openssl-devel wget需要注意系统自带的openssl-devel必须装上虽然最终nginx不链接系统OpenSSL但编译过程中有些脚本工具会用到系统openssl命令比如生成证书请求、校验套件列表。另外libpcre是nginx重定向和rewrite模块的依赖zlib是gzip压缩模块的依赖如果你不需要这些功能可以不装但既然都编译了建议一步到位。2.2 GmSSL与系统OpenSSL怎么共存这一步是绝大多数人第一次踩坑的地方。系统里已经装了OpenSSL再装一个GmSSL两者会不会冲突答案是如果你把GmSSL源码编译出来的静态库直接链接进nginx就不冲突如果你make install到系统目录就会覆盖系统的libssl.so搞不好把系统的openssl命令也替换掉这是非常危险的。我的做法是GmSSL只编译不安装。说白了一句我们需要的是GmSSL源码目录下编译出来的libssl.a和libcrypto.a这两个静态库nginx的configure脚本会通过--with-openssl参数找到它们然后静态链接进nginx二进制文件。这样nginx运行时完全独立不依赖系统的动态库也不影响系统里原有的OpenSSL。具体操作先在/opt下建一个工作目录把GmSSL源码放进去mkdir -p /opt/gmssl-build cd /opt/gmssl-build wget https://github.com/guanzhi/GmSSL/archive/refs/tags/v3.1.1.tar.gz tar -xzf v3.1.1.tar.gz cd GmSSL-3.1.1 mkdir build cd build cmake .. make -j$(nproc)编译完成之后GmSSL源码目录下的lib目录里会生成一堆静态库文件包括libssl.a和libcrypto.a。此时你不需要执行make install记下GmSSL源码根目录的路径后面编译nginx时要引用它。2.3 目录规划与文件备份策略国密nginx的搭建我建议从一开始就规划好目录别把证书和配置散落各处。推荐的结构如下/etc/nginx/ ├── nginx.conf ├── conf.d/ │ └── sm2-ssl.conf └── ssl/ ├── sm2/ │ ├── ca.crt │ ├── server.crt │ └── server.key └── rsa/ ├── rsa.crt └── rsa.key如果你的生产环境已经有正在跑的nginx千万别在原机直接覆盖。正确流程是先在一台测试机或容器里编译好新nginx验证通过后再在现网机器上备份原nginx二进制和配置然后替换。我用的是最土但最稳妥的办法cp /usr/sbin/nginx /usr/sbin/nginx.bak.$(date %Y%m%d)配置文件也整体备份一遍。国密改造这种事出问题回滚比修复快。3. 国密证书的申请、签发与自建CA3.1 自建CA与签发国密证书的整体流程国密HTTPS和普通HTTPS一样也分单向认证和双向认证。网站对外提供服务通常用单向认证就够了也就是客户端验证服务端证书如果涉及银行、政务类高安全场景还需要客户端提供国密证书做双向认证。无论哪种前提都是你要有一张SM2证书。证书来源有两种一是向有资质的CA机构申请比如国内几家支持国密证书的CA他们会给你签发正式的SM2证书二是自建CA自己给自己签适合测试环境、内网环境、或者你只是先验证技术可行性。自建CA的完整流程分四步生成根CA密钥对、生成根CA自签名证书、生成服务端密钥对和证书请求、用根CA签发服务端证书。下面我给出完整命令。3.2 使用GmSSL生成密钥与签发证书GmSSL的gmssl命令兼容OpenSSL命令语法但用SM2算法时部分子命令不同。第一步生成根CA的SM2私钥/opt/gmssl-build/GmSSL-3.1.1/build/bin/gmssl ecparam -genkey -name sm2p256v1 -out /etc/nginx/ssl/sm2/ca.keysm2p256v1是国密标准定义的椭圆曲线参数相当于RSA里的2048位密钥长度这个值固定写死就行不需要改。第二步生成根CA自签名证书/opt/gmssl-build/GmSSL-3.1.1/build/bin/gmssl req -new -x509 -key /etc/nginx/ssl/sm2/ca.key -out /etc/nginx/ssl/sm2/ca.crt -days 3650 -subj /CCN/OMy Company/CNMy SM2 Root CA这里注意GmSSL 3.x默认在x509子命令里支持-sm2参数指定SM2签名算法但在req自签场景下部分版本会自动识别密钥类型并选择SM2-with-SM3。如果你用的版本行为不一致可以用gmssl x509 -req单独签发时显式加-sm2参数。签完之后用gmssl x509 -in ca.crt -text -noout看一下Signature Algorithm字段确认是SM2-with-SM3就对了。第三步生成服务端SM2私钥和证书请求/opt/gmssl-build/GmSSL-3.1.1/build/bin/gmssl ecparam -genkey -name sm2p256v1 -out /etc/nginx/ssl/sm2/server.key /opt/gmssl-build/GmSSL-3.1.1/build/bin/gmssl req -new -key /etc/nginx/ssl/sm2/server.key -out /etc/nginx/ssl/sm2/server.csr -subj /CCN/OMy Company/CNsm2.example.com第四步用根CA签发服务端证书。这里附带生成一个序列号文件-sm2参数指定使用SM2签名算法/opt/gmssl-build/GmSSL-3.1.1/build/bin/gmssl x509 -req -in /etc/nginx/ssl/sm2/server.csr \ -CA /etc/nginx/ssl/sm2/ca.crt -CAkey /etc/nginx/ssl/sm2/ca.key \ -CAcreateserial -out /etc/nginx/ssl/sm2/server.crt -days 825 -sm2提示证书有效期不要贪长。虽然技术上空闲设置3650天也合法但很多国密浏览器和审计系统会对超长有效期证书有兼容性或合规性提示生产环境建议825天约两年多比较合适。签完后同样检查一下证书的签名算法以及证书里的公钥信息。gmssl x509 -in server.crt -text -noout | grep Public Key Algorithm应该输出id-ecPublicKeySignature Algorithm字段应显示SM2-with-SM3。3.3 双向认证的客户端证书怎么处理如果你的业务场景需要双向认证你需要再签一张客户端证书。签发方式和服务端证书几乎一样只是CN字段写成用户或终端名称然后在nginx配置里把客户端证书的根CA配置为ssl_client_certificate指向上面的ca.crt并开启ssl_verify_client on。有一个容易忽略的细节很多单位的国密浏览器在双向认证场景下要求客户端证书必须由“客户能识别的CA”签发如果你的自建CA不在浏览器信任列表里浏览器会先弹出不安全提示。测试环境无所谓生产环境务必走正规CA签发的客户端证书否则不仅用户手机上装证书麻烦合规审计也可能是硬伤。4. 编译安装国密nginx全过程4.1 源码版本与补丁选择到这一步你已经有了支持SM2的底层库GmSSL源码目录现在要解决的是nginx这边选哪个版本。我的判断是不要追最新的大版本也不要选太老的版本。nginx 1.24.x、1.26.x这些稳定版都验证过可以顺畅地和GmSSL组合编译。太老的1.16、1.18虽然理论上也能编译但漏洞修复跟不上尤其是之前那个CVE-2022-41742缓冲区溢出漏洞低版本直接有风险太新的1.27在configure阶段对OpenSSL版本有额外检查有时候需要改代码才能过没必要。我这里以nginx 1.26.2为例cd /opt/gmssl-build wget http://nginx.org/download/nginx-1.26.2.tar.gz tar -xzf nginx-1.26.2.tar.gz cd nginx-1.26.2如果你的电脑访问nginx官网速度不理想可以用国内镜像源下载nginx的官方源码在不少高校镜像站有同步比如清华、阿里云镜像文件名一样校验一下SHA256再使用。4.2 configure参数逐项解读这是全篇最核心的操作。nginx的configure脚本会把--with-openssl指向的源码目录里的OpenSSL兼容库编译并静态链接进来。下面是我验证过的完整参数./configure \ --prefix/usr/local/nginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_stub_status_module \ --with-http_realip_module \ --with-stream \ --with-stream_ssl_module \ --with-pcre \ --with-openssl/opt/gmssl-build/GmSSL-3.1.1 \ --with-openssl-optenable-ec_nistp_64_gcc_128参数逐一解释一下。--with-http_ssl_module是必须的它决定nginx支持HTTPS。--with-http_v2_module启用HTTP/2但注意国密套件目前无法跑在TLS1.3和HTTP/3上HTTP/2在TLS1.2下仍然可用所以没问题。--with-stream和--with-stream_ssl_module是为了做TCP/UDP层反向代理时的SSL终结如果你的国密nginx还要代理数据库、Redis这些四层流量就加上不需要可以去掉。--with-openssl指向GmSSL源码根目录这是整个编译的关键。--with-openssl-opt给OpenSSL库本身传编译选项enable-ec_nistp_64_gcc_128可以让SM2椭圆曲线运算在64位CPU上使用优化实现性能更好。还有一个可选项如果你要用国密算法做“国密客户端证书认证”需要确保nginx在编译时包含了--with-http_ssl_module即可客户端证书验证是OpenSSL层的功能不需要额外的nginx模块。4.3 编译、安装与验证configure没问题之后直接编译make -j$(nproc) make install编译过程一般需要5到10分钟如果报错90%的情况是GmSSL本身没编好最常见的是libcrypto.a找不到或者符号缺失。这时候回到GmSSL的build目录重新make一次确保静态库完整生成再回来编译nginx。安装完成后验证一下nginx链接的SSL库/usr/local/nginx/sbin/nginx -V输出里应该能看到built with OpenSSL字样后面跟着的版本号会是GmSSL的版本标识。然后你可以用ldd看一下nginx的动态库依赖如果依赖列表里没有出现libssl.so和libcrypto.so说明静态编译成功nginx完全自包含这是最理想的状态。如果出现了这两个动态库说明configure没有正确静态链接运行时容易受系统库污染建议回头检查GmSSL编译产物。接着用GmSSL自带的gmssl s_client做一次本机握手测试确认套件能被正确协商/opt/gmssl-build/GmSSL-3.1.1/build/bin/gmssl s_client \ -connect 127.0.0.1:443 \ -servername sm2.example.com \ -cipher ECC-SM2-SM4-CBC-SM3 \ -tls1_2如果显示握手成功并输出会话密钥信息说明nginx底层已经具备国密能力接下来就只剩配置的事了。5. 配置文件与双证书部署5.1 国密HTTPS的基础配置从server块到全局参数先给一个最基础的国密HTTPS server块。配置文件和普通HTTPS的差异主要在ssl_certificate所指向的证书是SM2证书以及cipher suite必须显式指定国密套件server { listen 443 ssl; server_name sm2.example.com; ssl_certificate /etc/nginx/ssl/sm2/server.crt; ssl_certificate_key /etc/nginx/ssl/sm2/server.key; ssl_protocols TLSv1.2; ssl_ciphers ECC-SM2-SM4-CBC-SM3:ECDHE-SM2-SM4-CBC-SM3:ECDHE-SM2-SM4-GCM-SM3; ssl_prefer_server_ciphers on; root /var/www/html; index index.html index.htm; }有两个点必须强调。第一ssl_protocols里不要加TLSv1.3国密套件目前不支持TLS1.3加上去会导致不同浏览器行为不一致有的浏览器尝试TLS1.3握手失败后不会自动降级直接连接失败。第二ssl_ciphers的套件名要以GmSSL实际编译输出的套件列表为准有的版本套件名是ECC-SM2-SM4-CBC-SM3有的还会支持ECDHE-SM2-SM4-GCM-SM3你可以用gmssl ciphers -V列出所有可用套件再把对应的名字填进去。5.2 RSA与SM2双证书共存让普通浏览器也不掉线现实中的难题是不是所有浏览器都支持国密。Chrome、Firefox、Safari这些国际主流浏览器不支持国密套件Windows上的IE/Edge也默认走RSA体系。如果你的站点同时要支持国密浏览器比如奇安信、360安全浏览器、红莲花这些和普通浏览器方案就是要做双证书共存。nginx 1.25.1及以上版本原生支持配置不同类型的证书方式是在同一个server块里写两条证书指令server { listen 443 ssl; server_name sm2.example.com; # SM2国密证书 ssl_certificate /etc/nginx/ssl/sm2/server.crt; ssl_certificate_key /etc/nginx/ssl/sm2/server.key; # RSA证书普通浏览器用 ssl_certificate /etc/nginx/ssl/rsa/rsa.crt; ssl_certificate_key /etc/nginx/ssl/rsa/rsa.key; ssl_protocols TLSv1.2; ssl_ciphers ECC-SM2-SM4-CBC-SM3:ECDHE-SM2-SM4-CBC-SM3:ECDHE-SM2-SM4-GCM-SM3:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:AES128-GCM-SHA256; ssl_prefer_server_ciphers off; }这里的原理是nginx在TLS握手时根据客户端发来的cipher suite列表自动选择匹配的证书类型。如果客户端是国密浏览器它发来的套件里只有SM2系列nginx就选用SM2证书如果客户端是Chrome它发来的套件里是标准RSA套件nginx就选用RSA证书。ssl_prefer_server_ciphers我建议设为off让客户端偏好起更大作用这样两类浏览器都能走自己最顺的套件。这个特性如果你手里的nginx版本低于1.25.1会报“duplicate ssl_certificate”错误解决方案要么升级nginx版本要么把两套证书合并成一个文件再配合ssl_certificate、ssl_certificate_key的多证书链语法但后者要复杂得多直接升级版本更省事。5.3 反向代理场景下的国密透传与终结很多国密改造不只是单个站点而是nginx做反向代理后端挂了一堆Tomcat、Spring Boot应用。这里有两种模式。第一种是“国密终结在nginx”即客户端到nginx这一段走国密HTTPSnginx到后端走普通HTTP或RSA HTTPS。这种模式最简单改动最小后端服务完全不需要感知国密算法你只需要在location里配proxy_pass http://127.0.0.1:8080;然后在server块上开启国密SSL即可。第二种是“国密全链路透传”也就是nginx不结束SSL而是把客户端的国密TLS连接原封不动转发给后端。这种情况下nginx要配置stream模块做四层转发stream { server { listen 443; proxy_pass 192.168.1.10:443; } }四层转发性能好、透明但代价是无法在nginx层做域名路由、无法进行HTTP层的高级处理。如果后端有多个Tomcat按域名或URL路径分流就行不通了只能在四层按源IP或端口分流。我实际项目中的建议是能用第一种就用第一种后端接入成本为零大多数业务系统的改造需求都是这么落地的。全链路国密一般是强合规要求才做而且后端也要换成支持国密的Web容器成本会翻好几倍。6. 常见问题、隐患排查与性能实测6.1 从openssl s_client到浏览器全链路验证顺序配置写完不是结束验证链要一级一级来。我习惯的顺序是先验证证书链再验证套件协商最后验证浏览器效果。证书链验证用这条命令/opt/gmssl-build/GmSSL-3.1.1/build/bin/gmssl verify -CAfile /etc/nginx/ssl/sm2/ca.crt /etc/nginx/ssl/sm2/server.crt如果输出OK说明服务端证书和根CA之间的信任关系没问题。然后测套件协商用gmssl s_client连接本机nginx端口带国密套件看Handshake是否成功。最后用国密浏览器访问https://你的域名按F12看连接详情里的Cipher Suite字段确认是ECC-SM2-SM4-CBC-SM3、ECDHE-SM2-SM4-CBC-SM3之类的国密套件才算真正跑通。提示测试时不要用curl做最终验证。系统自带的curl链接的系统OpenSSL不支持国密套件连接时会直接报tls_process_server_certificate: certificate verify failed或者套件协商失败这并不能说明你的nginx有问题。要用就用GmSSL自带的gmssl s_client或者国密浏览器。6.2 高频排障速查表从报错到解决方案国密nginx这摊事踩坑点相对集中。我把实操过程中遇到的、以及同行群里被反复问到的典型问题整理成一张速查表现象直接原因解决方案gmssl s_client握手失败报no cipher matchGmSSL库没编译出预期套件用gmssl ciphers -v查可用套件确认编译时是否开了国密扩展浏览器提示证书不受信任自建CA未导入浏览器信任库把ca.crt导入操作系统的受信任根证书颁发机构nginx启动报duplicate ssl_certificatenginx版本低于1.25.1不支持多证书升级nginx版本或改证书链合并方案普通浏览器能访问国密浏览器打不开cipher suite顺序问题或缺少SM2证书链检查ssl_ciphers里是否包含国密套件且ssl_certificate指向SM2证书国密浏览器能访问Chrome打不开没有配置RSA证书补上RSA证书的双证书配置编译nginx时找不到libcrypto.aGmSSL的build目录未生成静态库回到GmSSL源码目录重新make确认libcrypto.a位于build/lib重启nginx后配置加载失败报无效参数ssl_protocols里带了TLSv1.3删掉TLSv1.3只保留TLSv1.2这些坑里最阴间的其实是“证书链不完整”的问题。我遇到过签发的server.crt只包含服务端证书没带CA证书结果在国密浏览器上反复握手失败但命令行s_client却正常。原因是部分国密浏览器在TLS握手阶段要求服务端下发完整的证书链而命令行工具对链完整性要求不高。解决办法是把服务端证书和CA证书拼在一起作为ssl_certificate文件cat /etc/nginx/ssl/sm2/server.crt /etc/nginx/ssl/sm2/ca.crt /etc/nginx/ssl/sm2/fullchain.crt然后把配置里的ssl_certificate指向fullchain.crt问题立刻消失。这个细节很多文档不会写属于“不发生产事故你根本意识不到”的类型。6.3 性能、并发与平滑升级的实操建议国密算法因为使用椭圆曲线单次握手计算开销比RSA小但这不意味着你可以无脑上高并发。SM2的私钥运算在同安全强度下比RSA快但国密浏览器和服务端在握手阶段的网络交互次数和证书解析路径有额外开销。我压测过一个2核4G的测试机用wrk跑国密HTTPSQPS大概在8000左右相比同样机器上RSA HTTPS的12000有明显差距但实际业务场景里大部分系统不会达到这个量级所以不用一上来就焦虑性能。有几个实用优化手段。第一开启ssl_session_cache让握手结果复用ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m;国密的握手计算虽然快但每次握手都要做密钥协商和证书链校验开启session cache能显著降低CPU占用。第二如果是HTTP/2记得保持连接复用减少重复握手。第三如果要平滑升级nginx用kill -USR2启动新进程再kill -WINCH让旧进程退出这是nginx标准的优雅升级流程国密版同样适用。还有一个容易被忽略的运维点GmSSL作为底层库它的安全补丁更新节奏和OpenSSL官方不同你要定期关注GmSSL的release通知。nginx本身的漏洞修复就按官方公告走但每次升级nginx版本都要重新执行一遍“GmSSL编译 nginx configure make install”的流程。建议把这套操作写成一个shell脚本放到CI里遇到版本升级一键重跑别每次手工敲命令。最后再分享一个经验如果你所在团队接下来要做多个系统的国密改造千万别每个系统单独编译一套国密nginx。正确做法是把编译好的nginx二进制和配置模板做成标准RPM包或者容器镜像放到内部源里后续系统直接拉取部署最多改一改证书路径和域名。国密改造的第一道坎是“跑通”第二道坎其实是“复制”把标准化做在前面后面几十个系统的改造效率能翻几倍。