Ingress NGINX Controller 动态 SSL 证书机制解析:从 `--enable-dynamic-certificates` 弃用到 Only-Dynamic 架构
Ingress NGINX Controller 动态 SSL 证书机制解析从--enable-dynamic-certificates弃用到 Only-Dynamic 架构【免费下载链接】ingress-nginxIngress NGINX Controller for Kubernetes项目地址: https://gitcode.com/GitHub_Trending/in/ingress-nginx导读本文基于仓库中 docs/enhancements/20190724-only-dynamic-ssl.md 这一增强提案KEP系统梳理 Ingress NGINX Controller 从静态 SSL 配置全面转向仅动态Only-DynamicSSL 配置的演进历程与底层实现。文中将解释为什么动态证书模式可以免除 NGINX reload、ssl_certificate_by_lua在握手阶段如何按 SNI 挑选证书、以及当前控制器中配置足够动态化的判定逻辑与降级路径。读完本文你将理解该控制器在生产环境热更新 TLS 证书的完整机制并能结合源码定位证书从 Kubernetes Secret 到 NGINX 共享内存再到 TLS 握手的完整数据链路。一、提案背景为什么必须移除静态 SSL 配置模式2019-07-24 由 aledbf 提交、ElvinEfendi 评审的增强提案 20190724-only-dynamic-ssl.md其核心诉求是彻底移除静态 SSL 配置模式Remove static SSL configuration mode。提案的 Summary 给出了两个关键时间节点仓库当前代码已完全实现该提案因此以下描述的是该机制的最终形态0.19.0 起可以借助 Lua 在不触发 NGINX reload的情况下配置 SSL 证书0.24.0 起动态证书模式成为默认启用的模式。而 Motivation 一句话点明了动因The static configuration implies reloads, something that affects the majority of the users.即静态配置必然意味着 reload而 reload 会影响绝大多数用户连接中断、瞬时性能抖动、配置重载期间的一致性窗口等。提案的 Goals 与 Non-Goals 非常克制条目内容Goals弃用--enable-dynamic-certificates标志清理相关代码库Non-Goals不改变任何证书认证certificate authentication相关功能Proposal移除静态 SSL 配置也就是说这是一次收敛实现路径、减少维护面的工程决策动态证书能力已成熟并成为默认静态路径不再需要保留。二、动态证书的核心机制握手期的ssl_certificate_by_lua2.1 http 段的兜底证书与 server 段的 Lua 回调提案 Implementation Details 中明确提出要把ssl_certificate/ssl_certificate_key指令从每个 server 块迁移到http 段以避免 NGINX 在启动时因为缺少证书指令而报错。这在当前模板 rootfs/etc/nginx/template/nginx.tmpl 中体现得十分清晰# PEM sha: {{ $cfg.DefaultSSLCertificate.PemSHA }} ssl_certificate {{ $cfg.DefaultSSLCertificate.PemFileName }}; ssl_certificate_key {{ $cfg.DefaultSSLCertificate.PemFileName }};这两条指令位于http 上下文模板第 490–491 行使用配置项DefaultSSLCertificate对应的 PEM 文件——它可以是--default-ssl-certificate指定的证书也可以是控制器自签的 fake 证书见 internal/ingress/controller/config/config.go 的注释说明。这个兜底证书保证了 NGINX 无论何时都能完成 SSL 握手例如客户端不支持 SNI 时。而每个启用 HTTPS 的 server 块中则统一挂载动态证书入口模板第 597、927 行等ssl_certificate_by_lua_file /etc/nginx/lua/nginx/ngx_conf_certificate.lua;ssl_certificate_by_lua_file是 OpenResty 在SSL 握手阶段执行的回调此时客户端尚未发送任何 HTTP 请求但已经通过SNIServer Name Indication声明了目标主机名NGINX 可以借此动态决定该连接应该加载哪一张证书。2.2 证书选择的完整调用链入口脚本 rootfs/etc/nginx/lua/nginx/ngx_conf_certificate.lua 只有一行实质内容——调用核心模块local certificate require(certificate) certificate.call()真正的选择逻辑在 rootfs/etc/nginx/lua/certificate.lua 的_M.call()第 225–274 行其流程为获取 SNI 主机名ssl.server_name()返回客户端 SNI 中的主机名若客户端不支持 SNI 或未携带主机名则回退到默认值_DEFAULT_CERT_HOSTNAME第 16 行。在共享内存中查表get_pem_cert_uid(hostname)第 48–71 行先在certificate_servers共享字典中按主机名精确查找证书 UID若未命中则尝试通配符降级——将foo.example.com转换为*.example.com再次查询这正好支撑了通配符证书的按需选择。取证书内容通过 UID 从certificate_data共享字典中取出 PEM 证书与私钥。设置握手证书ssl.clear_certs()清空兜底证书随后ssl.set_der_cert/ssl.set_der_priv_key将转换后的 DER 证书与私钥注入当前 SSL 连接第 22–46 行。可选 OCSP stapling当启用 OCSP stapling 时调用ocsp_staple从ocsp_response_cache取出缓存的 OCSP 响应并装配未命中时通过ngx.timer.at(0, ...)异步后台抓取第 202–215 行。从源码结构可以推断由于证书数据存放在NGINX 共享内存shared dict中控制器侧更新共享字典后后续新 TLS 握手即可拿到新证书无需 reload NGINX 主进程——这正是动态证书免除 reload 的原理所在。2.3 共享字典与 Lua 侧的数据推送certificate_data、certificate_servers等共享字典的默认容量定义在 internal/ingress/controller/template/configmap.gocertificate_data: 20480, // KB即 20M certificate_servers: 5120, // 5M ocsp_response_cache: 5120, // 5M与 certificate_servers 保持一致用户可通过 ConfigMap 的lua-shared-dicts键调整这些尺寸相关单元测试见 internal/ingress/controller/template/configmap_test.goe2e 测试见 test/e2e/settings/lua_shared_dicts.go。字典过小或证书数量过多时Lua 侧写入失败会打印错误并可能触发移除已有条目的强制写入见 certificate.lua 第 183–191 行的forcible处理。模板渲染时还会校验字典大小下限template.go。控制器侧将证书写入共享字典的动作发生在配置同步流程中——证书的 PEM 文件在 internal/ingress/controller/store/backend_ssl.go 的syncSecret中落盘并由 Lua 侧ngx_conf_configuration.lua等脚本把certificate_data/certificate_servers键值对推入共享内存。配套的 Lua 单测见 rootfs/etc/nginx/lua/test/certificate_test.lua。三、控制器侧判定这个变更能不能不 reload动态证书只是动态能力的一环。控制器在每次同步时都要回答一个问题这次的配置变更能否纯动态地应用从而跳过 reload该判定实现在 pkg/util/ingress/ingress.go// IsDynamicConfigurationEnough returns whether a Configuration can be // dynamically applied, without reloading the backend. func IsDynamicConfigurationEnough(newcfg, oldcfg *ingress.Configuration) bool { copyOfRunningConfig : *oldcfg copyOfPcfg : *newcfg copyOfRunningConfig.Backends []*ingress.Backend{} copyOfPcfg.Backends []*ingress.Backend{} clearL4serviceEndpoints(copyOfRunningConfig) clearL4serviceEndpoints(copyOfPcfg) clearCertificates(copyOfRunningConfig) clearCertificates(copyOfPcfg) return copyOfRunningConfig.Equal(copyOfPcfg) }逻辑要点Backendsupstream清空后再比较因为后端节点/端点变更由 Lua 侧的balancer_by_lua动态处理不需要 reloadL4 服务端点TCP/UDP 四层服务的端点同样被忽略clearL4serviceEndpoints证书通过clearCertificates第 160–170 行将 server 的SSLCert置空后比较——正如其注释所说动态证书开启时证书变更应被忽略因为握手期 Lua 已经能动态换证书。对应的一组行为测试在 pkg/util/ingress/ingress_test.go当仅有证书或端点变化时IsDynamicConfigurationEnough返回 true可动态应用当出现新增/删除 host、变更 TLS 之外的 server 结构等结构性变化时返回 false需要 reload。判定结果在 internal/ingress/controller/controller.go 中被消费if !utilingress.IsDynamicConfigurationEnough(pcfg, n.runningConfig) { klog.InfoS(Configuration changes detected, backend reload required) // ... n.OnUpdate(*pcfg) 触发 reload } // 否则走动态路径 err wait.ExponentialBackoff(retry, func() (bool, error) { err : n.configureDynamically(pcfg) // ... })即结构性变更走OnUpdatereload非结构性变更证书、端点、backend走configureDynamically并通过指数退避DynamicConfigurationRetries步进wait.Backoff保证动态下发最终成功。reload 失败时会通过事件recorder.Eventf与指标IncReloadErrorCount、ConfigSuccess暴露给运维。四、证书生命周期从 Kubernetes Secret 到 PEM 文件为了把动态讲透还需要理解证书数据的来源。每个被 Ingressspec.tls引用的 Secret 都会经过 internal/ingress/controller/store/backend_ssl.go 的同步链syncSecret(key)第 38–72 行加锁syncSecretMu后调用getPemCertificate解析 Secret生成ingress.SSLCert若本地 store 中已有且内容相等则跳过否则Update/Add并发送 dummy 事件以触发控制器重新同步。getPemCertificate第 76 行起从 Secret 的tls.crtapiv1.TLSCertKey、tls.keyapiv1.TLSPrivateKeyKey、ca.crt、ca.crl、auth等数据项中解析证书、密钥、CA 与 CRL密钥名namespace/secretName被转换为namespace-secretName作为文件命名。落盘 PEM解析出的证书/密钥/CA 等以 PEM 文件形式写入文件系统PemFileName字段供 http 段的兜底ssl_certificate以及 Nginx 其他指令如proxy_ssl_certificate引用同时 Lua 侧把证书内容与主机名 → 证书 UID的映射写入共享字典。syncSecrets会遍历 Ingress 引用的全部 Secret 批量同步store.goSecret 变更事件同样会触发syncSecret如 store.go 第 609、643、1151 行的调用点从而在不 reload的情况下完成证书热更新。default SSL 证书则通过--default-ssl-certificate指定见 controller.go 的getDefaultSSLCertificate。五、实施约束与注意点结合提案的 Constraints 与当前代码使用/运维动态证书模式时有以下几点值得注意兜底证书不可缺http 段的ssl_certificate/ssl_certificate_key必须指向一个始终存在的 PEMDefaultSSLCertificate否则 NGINX 启动即报错。这也是提案要求把这两条指令从 server 块上移到 http 段的原因见 nginx.tmpl。无 SNI 客户端走默认证书握手时拿不到 SNI如老客户端、部分健康检查会直接使用默认证书certificate.lua 第 230–234 行。共享字典容量与证书规模匹配certificate_data默认 20M、certificate_servers与ocsp_response_cache默认各 5M。证书数量多或单证书链大时需通过 ConfigMap 的lua-shared-dicts扩容configmap.go。证书认证功能不受影响提案 Non-Goals 明确声明不改变 certificate authentication 相关能力Secret 中的ca.crt、ca.crl、auth处理路径依旧保留backend_ssl.go。OCSP stapling 走独立缓存动态证书模式下 OCSP 响应缓存在ocsp_response_cache中首次请求可能不带 stapled 响应见 certificate.lua 第 194–201 行的stale-while-revalidate式设计权衡。六、提案落地后的现状与测试保障该提案当前状态为implementable而仓库现状表明其已被完全实施--enable-dynamic-certificates标志已从代码中移除docs/kubectl-plugin.md第 271、288 行明确指出 kubectl 插件不再支持该已移除的配置标志动态证书成为唯一的证书提供方式Only-Dynamic相关行为有完善的测试覆盖动态判定单元测试pkg/util/ingress/ingress_test.go证书 Lua 单元测试rootfs/etc/nginx/lua/test/certificate_test.luae2e 动态证书测试test/e2e/lua/dynamic_certificates.go、test/e2e/leaks/lua_ssl.go后者用于验证 Lua SSL 相关路径无内存泄漏清理后配置的黄金文件对比test/data/cleanConf.expected.conf、test/data/cleanConf.src.conf 中的ssl_certificate_by_lua_block片段。从历史变更看该能力自 0.19.0 引入动态证书、0.24.0 默认开启到本次提案彻底移除静态路径形成了引入 → 默认化 → 唯一化的完整演进闭环。对使用者而言这意味着只要证书存放在 Kubernetes Secret 中并被 Ingress 引用控制器就能在零 reload 的前提下完成证书轮换——这正是该提案给集群运维带来的核心价值。延伸阅读提案原文docs/enhancements/20190724-only-dynamic-ssl.md动态证书 Lua 核心rootfs/etc/nginx/lua/certificate.lua、rootfs/etc/nginx/lua/nginx/ngx_conf_certificate.lua模板中 SSL 相关指令rootfs/etc/nginx/template/nginx.tmpl动态配置判定与控制器消费pkg/util/ingress/ingress.go、internal/ingress/controller/controller.goSecret 证书同步internal/ingress/controller/store/backend_ssl.go共享字典配置internal/ingress/controller/template/configmap.go【免费下载链接】ingress-nginxIngress NGINX Controller for Kubernetes项目地址: https://gitcode.com/GitHub_Trending/in/ingress-nginx创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考