Kubernetes证书管理实战:cert-manager架构、ACME验证与生产避坑指南
1. 为什么手动管证书这件事迟早会翻车但凡在生产环境跑过一段时间的 Kubernetes 集群大概率都经历过这样的场景某个内部服务的 TLS 证书悄悄过期了半夜告警炸群爬起来一看是三个月前手动签的那张证书忘了续。更麻烦的是集群里跑着几十上百个 Ingress、Webhook、内部 gRPC 服务每个都要证书每个证书的签发方式、有效期、续期时间点还都不一样靠人肉维护出事只是时间问题。这就是cert-manager要解决的核心问题。它本质上是 Kubernetes 的一个控制器Controller把证书的申请、签发、续期、轮换这一整套流程变成声明式的资源对象来管理。你只需要写一个 YAML告诉它我要一个给api.example.internal用的证书用 Lets Encrypt 签剩下的它自己搞定到期前自动续续完自动更新到 Secret 里业务侧完全无感。这篇文章适合三类人看一是刚接触 K8s、被证书问题折磨过的运维和开发二是已经在用 cert-manager 但只会照抄 YAML、不清楚背后原理的同学三是想搞清楚 ACME 协议、Issuer/ClusterIssuer 区别、DNS-01 和 HTTP-01 到底怎么选的技术负责人。我会从架构设计讲到实操落地把踩过的坑和参数选择的逻辑都摊开说尽量让你看完能直接抄作业也能明白为什么这么抄。先说清楚一个前提cert-manager 不是装完就万事大吉的银弹。它的能力边界、签发机构的限制、DNS 服务商的 API 权限、集群网络出口策略任何一个环节没配对都会导致证书卡在Pending或者False状态。所以下面我会花不少篇幅讲排查这部分才是真正值钱的东西。2. cert-manager 的整体架构与核心概念拆解2.1 它到底由哪几个组件组成很多人装完 cert-manager 只知道kubectl apply一个 yaml但不知道集群里多了什么。默认安装以 Helm 为例会在cert-manager命名空间下起三个 Deploymentcert-manager主控制器负责监听 Certificate、Issuer、CertificateRequest 等资源做调谐Reconcile逻辑。这是大脑。cert-manager-webhook准入控制器校验你提交的资源是否合法同时负责把 CRD 的转换逻辑跑起来。它自己也需要一张证书来跟 API Server 通信这张证书是自签的由 cert-manager 自己给自己签这里有个先有鸡还是先有蛋的问题官方用了一个 init 容器加自签 CA 的方式解决。cert-manager-cainjector专门负责把 CA bundle 注入到 WebhookConfiguration、APIService、CRD 这些地方。因为 K8s 的 webhook 需要 API Server 信任你的 CA这个组件就是干这个脏活的。理解这三个组件的分工很重要。比如你发现 webhook 报x509: certificate signed by unknown authority那大概率是 cainjector 没把 CA 正确注入而不是主控制器的问题。排查方向一下就清晰了。2.2 Issuer 和 ClusterIssuer 到底怎么选这是新手最容易纠结的点。简单说Issuer是命名空间级别的只能给同一个 namespace 下的 Certificate 签发。ClusterIssuer是集群级别的任何 namespace 都能引用。那什么时候用哪个我的经验是如果签发机构需要凭据比如 DNS 服务商的 API Token、ACME 账号私钥优先用 ClusterIssuer。因为凭据只需要维护一份放在一个固定的 namespace 里其他团队引用就行不用每个 namespace 都复制一遍 Secret。反过来如果某个团队有自己独立的签发需求比如内部 CA 只给特定业务签那就用 Issuer 做隔离。这里有个坑ClusterIssuer 引用的 Secret 必须和 cert-manager 控制器在同一个 namespace默认是cert-manager而不是你业务所在的 namespace。很多人把 Secret 建在自己业务 namespace 下然后 ClusterIssuer 一直报找不到 Secret就是这个原因。2.3 Certificate、CertificateRequest、Order、Challenge 的关系cert-manager 的资源对象是一层套一层的理解这个链条对排查问题至关重要Certificate你声明的最终目标 └── CertificateRequest一次签发请求 └── OrderACME 场景下的订单 └── Challenge域名所有权验证你平时只需要写Certificate后面三个都是 cert-manager 自动创建的。但当证书卡住时你得顺着这条链往下看Certificate状态是False去看它关联的CertificateRequest再看Order最后看Challenge的详细事件问题往往就藏在最底层的 Challenge 里比如 DNS 记录没生效、HTTP 路径被防火墙拦了。3. 签发方式选型ACME 的两种验证模式怎么挑3.1 HTTP-01 验证简单但有硬性限制HTTP-01 的原理是ACME 服务端给你一个 token让你把它放到http://你的域名/.well-known/acme-challenge/token这个路径下它来访问验证。cert-manager 会自动创建一个临时的 Ingress 或者用你指定的 Ingress 来响应这个请求。优点是配置简单不需要 DNS 服务商的 API 权限。缺点是不支持泛域名证书*.example.com这是 ACME 协议层面的限制不是 cert-manager 的问题。你的服务必须能从公网被访问到 80 端口。内网服务、纯内网集群基本没戏。如果前面有 CDN 或者 WAF可能会拦截这个验证路径。我一般只在公网可访问的单域名服务场景下用 HTTP-01比如对外的官网、API 网关。3.2 DNS-01 验证泛域名和内网场景的唯一解DNS-01 的原理是ACME 服务端给你一个值让你在_acme-challenge.你的域名加一条 TXT 记录它去查 DNS 验证。cert-manager 通过调用 DNS 服务商的 API 自动加记录、验证、删记录。它的优势很明显支持泛域名一张证书搞定*.example.com和example.com。不需要服务暴露到公网只要 DNS 能解析就行内网服务也能签。不受 80 端口和 CDN 影响。代价是需要 DNS 服务商的 API 凭据而且这个凭据权限要开对。cert-manager 支持一大堆 DNS 提供商Route53、Cloudflare、阿里云 DNS、腾讯云 DNSPod 等通过 webhook 的方式扩展。注意很多 DNS 提供商的支持是以独立 webhook 组件形式存在的不是装在主控制器里需要额外部署。3.3 两种模式对比速查维度HTTP-01DNS-01泛域名支持不支持支持服务需公网可达需要80端口不需要需要 DNS API 凭据不需要需要配置复杂度低中高受 CDN/WAF 影响可能基本不受典型场景公网单域名泛域名、内网服务我的建议很直接只要你的 DNS 服务商有对应的 webhook 支持无脑选 DNS-01。一次配好泛域名、内网、多环境全都能覆盖长期看省心得多。4. 从零落地完整实操流程4.1 安装 cert-manager 的几种方式与选择官方推荐用 Helm 或者直接 apply 官方发布的静态 manifest。我实测下来Helm 更适合生产因为升级、回滚、改配置都方便而且能通过 values 控制副本数、资源限制、亲和性这些。用 Helm 安装的基本命令这里只写关键参数具体版本号按你集群的 K8s 版本对齐helm repo add jetstack https://charts.jetstack.io helm repo update helm install cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --set installCRDstrue \ --set replicaCount2 \ --set webhook.replicaCount2 \ --set cainjector.replicaCount2几个关键点解释一下installCRDstrue会自动装 CRD。生产环境我建议单独管理 CRD用--set installCRDsfalse然后手动 apply避免 Helm 升级时误删 CRD 导致所有 Certificate 资源丢失。这个坑很致命CRD 一删所有证书对象全没了。replicaCount设成 2 是为了高可用。cert-manager 用的是 leader election多副本只有一个在干活但挂了能快速切换。装完先别急着用kubectl get pods -n cert-manager确认三个组件都Running且Ready。注意如果你的集群用了 PodSecurityPolicy 或者 Pod Security Admission 的 restricted 模式cert-manager 的 Pod 可能需要额外的安全上下文配置装之前先看官方文档的兼容性说明否则 Pod 起不来。4.2 配置 DNS-01 的 ClusterIssuer以某 DNS 服务商为例假设我们用某个支持 API 的 DNS 服务商先创建存放 API 凭据的 Secret。凭据的具体字段名取决于你用的 webhook 组件这里用通用的写法示意apiVersion: v1 kind: Secret metadata: name: dns-provider-credentials namespace: cert-manager type: Opaque stringData: api-token: 你的API令牌然后创建 ClusterIssuerapiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-dns spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: opsexample.internal privateKeySecretRef: name: letsencrypt-dns-account-key solvers: - dns01: webhook: groupName: acme.example.internal solverName: dns-solver config: apiTokenSecretRef: name: dns-provider-credentials key: api-token这里几个参数值得展开说server先用 staging 环境测试Lets Encrypt 的生产环境有速率限制比如同一域名每周签发次数有限调试阶段反复失败很容易触发限流被锁一段时间。staging 地址是https://acme-staging-v02.api.letsencrypt.org/directory签出来的证书浏览器不信任但流程能跑通。privateKeySecretRef这是 ACME 账号的私钥cert-manager 自动生成和管理你不需要管但别删。solvers定义用哪种方式验证。DNS-01 的 webhook 配置里groupName和solverName必须和 webhook 组件注册的一致写错了会报找不到 solver。创建完用kubectl describe clusterissuer letsencrypt-dns看状态Ready为True才算成功。如果False看 Events 里的报错通常是凭据不对或者网络不通。4.3 签发第一张泛域名证书ClusterIssuer 就绪后写一个 Certificate 资源apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: wildcard-example namespace: default spec: secretName: wildcard-example-tls duration: 2160h # 90天 renewBefore: 720h # 到期前30天续期 issuerRef: name: letsencrypt-dns kind: ClusterIssuer dnsNames: - example.internal - *.example.internal参数选择的逻辑durationLets Encrypt 的证书固定 90 天你写更长也没用它会按签发机构的实际有效期来。写 2160h90天是为了语义清晰。renewBefore到期前多久开始续。默认是有效期的 2/3也就是 60 天时续。我习惯设成 30 天留足排查时间。如果续期失败还有 30 天可以人工介入不至于直接过期。secretName签出来的证书和私钥会存到这个 Secret 里业务侧挂载这个 Secret 就行。apply 之后用kubectl get certificate看状态READY变成True就成功了。整个过程通常几十秒到几分钟取决于 DNS 记录的生效速度。4.4 业务侧怎么消费这张证书证书签好后Secret 里会有tls.crt、tls.key、ca.crt三个 key。消费方式看你的场景Ingress直接在 Ingress 的tls字段引用这个 SecretNginx Ingress Controller 会自动加载。应用直接挂载用 volume 把 Secret 挂到容器里应用读文件。注意 K8s 更新 Secret 后挂载的文件会延迟一段时间才更新kubelet 同步周期应用需要能热加载证书或者配合 reloader 类工具触发重启。Istio/Service MeshIstio 有自己的证书管理但也可以集成 cert-manager 签的证书做 ingress gateway 的 TLS。这里有个实操心得如果你的应用不支持热加载证书Secret 更新后 Pod 不会自动重启。我一般会配合一个叫 reloader 的小工具它监听 Secret 变化自动给关联的 Deployment 打 patch 触发滚动更新。这样证书续期后业务无感。5. 常见故障排查与避坑经验实录5.1 证书一直 Pending 的排查路径这是最高频的问题。我的排查顺序是固定的kubectl describe certificate name看 Events通常会有明确报错。如果 Certificate 没线索找它关联的 CertificateRequestkubectl get certificaterequest看状态。再往下找 Orderkubectl get orderkubectl describe order name。最后看 Challengekubectl get challengekubectl describe challenge name。DNS-01 的问题基本都在这里暴露比如DNS record not yet propagated或者unauthorized。5.2 DNS-01 验证失败的几种典型原因现象可能原因解决方向Challenge 一直 pendingDNS 记录未生效检查 DNS 服务商 API 权限、TTL 设置unauthorizedAPI 凭据错误或权限不足重新核对 Token 和权限范围记录加了但验证失败存在多条冲突 TXT 记录清理旧的_acme-challenge记录间歇性失败DNS 传播延迟调大 webhook 的传播检查超时时间特别说一下多条冲突 TXT 记录这个坑。如果你之前手动加过_acme-challenge的 TXT 记录或者多个 Certificate 同时给同一个域名签DNS 上会有多条 TXTACME 服务端可能读到旧的那条导致验证失败。解决办法是确保同一时间只有一个 Challenge 在跑或者清理历史记录。5.3 速率限制被锁怎么办Lets Encrypt 的速率限制是真实存在的常见的有同一域名每周签发上限、同一账号每小时请求上限、失败验证次数上限。一旦触发会被锁一段时间期间所有签发都失败。我的经验是调试阶段一律用 staging确认流程完全跑通再切生产。切换时注意staging 和生产的账号私钥是不同的privateKeySecretRef最好用不同的名字避免混淆。另外如果已经被锁看错误信息里的重试时间别反复重试加重惩罚。5.4 webhook 组件相关的坑如果你用了 DNS webhook有几个高频问题webhook Pod 没起来看它的日志常见是 RBAC 权限不足或者镜像拉取失败。找不到 solvergroupName和solverName拼写错误或者 webhook 的 Service 名字和注册的不一致。webhook 证书过期webhook 自己也需要 TLS 证书跟 API Server 通信这个证书由 cert-manager 管理如果 cert-manager 本身出问题webhook 也会挂形成死锁。这种情况需要手动介入恢复。提示生产环境建议给 cert-manager 的组件配置 PodDisruptionBudget 和资源限制避免节点驱逐时全部挂掉导致证书管理瘫痪。6. 生产环境的进阶配置与长期维护6.1 监控与告警怎么配cert-manager 暴露了 Prometheus 指标默认在 9402 端口。几个必须关注的指标certmanager_certificate_expiration_timestamp_seconds证书过期时间戳配合告警规则在到期前 14 天触发。certmanager_certificate_ready_status证书就绪状态非 1 就告警。certmanager_http_acme_client_request_countACME 请求计数异常升高说明有问题。告警规则的核心思路是不要等证书过期才告警要在续期失败时就告警。因为续期失败到真正过期之间通常有几十天窗口这段时间足够人工介入。6.2 多集群和多环境的证书管理策略如果你管着多个集群开发、测试、生产证书策略要分开开发/测试可以用 staging 或者内部自签 CA避免消耗生产环境的速率配额。生产用正式的 ACME 或者企业级 CA凭据严格管控。多集群每个集群独立部署 cert-manager独立配置 ClusterIssuer。不要想着跨集群共享网络和权限都会很麻烦。如果公司有内部 CA比如基于 Vault 或者自建 PKIcert-manager 也支持通过Venafi、Vault、CA等 Issuer 类型对接。内部 CA 的好处是签发快、无速率限制、可控性强适合内网服务。6.3 升级 cert-manager 的注意事项cert-manager 的版本升级有几个雷区CRD 升级新版本可能引入新的 CRD 字段升级前先 apply 新版 CRD再升级控制器。顺序反了可能导致控制器读不懂旧资源。API 版本变更cert-manager 从v1alpha2到v1有过大改老资源需要迁移。升级前用cmctl upgrade工具检查兼容性。webhook 兼容性主控制器和 webhook 版本要匹配混用可能出问题。我的做法是升级前先在测试集群跑一遍确认所有 Certificate 都能正常续期再上生产。生产升级选在业务低峰期升级后盯 24 小时监控。6.4 备份与灾难恢复cert-manager 的状态主要存在两个地方一是 K8s 里的各种 CR 资源二是 ACME 账号私钥和签发的证书 Secret。备份策略定期备份cert-managernamespace 下的所有 Secret尤其是账号私钥。备份所有 Certificate、Issuer、ClusterIssuer 资源定义用kubectl get -o yaml导出。如果集群整个挂了恢复时先装 cert-manager再恢复账号私钥再 apply 资源定义证书会自动重新签发。这里有个细节ACME 账号私钥如果丢了重新注册账号也能用但之前签发的证书的续期关系会断需要重新走一遍签发流程。所以账号私钥一定要备份。7. 一些容易被忽略的细节和我的个人习惯聊几个文档里不太写、但实际很影响体验的点。第一Certificate 的secretName命名要有规范。我习惯用服务名-tls的格式一眼能看出是给谁用的。集群里证书多了之后命名混乱会让你排查时抓狂。第二renewBefore不要设得太短。有人设成 7 天觉得够用。但如果续期那几天正好赶上 DNS 服务商抽风、或者速率限制7 天根本不够排查。30 天是相对安全的缓冲。第三给 Certificate 打标签。用labels标记所属团队、环境、用途配合kubectl get certificate -l teamxxx能快速筛选。这个习惯在证书数量上到几十张之后价值巨大。第四定期审计证书清单。我每个月会跑一次kubectl get certificate -A导出所有证书检查有没有孤儿证书对应的服务已经下线但证书还在续期、有没有即将过期但状态异常的。这个动作能提前发现很多隐患。第五测试续期流程。别等真到期了才发现续期有问题。可以手动删掉 Certificate 关联的 Secret触发重新签发验证整个流程是否通畅。这个操作在 staging 环境做生产环境慎用。最后说个我踩过的坑有一次集群网络策略调整cert-manager 的 Pod 访问不了 DNS 服务商的 API导致所有证书续期失败。但因为续期是提前 30 天开始的当时没告警等到发现时已经有一批证书过期了。从那以后我给 cert-manager 加了专门的网络策略审计和续期失败告警任何一次续期失败都会立即通知。证书管理这件事核心不是配好而是持续可观测。配好只是开始能及时发现它出问题才是真正的稳定。