必须使用ssl_certificate_by_lua*在TLS握手阶段动态加载PEM证书与私钥,配合LRU缓存和占位证书,禁用session_cache,才能实现零中断百万级证书自动轮转。

直接用 ssl_certificate 指令无法实现真正的“动态加载”——它只支持静态配置,每次更新都需 reload Nginx,必然中断连接。要达成在线证书自动轮转与维护,必须绕过该指令的限制,转向运行时按需加载的机制。
核心思路:TLS 握手阶段实时加载证书
OpenResty 提供 ssl_certificate_by_lua* 系列指令,允许在 TLS 握手完成前(SNI 域名已知、但证书尚未选定)执行 Lua 逻辑,从文件、内存或远程服务中读取对应域名的 PEM 格式证书与私钥,并通过 API 注入当前连接上下文。
- 该过程发生在 worker 进程内,不依赖全局配置变更,完全规避 reload
- 每个新 TLS 连接独立触发,天然支持多租户、泛域名、测试环境等高频切换场景
- 配合 LRU 缓存(如
lua-resty-lrucache),可避免重复 IO 和解析开销
关键配置与安全约束
以下为生产可用的最小必要设置,缺一不可:
- OpenResty 版本 ≥ 1.19.3.1,且编译时启用
--with-http_ssl_module和 OpenSSL 1.1.1+ - server 块中保留通配
server_name _,并配置占位证书(fallback),防止首次加载失败时握手崩溃 - 禁用
ssl_session_cache或设为off,否则旧会话可能复用已失效证书 - 证书与私钥必须为标准 PEM 格式;路径需经白名单校验,禁止用户可控路径注入
自动轮转的落地闭环
动态加载只是“读取”环节,完整轮转还需配套更新与触发机制:
- Let’s Encrypt 续期后,新证书写入预设目录(如
/etc/nginx/ssl/{domain}.crt),文件名含域名标识 - 使用
inotifywait或定时脚本检测文件变更,触发 Lua 缓存清理(如调用lrucache:delete(domain)) - 新连接自动加载新版,旧连接继续使用原证书直至自然断开,零中断过渡
- 建议增加健康检查端点(如
/_ssl/status?domain=xxx),返回当前生效证书指纹与有效期
替代方案:Nginx 开源版 + njs 共享字典
若无法使用 OpenResty,Nginx 1.25+ 开源版配合 njs 模块亦可实现类似效果:
- 通过
js_shared_dict_zone创建共享内存区,存放域名 → 证书内容的映射 - 用
js_set在 SSL 上下文中读取字典值,再由ssl_certificate和ssl_certificate_key引用变量 - 提供 HTTP API(如
PUT /api/cert)更新字典条目,无需 reload 即刻生效











