Kubernetes集群HTTPS证书自动化:cert-manager DNS01实战指南 如果你维护的 Kubernetes 集群已经进入了多个团队共用的阶段HTTPS 证书一定是你绕不过去的一件事。cert-manager 是这个场景里最常用的自动化证书管理组件而 ACME DNS01 是它几种验证方式里最“省心”也最“麻烦”的一种。说它省心是因为只要域名的 DNS 解析权限在你手里它就能帮你签出包括泛域名在内的证书说它麻烦是因为它需要你对接 DNS 服务商的 API调试步骤比 HTTP01 多不少。这篇内容我就围绕 cert-manager 的 DNS01 方式从原理、环境准备、配置落地到故障排查完整走一遍适合正在搭集群内部 HTTPS、或者想给内网服务签发通配符证书的开发、运维同学参考。1. cert-manager ACME 与 DNS01这套机制到底解决了什么问题1.1 没有自动证书时集群 TLS 有多痛苦先讲一个很典型的场景。集群里的服务越来越多Ingress 网关也搭起来了但证书还是手工申请、手工上传、手工续期。三个月一换的证书每次都要找域名管理员加记录找安全的人审批再更新到集群的 Secret 里。赶上节假日证书过期线上直接跳出红色警告整个团队跟着一起折腾。更尴尬的是有些系统跑在内网公网访问不到用常规的 HTTP 验证方式根本拿不到证书。还有一些服务需要泛域名证书比如*.example.com方便新环境临时起个子域名就用但手工申请通配符证书的审核流程更长很多人干脆就放弃泛域名一个服务一个证书维护成本直接翻倍。cert-manager 就是用来终结这种痛苦的。它在 Kubernetes 里以控制器的形式运行监听 Certificate 这个自定义资源然后自动向 ACME 服务发起证书申请、完成域名验证、把签好的证书连同私钥写进 Secret。整个过程从申请到落地都是声明式的你只要描述“我想要一张包含哪些域名的证书”剩下的它自己搞定到期之前还会自动续期。1.2 HTTP01 与 DNS01 的差别选型背后的考量ACME 协议里最常见的两种域名所有权验证方式是 HTTP01 和 DNS01。HTTP01 是让 ACME 服务器访问你域名的 80 端口获取一个临时文件内容以此证明你控制该域名。它配置简单大多数新手的第一个证书都是这么签下来的但也有很明显的短板。首先你的服务必须能被公网访问而且 80 端口要对外开放否则验证请求根本过不来。其次HTTP01 只适合验证单个具体的域名签发不了泛域名证书因为 ACME 服务器无法预先知道你要给哪个子域签发不可能通过 HTTP 去访问一个尚不存在的地址。再有如果你的域名解析在 DMZ、内网环境里HTTP01 基本走不通。DNS01 则完全不同。它利用“谁能修改域名 TXT 记录谁就拥有该域名”的原理要求你在待验证域名的_acme-challenge子域下创建一条 TXT 记录内容由 ACME 服务器返回。验证方去查询公网 DNS 的这条记录匹配上了就认为你对域名有控制权。这样一来它不需要开放任何入站端口不要求域名解析必须对应到某台公网服务器而且理论上对任意子域名、泛域名都有效。对比项HTTP01DNS01验证原理访问域名 80 端口获取临时文件查询指定 TXT 记录内容是否需要开放入口是必须 80/443 可达否完全不需要是否支持泛域名否是是否需要 DNS 服务商 API否是接入复杂度低中高典型适用场景公网站点、快速验证内网服务、泛域名、无法暴露端口所以当你面对“内网集群、没有公网入口、需要泛域名证书、不想手工管续期”这四个条件时DNS01 基本上就是唯一解。它多出来的那点复杂度主要花在对接 DNS 服务商的 API 凭证上这个后面细说。1.3 DNS01 验证流程拆解一条 TXT 记录的完整旅程如果只看官方文档DNS01 的流程会被描述得很抽象真正跑起来你还是要清楚每一步的触发关系。我把整条链路拆开看第一步你创建了一个 Certificate 资源里面写了域名列表和签发者引用。cert-manager 控制器发现这个资源后会向 ACME 服务器发起一个 Order 请求。这个 Order 里会列出需要验证的所有域名。第二步ACME 服务器为每个域名生成一个随机验证串并通过 Challenge 资源返回给 cert-manager。对于 DNS01验证串的内容就是需要写入 TXT 记录的那段值。第三步cert-manager 根据你配置的 DNS 服务商凭证调用 API 在_acme-challenge.example.com下创建 TXT 记录。这一步是关键如果 API 凭证权限不够、域名不在该账号下或者服务商接口返回异常整条链路就卡在这里。第四步cert-manager 等待 DNS 记录在全球生效。它会反复查询这条 TXT 记录确认能查到之后再通知 ACME 服务器“可以验证了”。第五步ACME 服务器自己去公网 DNS 查询这条 TXT 记录内容匹配则验证通过。随后签发证书把证书和私钥以 Secret 的形式写回 Kubernetes。第六步验证完毕后cert-manager 会调用 DNS 服务商 API 把临时的 TXT 记录清理掉。你会发现整条链路里最核心的环节就是第三步和第四步后者尤其容易出问题。因为 DNS 记录从写入到能被公共递归服务器查到存在一个传播窗口不同服务商、不同 TTL 下差异很大。我在实际集群里跑的时候90% 的 DNS01 报错都集中在第四步的“等待传播”上后面我会专门写一个故障排查章节。2. 环境准备与安装版本、集群和 DNS 凭证缺一不可2.1 安装 cert-manager 前先核对这几件事在动手安装之前建议先花几分钟把环境捋一遍否则后面排查问题会很痛苦。首先是 Kubernetes 版本。cert-manager 的版本发布节奏相对固定老版本对 Kubernetes 版本有最低要求。以目前常见的 1.14/1.15 系列为例要求 Kubernetes 1.22 以上基本上没问题不过最好还是以官方文档的兼容矩阵为准别在一个老旧的 1.18 集群上硬上最新版很容易出现 CRD 结构不兼容的情况。其次是 Helm 工具。虽然也可以直接用 manifest 安装但我强烈建议用 Helm因为后续升级、调参都方便。Helm 版本最好 3.8 以上太低了对某些模板函数支持不够。然后是域名和服务商的核对。你要确认目标域名确实托管在某个支持 API 管理的 DNS 服务商上并且你有权限创建 API 凭证。如果是企业内部的 DNS 服务器需要先确认它是否提供可编程接口、是否允许自动创建 TXT 记录很多传统内部 DNS 并不支持这种操作那就只能考虑自建 CA 或者换域名托管方案了。最后是网络连通性。cert-manager 需要访问 ACME 服务器通常是 443 端口这个出站流量要放行。同时它查询 TXT 记录时会使用集群内的 DNS 配置如果集群 DNS 无法解析公网域名或者内网 DNS 有拦截就需要额外配置递归域名服务器参数这个坑我后面也会专门讲。2.2 Helm 安装 cert-manager 的完整过程安装过程本身不复杂但几个参数要理解清楚。我一般会在集群里单独开一个 cert-manager 命名空间和业务命名空间隔离方便权限控制和日志查看。安装命令大致如下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 \ --version v1.14.4这里有两个地方值得多说一句。installCRDstrue是让 Helm 顺便把 cert-manager 所需的 CRD 装好如果你不设置这个参数CRD 不会自动加载之后创建 ClusterIssuer 或 Certificate 资源时会直接报“找不到类型”之类的错误。另一个是版本号建议固定到一个明确版本而不是用latest或者不写版本。生产环境里latest等于把版本决定权交给安装当天一旦上游发版行为变化你很难追踪到底装的是哪一版。安装完成后确认一下组件状态。正常情况下会有三个 Deploymentcert-manager、cert-manager-cainjector、cert-manager-webhook全部 Running 才算就绪。kubectl get pods -n cert-manager kubectl get crd | grep cert-manager如果你的集群里跑了 NetworkPolicy 或者 PSP还要额外放行 cert-manager 所在的命名空间。我之前在一个安全加固过的集群里部署直接默认拒绝所有入站流量导致 webhook 组件和 kube-apiserver 通信失败证书签发和资源校验全部异常排查了很久才定位到是网络策略的问题。2.3 DNS 服务商凭证怎么准备才安全DNS01 需要一个能操作 DNS 记录的 API 凭证。不同 DNS 服务商提供的凭证类型不一样有的是 Access Key ID 加 Secret有的是 API Token有的还分全局权限和子账号权限。我这里不具体点名某个厂商但原则是通用的权限最小化、独立凭证、别用管理员账号。权限最小化意味着这个凭证最好只能操作你要用来验证的那些域名的 DNS 记录不要给它全局所有资源的权限。ACM 验证只需要创建、查询、删除 TXT 记录所以理论上只给 DNS 写权限就够了。有的服务商还支持限定 Zone 范围那就把条件缩窄到对应域名上哪怕多花一步配置也值得。独立凭证的意思是不要拿你平时登录控制台或者操作其他云资源的主凭证来用。DNS01 凭证会以明文形式存放在 Kubernetes Secret 里一旦集群被攻破泄露的就只限于 DNS 记录操作权限影响面可控。我之前见过有人把拥有全产品线权限的密钥放进集群等于把整个云账号的钥匙送给了摸到 Secret 的人这是最典型的反面案例。准备好凭证之后你可以先用服务商的控制台或者命令行工具手动创建一条测试用的 TXT 记录确认凭证能正常读写。这一步能节省后面大把排查时间因为很多“cert-manager 报权限错误”的问题根子其实是凭证本身就没配好。3. 核心配置实战从 Secret 到 Certificate 的完整链路3.1 创建 DNS 凭证 Secret避开一个常见误区凭证要通过 Kubernetes Secret 传给 cert-manager这个 Secret 一般放在 cert-manager 命名空间里因为 Issuer 或 ClusterIssuer 引用它的时候默认会去自己所属的命名空间找。样例如下apiVersion: v1 kind: Secret metadata: name: dns-credentials namespace: cert-manager type: Opaque stringData: api-token: 你的 API Token这里有一个很多人第一次都会踩的误区直接用kubectl create secret generic --from-literalapi-tokenxxx。在测试环境这么干没问题但在生产环境Shell 的历史记录里会留下完整的 Token。更安全的做法是用上面的 YAML 文件保存文件本身落在只有你能读的目录里然后kubectl apply -f提交。stringData 字段在写入集群后也会被 kubectl 处理成 base64 的 data不会以明文出现在集群对象里。还要注意一点type: Opaque就够了不需要也不应该设置成 Kubernetes 的kubernetes.io/tls类型。TLS 类型的 Secret 期望里面有tls.crt和tls.key两个字段用来存证书数据而 DNS 凭证只是给 cert-manager 引用的普通密文混用类型会造成语义混乱以后查看的时候容易误解。3.2 ClusterIssuer 配置详解与通配符域名支持凭证就绪后下一步是定义一个 ClusterIssuer。它是集群级别的签发者整个集群任意命名空间都能引用不需要每个命名空间单独建一份。我这里以某个支持 API Token 的 DNS 服务商为例配置如下apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: acme-dns01-prod spec: acme: email: opsexample.com server: https://acme-v02.api.letsencrypt.org/directory privateKeySecretRef: name: acme-issuer-account-private-key solvers: - dns01: apiTokenSecretRef: name: dns-credentials key: api-token字段层面email 是 ACME 账户的联系邮箱如果证书申请出了问题证书机构会通过这个邮箱联系你。server 是 ACME 服务地址我写的是公网标准生产端点实际使用中如果你的证书机构是其他服务商就换成它提供的目录地址。privateKeySecretRef 这里容易误解它保存的是 ACME 账户的私钥不是最终证书的私钥。ACME 协议要求每个账户有一个注册私钥用于后续和证书机构通信的身份认证。cert-manager 会自动生成并保存到这个名字对应的 Secret 里不需要你手动创建。solvers 是核心它声明了用什么样的方式解决域名验证。dns01 下面的 apiTokenSecretRef 指向我们上一节创建的凭证。值得一提的是如果你同时管理多个域名其中一些要用 DNS01、另一些要用 HTTP01或者不同域名属于不同 DNS 服务商就可以在 solvers 数组里声明多个条目并用 selector 做匹配。比如solvers: - selector: dnsZones: - example.com dns01: apiTokenSecretRef: name: dns-credentials key: api-token这样设置后只有 example.com 及其子域才会走这个 DNS01 方案。多域名、多服务商的场景下selector 是避免误用凭证的关键工具。3.3 提交 Certificate 并确认证书签发成功ClusterIssuer 做完最后一步就是创建 Certificate 资源。拿泛域名证书举例apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: example-com-tls namespace: prod spec: secretName: example-com-tls-secret dnsNames: - example.com - *.example.com issuerRef: name: acme-dns01-prod kind: ClusterIssuer这里我把裸域名 example.com 和泛域名*.example.com放在同一张证书里。ACME 服务器会对每个域名分别做验证DNS01 模式下都会去_acme-challenge.example.com创建 TXT 记录只是验证内容可能不同。一张证书同时覆盖裸域和所有子域是集群内部比较实用的做法。提交之后查看证书状态kubectl -n prod get certificate kubectl -n prod describe certificate example-com-tls正常情况下过几十秒到几分钟Certificate 的状态会变成 Readydescribe 的 Events 里会依次出现 Order、Challenge 成功之类的记录。如果一直卡在 pending 状态再看 Challenge 和 Order 对象kubectl -n prod get challenges kubectl -n prod describe challenge challenge-nameCertificat 签发成功后你需要的 Secretexample-com-tls-secret会出现在 prod 命名空间里里面包含tls.crt和tls.key。可以用下面的命令直接检查证书有效期kubectl -n prod get secret example-com-tls-secret \ -o jsonpath{.data.tls\.crt} | base64 -d | openssl x509 -noout -dates看到notBefore和notAfter时间正确说明整条链路已经打通了。3.4 让 Nginx Ingress 和 Deployment 用上新证书证书签出来最终是要给业务用的。最直接的用法是 Ingress 引用这个 SecretapiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example-web namespace: prod spec: tls: - hosts: - example.com - *.example.com secretName: example-com-tls-secret rules: - host: example.com http: paths: - path: / pathType: Prefix backend: service: name: example-web-svc port: number: 80Ingress 控制器会自动加载 Secret 里的证书并监听 443 端口这个是大多数人都熟悉的路径。但还有一个隐藏的坑也是我实际踩过的如果你的业务 Pod 不是通过 Ingress 终止 TLS而是在 Deployment 里直接挂载证书文件那么证书续期后Secret 更新了但挂载该 Secret 的 Deployment 不一定能在不重启的情况下加载新证书。Kubernetes 里Secret 作为卷挂载更新后 kubelet 会同步更新容器里的文件但你要是把证书值通过环境变量注入到 Pod或者应用初始化时才读取一次证书那它不会自动重载。我在某个服务里就遇到过证书明明已是新的应用却还在用旧证书握手排查半天才发现是应用缓存了旧证书内容。解决办法要么是给 Deployment 加一个 Reloader监听 Secret 变更并触发滚动重启要么在业务侧处理证书热加载。生产环境我建议用 Reloader 这类工具简单省事kubectl annotate deployment example-web \ secret.reloader.stakater.com/reloadexample-com-tls-secret这样每次证书 Secret 更新Deployment 都会自动重启证书文件必然是最新的。4. 实测问题排查DNS01 最常见的 5 类故障4.1 TXT 记录“明明看到了还是报 pending”DNS01 最常见的故障是 Challenge 一直 pending日志里出现类似 “waiting for DNS record to propagate” 的提示。你手动去查 TXT 记录发现记录明明已经在 DNS 服务商的控制台里出现了但 cert-manager 就是认为还没生效。这里的原因通常是 cert-manager 使用的 DNS 解析路径和你手动查询的路径不一致。它默认会用集群内置的 DNS 配置去递归查询。如果你的集群 DNS 采用了内网解析、或者转发规则覆盖了你的业务域名那么它查询到的 TXT 记录可能不是你刚创建的那条甚至干脆查不到。解决方式是给 cert-manager 指定公共递归 DNS 服务器让它不依赖集群 DNS。可以在 Helm 安装时通过 extraArgs 传入--set extraArgs{--dns01-recursive-nameservers8.8.8.8:53,1.1.1.1:53}用公共 DNS 作为查询源传播检查的结果才更接近公网实际状态。注意这里要写成IP:53的格式多个服务器用逗号分隔。还有一类情况是 DNS 服务商写入记录和真正可查询之间存在很长的传播窗口特别是 TTL 设得很大的时候。我建议把托管域名的默认 TTL 调低一些比如 60 秒这样验证时传播更快如果你不能在服务商侧调整 TTL也可以给 cert-manager 加--dns01-propagation-delay120增加等待时间但这是治标不治本还是优先排查解析路径和 TTL。4.2 DNS 服务商 API 报错与权限配置第二种常见问题是 cert-manager 在创建或删除 TXT 记录时API 返回 403、401 或权限不足之类的错误。错误信息里一般会带上 HTTP 状态码看到 401 通常就是凭证无效或者 Token 过期403 则是凭证有效但权限范围不含目标域名。我在排查这类问题时第一件事是先确认凭证在服务商控制台里能否手动创建 TXT 记录。比如你有个测试域名先手动加一条_acme-challenge.test.example.com看能否成功再删掉。如果手动操作都不行那就是域名没托管在这个账号下或者 Zone 权限没关联跟 cert-manager 没关系。如果手动可以而 cert-manager 不行就检查 ClusterIssuer 中 solvers 的配置是不是匹配了错误的 DNS 服务商。尤其是你在多个域名、多个服务商之间切换的时候selector 写错会导致 cert-manager 拿着 A 服务商的凭证去调 B 服务商的 API报错内容千奇百怪。建议在 ClusterIssuer 里把 selector.dnsZones 明确写出来不给它“猜”的机会。4.3 泛域名、CAA 记录与私网 DNS 的取舍泛域名证书是 DNS01 最吸引人的特性但它也引出了几个细节。首先是 CAA 记录。CAA 是用来声明“哪些证书机构可以为该域名签发证书”的 DNS 记录。如果域名的 CAA 记录里没有包含你使用的 ACME 证书机构签发会被拒绝报错类似CAA record for domain prevents issuance。解决办法是在域名的 DNS 配置里加一条 CAA 记录指定允许的证书机构。例如example.com. CAA 0 issue letsencrypt.org这样证书机构就能确认自己有权为这个域名签发证书。对于泛域名证书CAA 检查是覆盖裸域和子域的所以记得挂在主域名上。另一个容易搞混的点是私网域名和公共 ACME 的关系。ACME 的 DNS01 验证要求 ACME 服务器能通过公网 DNS 查询到 TXT 记录。如果你的业务域名完全跑在内部 DNS 里没有公共权威解析公共 ACME 服务器无法验证证书就签不下来。这种情况建议改用 cert-manager 的 CA Issuer 模式在集群内自建证书体系或者把域名放到公网托管服务商上专门做解析。不要指望 DNS01 能穿透完全封闭的内网 DNS这在原理上就过不去。4.4 证书续期失效与 Secret 不更新的处理cert-manager 会在证书过期前自动续期默认大约是有效期还剩三分之一时开始。以 90 天证书为例大约在第 60 天就会发起续期。如果发现证书没自动续先检查 Certificate 对象的状态和事件kubectl -n prod get certificate kubectl -n prod describe certificate example-com-tls常见原因包括ACME 服务器端对同域名重复签发有频率限制导致续期 Order 失败DNS 服务商凭证过期或权限被回收以及 ClusterIssuer 对应的 ACME 账户私钥 Secret 被误删账户重新注册后可能触发限制。如果确认 Certificate 状态正常但 Secret 里的内容没变多半是时间还没到续期点。你可以调整spec.renewBefore来提前续期时间spec: renewBefore: 720h比如 720 小时30 天想让证书提前一个月续期就这么写。别忘了即使证书 Secret 更新了前面讲过的 Deployment 重启问题也要一并考虑否则“证书续期成功但业务还在用旧证书”的现象依然会出现。4.5 排查工具和日志定位的完整思路DNS01 出问题时一个清晰的分层排查思路能省很多时间。我一般按“资源状态 → 事件 → 控制器日志 → DNS 记录实测”四步走。第一步看资源状态kubectl get certificate,order,challenge -n prod直观判断是 Order 没创建、Challenge 未通过还是 Certificate 已经 Ready。第二步看事件kubectl describe里的 Events 区域会给出错误摘要很多问题到这一步就已经有明确指向。第三步看 cert-manager 控制器的日志kubectl -n cert-manager logs deployment/cert-manager --tail200日志里会包含向 ACME 服务器请求的细节、API 调用的错误码以及 DNS 查询的超时信息。最后一步才是去实测 DNS 记录dig short TXT _acme-challenge.example.com如果手动能查到但 cert-manager 一直 pending回到 4.1 讲过的递归解析问题。如果手动也查不到说明 API 创建记录这一步就没成功回到 4.2 检查凭证和服务商配置。这套流程走下来大部分问题都能在十分钟内定位。5. 实操心得一套可以复用的安全基线5.1 我现在的标准做法在几个集群里反复折腾之后我总结了一套相对稳定的落地方式。新环境一律先装一个 staging 用的 ClusterIssuerACME 服务地址用测试端点专门用来验证配置正确性避免在正式证书机构上浪费签发次数和频率配额。等 staging 证书成功签出来再把 ClusterIssuer 切到生产端点或者在测试确认没问题后直接改成生产地址。DNS 凭证方面我现在会为每个集群单独创建一组 API Token限定只允许操作业务域名所在的 Zone。Secret 里的凭证字段名也会写入到文档里避免半年后接手的人对着 YAML 猜到底是api-token还是api-token-key。这种小细节平时没人注意出问题时会成为排查路上的一座大山。证书对象统一放在业务命名空间命名规范固定为“域名 tls”比如example-com-tls。Ingress 和 Deployment 引用 Secret 时就通过这个约定的名字直接关联。同时所有 Deployment 都接入了 Reloader证书一更新就自动滚动重启省去人工干预也避免旧证书被应用缓存的问题。5.2 一些补充建议如果条件允许我很推荐给证书加一套监控。不用太复杂用 Prometheus 的 blackbox_exporter 定期访问一个使用该证书的 HTTPS 端点或者写一个小脚本读取 Secret 里的证书有效期并推送告警。cert-manager 自身也有 metrics 暴露给 Prometheus可以监控certmanager_certificate_expiration_timestamp_seconds。我一般会设一个提前 14 天的告警万一自动续期失败还有足够时间人工介入。升级 cert-manager 时也别大意。它包含多个 CRD版本升级后 CRD schema 可能有变化建议先在有 staging 环境的小集群上验证再滚动升级生产集群。操作前备份一下 cert-manager 的配置资源尤其是 ClusterIssuer、Certificate、Secret万一升级过程中账户私钥丢失恢复起来会轻松很多。最后还有一个很多人忽略的点DNS01 不只适用于公共证书机构。如果你想把内部的私有 CA 也自动化管理起来cert-manager 的 CA Issuer 和自签名 Issuer 同样能派上用场但那是另一套玩法了。DNS01 适合的是有公网域名、想拿可信证书、又希望全自动化的场景。按我上面的步骤走一遍再结合你自己的网络环境微调参数这方案基本可以在团队里稳定跑上几年。