Nginx不支持动态控制证书链发送,但可通过OpenResty的ssl_certificate变量(需1.19.3.1+)或Lua内部重定向至不同server块,结合客户端声明的X-TLS-Capability头,按需返回含/不含中间证书的PEM文件,兼顾老旧与现代客户端兼容性。

在生产环境中,某些客户端(如老版本 Android、部分嵌入式设备)不支持 TLS 证书链自动拼接,需要服务端显式返回完整的证书链;而现代浏览器和客户端反而可能因冗余证书链导致握手延迟或兼容性问题。Nginx 本身不支持动态控制 ssl_trusted_certificate 或证书链发送行为,但借助 ngx_http_lua_module,可在 SSL 握手后的 HTTP 请求阶段,通过响应头干预(实际是控制是否启用 full chain),再配合 Nginx 的多证书配置与 Lua 的运行时决策,实现“按需返回证书链”的效果——注意:严格来说,证书链是在 TLS 握手阶段由 server hello 发送的,无法在 HTTP 阶段动态插入;因此真实可行方案是:预置多套 server 配置(不同 ssl_certificate 指向含/不含中间证书的 PEM),再用 Lua 在 rewrite 或 access 阶段识别请求头,触发内部重定向到对应 upstream 或 location,从而复用不同 TLS 配置的 server 块。
前提:编译 Nginx 时启用 Lua 支持并准备两套证书文件
确保 Nginx 已静态或动态加载 ngx_http_lua_module(推荐 OpenResty)。准备两组证书 PEM 文件:
- full.crt:域名证书 + 中间 CA(如 Let's Encrypt R3)拼接而成,供旧客户端使用;
- base.crt:仅含域名证书,无中间证书,供现代客户端使用(减少 handshake size);
- key.pem:私钥保持一致。
将二者分别部署在不同 server 块中,绑定相同 IP:Port 但通过 listen ... ssl http2 reuseport + server_name 区分不可行(端口冲突),正确做法是用不同端口或利用 Nginx 的 map + ssl_certificate 变量(OpenResty 支持运行时变量赋值)。
核心方案:用 Lua 动态设置 ssl_certificate 变量(OpenResty 推荐)
OpenResty 1.19.3.1+ 支持在 ssl_certificate 和 ssl_certificate_key 指令中使用变量(需开启 ssl_engine 兼容模式)。操作步骤如下:
- 在
http块定义 map,根据请求头(如X-Client-Capability: no-chain)映射证书路径:
map $http_x_client_capability $cert_path {
default "/etc/nginx/ssl/base.crt";
"no-chain" "/etc/nginx/ssl/full.crt";
}- 在 server 块中启用变量证书:
server {
listen 443 ssl http2;
server_name example.com;
<pre class="brush:php;toolbar:false;">ssl_certificate $cert_path;
ssl_certificate_key "/etc/nginx/ssl/key.pem";
# 启用 lua,在 rewrite 阶段读取 header 并触发 map 更新(实际 map 是实时生效的)
rewrite_by_lua_block {
-- 可选:记录决策日志
ngx.log(ngx.INFO, "Using cert: ", ngx.var.cert_path)
}
location / {
proxy_pass http://backend;
}
}
⚠️ 注意:该功能依赖 OpenSSL 1.1.1+ 和 OpenResty 特定版本,Nginx 官方主线暂不支持变量证书路径(会报错 “invalid number of arguments”)。
兼容方案:Lua 内部重定向到不同 server 块(纯 Nginx 可行)
若无法升级 OpenResty,可采用“单端口 + 多 location + 内部重定向”方式:
- 主 server 块监听 443,不做 SSL 终止,仅做 header 判断和重定向;
- 另设两个内部 server 块,分别监听 8443(full chain)和 8444(base only),启用 SSL;
- 主 server 中用 Lua 判断
$http_user_agent或自定义 header,执行ngx.exec("@full")或ngx.exec("@base"); - 两个 named location 分别 proxy_pass 到对应内部 HTTPS server(需启用
proxy_ssl_*参数透传证书)。
此方式增加一次代理跳转,有轻微性能损耗,但 100% 兼容所有 Nginx + Lua 环境。
如何判断客户端是否需要证书链?实用 header 规则
不建议依赖 User-Agent(易误判),更可靠的是由客户端主动声明能力:
- 前端 SDK 或 App 在首次 HTTPS 请求时,添加自定义 header:
X-TLS-Capability: full-chain或X-TLS-Capability: minimal; - 对未携带该 header 的请求,默认走 base.crt(保守策略);
- 也可结合 TLS fingerprint(如 JA3)做 UA 补充判断,但需额外模块,复杂度高,生产慎用。
验证方法:用 openssl s_client -connect example.com:443 -servername example.com | openssl x509 -noout -text 查看返回的证书数量,或用 Wireshark 抓包观察 server hello 中 Certificate 消息包含几个证书。











