<p>必须使用 ssl_certificate_by_lua* 指令在 TLS 握手完成、SNI 解析成功后执行 Lua 逻辑,以动态切换证书;需满足 OpenResty ≥ 1.19.3.1、禁用 ssl_preread 和会话缓存、证书为无密码 PEM 格式等前提,并配置 fallback 证书与 Lua 脚本实现安全 SNI 获取、缓存加载及优雅降级。</p>

要在 SSL 握手阶段根据 SNI 动态切换证书,必须使用 ssl_certificate_by_lua* 指令,在 TLS 握手完成、SNI 解析成功后立即执行 Lua 逻辑——这是唯一能同时读取 SNI 并干预证书加载的阶段。
确认运行环境与前提条件
动态证书切换不是配置即生效的功能,需确保以下几点已满足:
- OpenResty 版本 ≥ 1.19.3.1(推荐最新稳定版),且编译时启用了
--with-http_ssl_module和 OpenSSL 1.1.1+; - Nginx 配置中未启用
ssl_preread on(该模式跳过握手,$ssl_server_name不可用); - 禁用共享内存会话缓存:
ssl_session_cache off,防止旧会话复用过期证书; - 证书与私钥为无密码 PEM 格式,存放路径受严格白名单校验(如仅允许
/etc/letsencrypt/live/下的文件)。
核心配置:server 块 + fallback 证书
每个 HTTPS server 块需保留一个兜底证书,确保 Lua 加载失败时仍可完成握手,避免连接中断:
- 监听
443 ssl,server_name _匹配所有 SNI 域名; -
ssl_certificate和ssl_certificate_key指向一个有效的通配符或自签名 fallback 证书; - 添加
ssl_certificate_by_lua_file /path/to/dynamic_ssl.lua,由 Lua 脚本接管后续证书选择。
Lua 脚本实现关键逻辑
脚本需完成三件事:安全获取 SNI、查证并加载对应证书、出错时优雅降级:
- 调用
ngx.ssl.server_name()获取客户端声明的域名(非ngx.var.ssl_server_name,后者在部分 OpenResty 版本中不可靠); - 用
lua-resty-lrucache缓存已解析的证书内容,避免每次握手都触发磁盘 IO 和 PEM 解析; - 通过
ngx.ssl.set_cert()和ngx.ssl.set_priv_key()设置证书链与私钥; - 若证书缺失或解析失败,不中断流程,让 Nginx 自动回退到配置中的 fallback 证书。
对接 Let’s Encrypt 自动续期
动态加载天然适配 ACME 流程,无需 reload 即可生效:
- 证书文件更新后,Lua 可基于文件
mtime或lua_shared_dict中记录的时间戳判断是否需要刷新缓存; - 建议在 Certbot 的
--deploy-hook中写入时间戳或触发信号,而非直接修改 Nginx 配置; - 整个过程对线上流量零干扰,新证书在下一次握手时自动生效。


















