必须用OpenResty≥1.21.4.1,通过ssl_certificate_by_lua在TLS握手阶段按需加载证书,结合结构化存储、本地同步IO、共享字典缓存及并发限制,实现千万级域名SSL动态管理。

要实现千万级域名的独立 SSL 证书动态管理,不能靠静态 server 块堆叠——那会直接卡死在配置加载、内存占用和 reload 时效上。核心路径只有一条:用 OpenResty + ssl_certificate_by_lua 在 TLS 握手阶段按需加载证书,配合文件组织、缓存与权限控制,才能真正支撑海量 SNI 域名。
必须用 OpenResty,原生 Nginx 不行
原生 Nginx 所有 SSL 配置(ssl_certificate 等)都是编译期或 reload 期绑定的,不支持运行时按域名切换证书。只有满足以下任一条件才可行:
- OpenResty ≥1.21.4.1(推荐),已内置
ssl_certificate_by_lua*指令 - Nginx ≥1.19.0 + 手动编译
ngx_http_ssl_module和lua-nginx-module
验证方式:nginx -V 2>&1 | grep -o 'ssl_certificate_by_lua',无输出即不可用。
证书按域名结构化存储,禁止硬编码路径
千万域名意味着千万级证书文件,不能写死在配置里,也不能用 if 判断拼接路径。标准做法是:
- 根目录统一,如
/etc/nginx/ssl_certs/ - 每个域名一个子目录:
/etc/nginx/ssl_certs/example.com/fullchain.pem、privkey.pem - 私钥权限严格设为
600,Nginx worker 进程需有读取权限(通常属主 root 即可) - 避免使用符号链接或网络文件系统(NFS),防止 handshake 阶段 I/O 阻塞
在 ssl_certificate_by_lua 中完成证书加载与错误兜底
该 Lua 块在 TLS ServerHello 前执行,必须返回 PEM 字符串(不是路径),且不能有耗时操作。示例逻辑如下:
- 从
ngx.var.server_name获取请求域名 - 拼出证书与私钥完整路径,调用
io.open()读取内容 - 成功则调用
ngx.ssl.set_der_cert()和ngx.ssl.set_der_priv_key() - 失败时建议返回
ngx.exit(444)主动断连,而非 fallback 到默认证书(否则易被滥用)
注意:不要在该块中做 DNS 查询、HTTP 请求或复杂正则匹配;所有 IO 应为本地同步读取,且文件需预热进 page cache。
性能加固:加缓存、控并发、限重试
千万域名下,高频证书读取会成为瓶颈,必须叠加优化:
- 用
lua_shared_dict缓存已加载证书的 DER 格式内容,TTL 建议 24h,避免重复解析 PEM - 限制单个 worker 每秒最大证书加载次数(例如用
resty.limit.count),防恶意 SNI 扫描 - 对不存在域名的请求,记录日志但不重试,避免磁盘扫描放大攻击面
- 关闭
ssl_session_cache的 shared 模式(除非有专用 session 共享服务),改用builtin:1000避免锁竞争
不复杂但容易忽略


















