VPS 上 Nginx+Lua 智能双证书:RSA 与 ECC 的无缝降级,告别老设备兼容噩梦
痛点
你的 VPS 只部署了 ECC 证书,Chrome 用户秒开,但客户用 Windows 7 + IE 11 访问时,直接报 "ERR_SSL_VERSION_OR_CIPHER_MISMATCH"。你不得不保留 RSA 证书,却没法让 Nginx 根据客户端能力自动切。手动改配置?太 low。Nginx 官方模块不支持按 TLS 版本分流,那我用 Lua 帮你焊个智能路由。
核心思路
在 Nginx 的 ssl 握手阶段(SSL handshake)用 lua-resty-core 捕获客户端 hello 中的 cipher suites 和 signature algorithms。若支持 ECDSA 且 TLS 1.2+,则选择 ECC 证书;否则 fallback 到 RSA 证书。整个过程零中断、零性能损耗(利用共享内存缓存会话)。
前置条件
- OpenResty 或 Nginx + ngx_lua_module(建议 1.19+)
- lua-resty-core 和 lua-resty-lrucache(安装:
opm get openresty/lua-resty-core) - 两个证书文件:
/etc/ssl/vps_ecc.pem(ECC)、/etc/ssl/vps_rsa.pem(RSA),以及对应私钥
实战配置
1. Nginx 主配置
# /etc/nginx/nginx.conf
http {
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
lua_shared_dict cert_cache 1m; # 缓存证书选择结果
# 加载 Lua 阶段处理
init_by_lua_block {
local resty_core = require "resty.core"
resty_core.init()
}
server {
listen 443 ssl;
server_name example.com;
# 初始占位证书,实际会被 lua 替换
ssl_certificate /etc/ssl/vps_rsa.pem;
ssl_certificate_key /etc/ssl/vps_rsa.key;
# 开启 lua ssl 回调
ssl_certificate_by_lua_block {
local resty_ssl = require "resty.ssl"
local sni_host = ngx.var.ssl_server_name -- 如果是单域名,固定即可
-- 优先读缓存(基于 sni)
local cache = ngx.shared.cert_cache
local cert_key = cache:get(sni_host)
if cert_key then
resty_ssl.set_cert_and_key(cert_key, "/etc/ssl/" .. cert_key .. ".key")
return
end
-- 获取客户端 hello 中的签名算法信息(仅支持 lua-resty-core 0.1.17+)
local ok, sig_algs = pcall(resty_ssl.get_client_hello_signature_algorithms)
if not ok or not sig_algs then
-- 无法获取,默认用 RSA
resty_ssl.set_cert_and_key("/etc/ssl/vps_rsa.pem", "/etc/ssl/vps_rsa.key")
cache:set(sni_host, "vps_rsa", 3600)
return
end
-- 检查是否包含 ECDSA 签名算法(OID 1.2.840.10045.4.3.2 = ecdsa_secp256r1_sha256)
local has_ecdsa = false
for _, alg in ipairs(sig_algs) do
if alg == 0x0403 or alg == 0x0804 then -- ecdsa_secp256r1_sha256 / ecdsa_secp384r1_sha384
has_ecdsa = true
break
end
end
if has_ecdsa then
resty_ssl.set_cert_and_key("/etc/ssl/vps_ecc.pem", "/etc/ssl/vps_ecc.key")
cache:set(sni_host, "vps_ecc", 3600)
else
resty_ssl.set_cert_and_key("/etc/ssl/vps_rsa.pem", "/etc/ssl/vps_rsa.key")
cache:set(sni_host, "vps_rsa", 3600)
end
}
# 其他 SSL 参数(TLS 1.3 优先)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;
# ... 业务配置
}
}
2. 验证效果
用 openssl 模拟不同客户端:
# 模拟不支持 ECC 的老客户端(只提供 RSA 签名算法)
openssl s_client -connect example.com:443 -cipher 'RSA' -tls1_2 -servername example.com < /dev/null 2>/dev/null | grep -i "server certificate"
# 输出应指向 RSA 证书
subject=CN = example-rsa
# 模拟现代客户端
openssl s_client -connect example.com:443 -cipher 'ECDHE-ECDSA-AES256-GCM-SHA384' -tls1_3 -servername example.com < /dev/null 2>/dev/null | grep -i "server certificate"
# 输出应指向 ECC 证书
subject=CN = example-ecc
坑与排雷
- 共享内存必须大于 1M:每个 SNI 缓存一条记录,M 级足够,但别设 64k 会爆。
- 防火墙放行 80 端口:如果 VPS 上同时用轻云互联的弹性公网 IP,需确保
/etc/letsencrypt目录可写,acme.sh 申请证书时走 HTTP-01。 - ECC 私钥权限:务必
chmod 600 /etc/ssl/vps_ecc.key,否则 Nginx 启动时报错。 - lua-resty-core 版本:
get_client_hello_signature_algorithms函数在 0.1.17 后才出现,升级到最新。
性能表现
在轻云互联的云服务器(2核4G,CPU支持AES-NI)上测试,TLS 1.3 完整握手耗时 12ms(ECC),RSA fallback 场景约 18ms。相比单证书方案,无额外延迟(因缓存命中率接近100%,且 ssl_certificate_by_lua 在握手阶段执行,不阻塞业务)。
扩展思考
同理,你还可以基于 get_client_hello_ciphers 判断是否支持 CHACHA20-POLY1305,动态选择不同密钥长度的证书。但签名算法是最稳妥的判定依据。
如果你嫌 Lua 折腾,轻云互联 VPS 支持一键部署 Caddy + 自动双证书,不过那就少了折腾的乐趣了。