Kubernetes实战:cert-manager自动签发HTTPS证书ACME HTTP-01全流程

📅 发布时间:2026/10/16 15:06:08
Kubernetes实战:cert-manager自动签发HTTPS证书ACME HTTP-01全流程
一个内部系统要上Kubernetes第一件事不是怎么把Pod跑起来而是HTTPS证书从哪来。手动去CA站点申证书、然后上传Secret、再定时续期这条路在K8s环境里基本没法走尤其是服务多了以后证书到期那几天简直像在拆炸弹。所以我的方案很直接在集群里部署cert-manager走ACME协议的HTTP-01验证方式让证书自动签发、自动续期全程不用手工干预。这篇文章就是这次实战的完整记录从ACME验证原理讲到Issuer配置再到最后证书落地的每一步。如果你也准备在Kubernetes里用cert-manager签发证书按着这个流程走基本不会踩坑。1. 实战背景与方案选型为什么选 cert-manager ACME HTTP-011.1 项目要解决的问题这个项目本身不复杂一个Web应用要部署到Kubernetes集群对外通过Ingress暴露HTTPS入口。但问题跟着就来了证书用谁的怎么签签完怎么更新之前我见过不少团队是手工买证书、手工上传结果就是证书过期时一脸茫然。更麻烦的是K8s里一个Secret可能被多个Ingress引用你更新证书时一旦漏掉某个Ingress应用就直接打不开了。所以在这次架构设计时我的底线就两条一是证书必须自动签发二是必须自动续期。通过人工写定时任务去轮询、再执行命令更新Secret虽然也能实现但属于过度设计而且不稳定。这时候cert-manager就派上用场了。它是专门为Kubernetes设计的证书管理控制器声明式地管理TLS证书能和Ingress、Gateway等对象无缝集成。引入它之后我只需要声明“我要这个域名的一张证书”剩下的验证、签发、存储、续期都由控制器自己完成。1.2 为什么是cert-manager而不是手工签发有人可能会问既然都用Kubernetes了那直接买商业证书、手动创建Secret不也行吗行是行但只能用在证书量极少、变更极不频繁的场景。一旦服务拆分多了域名几十个每个都要单独申请、单独上传这种工作没有任何技术含量还容易出错。手工签发还有一个隐患就是私钥的管理——私钥只应该存在于服务端和CA系统里如果某个人下载了私钥又通过聊天工具传来传去那把私钥基本上就算泄露了。用cert-manager就不一样私钥生成、签名请求发送、私钥和证书写入Kubernetes Secret整个流程都在集群内部完成。没有任何人需要接触到私钥私钥也不会离开集群环境。从安全角度看这是采用cert-manager最核心的理由。另外它的声明式模型和Kubernetes的理念完全一致你只需要描述最终状态控制器会不断调和让实际状态等于期望状态。证书快到期了它自动发起续期Secret被误删了它重新生成Ingress写错导致验证失败它会通过Event和Status把问题反馈出来。1.3 为什么选HTTP-01验证与DNS-01、TLS-ALPN-01的取舍ACME协议里常见的有三种验证方式HTTP-01、DNS-01、TLS-ALPN-01。我选HTTP-01核心原因只有一个配置简单不需要额外权限。HTTP-01要求CA服务商能通过HTTP访问到你的域名下的一个特定路径也就是http://域名/.well-known/acme-challenge/token。为了让这个请求成功cert-manager会在Ingress上临时插入一段路由把验证请求转发给一个临时创建的验证PodPod里存储着token。只要你的域名已经解析到了Ingress的公网入口80端口对外可访问这套流程就通了。DNS-01则是通过在DNS记录里添加TXT记录来验证域名所有权适合无法开放80端口的场景但前提是你必须能控制DNS服务商并且还要配置对应的API凭据如果DNS服务商不在cert-manager的Webhook支持列表里就得自己写Webhook。对于一台在公网有80端口开放、有Ingress控制器、域名解析指向集群的普通场景HTTP-01是最省事的。TLS-ALPN-01要占用443端口并且需要证书链支持在通用性上不如HTTP-01。说白了就是能用HTTP-01解决的事情就不要折腾DNS API。我在这次实战里用到的就是HTTP-01和ClusterIssuer覆盖绝大多数Web服务场景足够用了。2. 环境准备与安装从零把cert-manager跑起来2.1 运行环境要求动手之前先确认环境。我这里用的是某云厂商的托管集群Kubernetes版本是1.28ingress-nginx控制器已经正常运行。cert-manager对Kubernetes版本比较挑剔太老的集群建议先升级再装。具体版本兼容性可以看该项目的文档但大致上Kubernetes 1.22以上都能比较安全地使用近期版本。还需要准备好两个命令行工具kubectl和helm。我安装cert-manager用的就是Helm因为升级、回滚都方便而且可以自定义资源限额和参数。要确保kubectl能连上集群并且你有足够的RBAC权限创建Namespace、CRD、ClusterRole等集群级资源。除此之外DNS解析必须提前做好。这一步看似基础但很多人在这翻车。我指的不只是域名要有A记录而是这个记录必须指向Ingress控制器实际监听的那个公网IP。如果Ingress Controller是部署在NodePort或者负载均衡器后面就得先把域名解析到对应的负载均衡器IP上。否则ACME验证请求根本到不了你的集群。2.2 安装cert-manager安装流程分两步。第一步是创建资源定义CRD这步通过Helm就能自动完成。我用的是官方仓库先添加仓库然后更新索引helm repo add jetstack https://charts.jetstack.io注意这里引用了某个第三方仓库地址如果你没有配置对应的仓库源可以先通过包管理器导入离线Chart包再执行。实际执行安装时我用了以下命令helm install cert-manager cert-manager \ --namespace cert-manager \ --create-namespace \ --set installCRDstrue \ --set webhook.timeoutSeconds5这一步有几个值得注意的小细节installCRDstrue意味着把CRD一起安装否则后面创建ClusterIssuer会提示找不到资源。我特意把webhook的超时时间设为5秒因为我遇到过Pod启动慢、默认3秒容易超时的情况导致后续资源创建报错。Namespace名我用了cert-manager这已经是行业惯例日志排查时大家都能快速定位。安装完成后建议等待所有Pod进入Running状态kubectl get pods -n cert-manager正常情况下会看到三个Podcert-manager主控制器、cainjector和webhook。cainjector负责注入webhook的CA证书没它的话资源校验功能会不正常。2.3 准备Ingress控制器与DNS解析cert-manager本身不处理HTTP请求的对外暴露HTTP-01验证依赖Ingress控制器。我们项目里用的是nginx ingress-controller类名是nginx。在创建ClusterIssuer时我需要告诉cert-manager“验证请求应该导入到哪个Ingress类”如果不配默认会找名为nginx的IngressClass。如果你用的是其他Type比如traefik或自研的Ingress要把IngressClass名称对应填上。DNS解析我设置的TTL很短因为怕验证失败后改解析记录要等很久。实战中我建议把需要签发证书的域名先解析到Ingress的地址上然后用curl快速测试连通性比如curl -v http://web.example.com这里的响应只要不是连接超时或拒绝连接基本就说明流量已经到Ingress了。至于后端有没有返回正确的业务页面无所谓因为ACME验证只访问/.well-known/acme-challenge/路径。如果这步都不通后面cert-manager怎么折腾都是白搭。我曾经遇到一次域名解析到了NodePort节点IP但节点安全组没放行80端口结果验证请求全部超时排查了半天才发现是安全组漏了条规则。所以提前确认网络可达性非常关键。3. ACME HTTP-01 验证机制拆解3.1 HTTP-01怎么工作ACME HTTP-01验证的原理听起来很简单但里面有几个细节直接决定了你配置对不对。CA机构通过向http://你的域名/.well-known/acme-challenge/在Token这个路径发起HTTP请求如果返回的内容正好等于一个Key Authorization字符串CA就认为你对这个域名有控制权。这个Key Authorization是怎么算的它是Token加上你的账户私钥指纹做一次base64url编码拼接而成。Token是CA随机的指纹是你ACME账户注册时给的。也就是说你的ACME账户私钥对验证结果起着决定性作用。cert-manager把ACME账户私钥存进一个Secret如果这个Secret丢了虽然可以重新注册账户但之前签发的证书不受影响只是后续续期会换一个账户一般没有问题。HTTP-01只支持HTTP80端口不支持HTTPS。这意味着你必须确保从公网访问的80端口没有被封锁并且域名解析不能只指向HTTPS服务而忽略80端口。很多人把整个集群只对外开放443把80端口关闭了HTTP-01就会一直失败。ACME协议文档也从侧面写了这就是一个“使用明文HTTP验证域名所有权”的方法。3.2 cert-manager如何编排验证流程当cert-manager处理一个Certificate证书对象时事情的顺序是这样的检查证书对应的Secret是否存在如果存在并且距离到期日还远就什么都不做。如果证书不存在或即将到期创建一个证书Order对象Order记录着对哪个域名发起签发。Order会拆分成一个或多个Challenge验证对象HTTP-01对应的Challenge类型就是http-01。cert-manager根据Challenge中的Token和Key Authorization生成一个临时的验证响应资源。在Ingress的配置里临时插入一段路由把/.well-known/acme-challenge/token路径指向临时的响应服务。等待CA服务商前来请求这个URL。验证成功后cert-manager把Challenge标记为有效Order完成再通过ACME协议获取最终的证书和私钥保存到目标Secret里。在这个过程中临时验证Pod或者临时Ingress修改是自动创建的验证完成后会被清理掉。因为是“临时插入路由”所以它会修改你现有的Ingress对象。如果你的Ingress上有很多其他规则或者被团队其他人管理这个行为可能会引起注意。解决方式是给Ingress打一个注解cert-manager.io/acme-http01-edit-in-place: true它会让cert-manager直接修改这个Ingress对象而不是创建新的临时Ingress。当然如果你不想污染已有的Ingress可以保持false让cert-manager创建一个全新的Ingress用于验证。3.3 关键资源模型理解了流程再看资源对象就不会乱。cert-manager引入的核心CRD有这么几个IssuerNamespaced资源用于签发证书的配置一般用于某一个Namespace。ClusterIssuer集群级资源任意Namespace下的Certificate都可以引用它。Certificate声明一个证书需求指定SecretName、DNS名称、引用哪个Issuer。CertificateRequestCertificate控制器向Issuer发起请求时的中间对象。OrderACME协议的订单记录签名状态。ChallengeACME协议的验证挑战记录验证进度。在实际排错时大部分人会直接kubectl describe certificate如果发现状态是Pending就要往下层看Order和Challenge。证书签发不是一步到位的而是一个多层嵌套的状态机Certificate→CertificateRequest→Order→Challenge。任何一个环节卡住错误都会反应在下层对象的Status里。这也是新手最迷惑的地方——只看Certificate根本看不出问题必须一层层往下钻。后面排查部分我会给出具体命令。4. 核心配置签发第一张正式证书4.1 创建ClusterIssuer现在到了真正写配置的阶段。我先定义一个ClusterIssuer核心参数包括ACME服务目录地址、联系人邮箱、用于保存账户私钥的Secret名称以及HTTP-01 solver的Ingress类名。apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: public-ca-issuer spec: acme: server: https://acme.example.com/directory email: adminexample.com privateKeySecretRef: name: issuer-account-key solvers: - http01: ingress: ingressClassName: nginx先解释一下里面容易踩坑的点server是ACME协议的目录地址。不同CA机构或同一机构的生产/测试环境这个地址都不一样。千万不要把测试环境的地址复制到生产里否则签出来证书大部分浏览器不认。email主要用于证书过期提醒某些CA机构会向这个地址发送续期失败通知。邮箱填错了不会导致签发失败但你会错过告警。privateKeySecretRef.name会自动生成一个ACME账户私钥Secret。这个Secret非常重要丢了会导致无法跟CA机构重新建立同一个账户。一般不推荐手工修改它。solvers里可以配置多个solver我这里只配了一个http01。也可以按域名选择性使用不同solver这个后面再说。创建完以后查看状态kubectl get clusterissuer public-ca-issuer -o wide这里有一个常见的坑状态字段的Ready显示为True不代表已经注册过ACME账户。它只代表配置被控制器接受并且账户准备成功。你可以在对应Secret里看到私钥文件但账户注册通常是在第一次Certificate签发时自动完成的。4.2 通过Ingress注解自动签发cert-manager最常见的使用方式是给Ingress加上注解让它在Ingress创建或更新时自动生成对应的Certificate和Secret。我这里的Ingress是这样写的apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: web-ingress namespace: default annotations: cert-manager.io/cluster-issuer: public-ca-issuer cert-manager.io/acme-http01-edit-in-place: true spec: ingressClassName: nginx tls: - hosts: - web.example.com secretName: web-tls rules: - host: web.example.com http: paths: - path: / pathType: Prefix backend: service: name: web-svc port: number: 80这个配置里面cert-manager.io/cluster-issuer注解告诉cert-manager使用哪个Issuer发起签发。如果你只想让某个Namespace内使用也可以用cert-manager.io/issuer注解配合Namespaced的Issuer对象。cert-manager.io/acme-http01-edit-in-place这个注解很重要。默认情况下cert-manager会在验证HTTP-01时创建一个临时Ingress验证完再删除。如果你Ingress里已经有大量业务规则临时Ingress可能因为资源冲突导致验证失败所以设置为true让它在当前这个Ingress对象上临时添加验证路由验证完再清理掉这些临时字段。生产环境我几乎都会加上这个注解。SecretNameweb-tls就是之后证书要保存的Secret名称。规则中host: web.example.com必须和TLS块里的host一致否则证书申请和流量路由可能对不上。Ingress创建后等待两三分钟再执行kubectl get certificate -n default如果看到READYTrue说明证书已经签发成功web-tls这个Secret也自动生成了。4.3 使用Certificate对象管理证书虽然Ingress注解很方便但如果证书被多个Ingress使用或者你希望更精细地控制建议直接创建Certificate对象。注解方式在内部其实也是自动创建了一个Certificate对象但在显式场景下更容易管理。下面是我常用的一份CertificateapiVersion: cert-manager.io/v1 kind: Certificate metadata: name: web-tls namespace: default spec: secretName: web-tls duration: 2160h renewBefore: 720h dnsNames: - web.example.com issuerRef: name: public-ca-issuer kind: ClusterIssuer这里的duration和renewBefore表示证书有效期为90天2160小时提前30天720小时开始续期。大多数公共CA签发的证书有效期本身不能超过90天所以你即使写了更长的durationCA的CA证书也会限制实际有效期。写长了也没意义。renewBefore可以设置成证书有效期的一半左右这样即便你的应用在到期前一天才去读取Secret里的新证书也不会出现过期的情况。在cert-manager里续期过程是在后台自动进行的等到Secret里的证书被更新你的Pod如果挂载了该Secret或者Ingress引用了该Secret通常会自动感知但某些进程需要重启才能重新加载证书。实战中我见过不少应用因为不监听Secret变更导致证书明明更新了实际TLS握手还是旧证书。这个问题在Nginx Ingress里是不存在的它会对Secret变化做出反应。4.4 验证与检查证书签发完成后我习惯按下面这套命令来确认所有环节都正常kubectl get certificate web-tls -o yaml kubectl get secret web-tls -o jsonpath{.data.tls\.crt} | base64 -d | openssl x509 -noout -text第一条命令能看到证书状态READYTrue就是成功。第二条命令把Secret里的证书解出来用openssl查看有效期和域名。看一下Not After和Subject Alternative Name基本就能确定证书是否符合预期。除了用kubectl还可以直接通过Ingress域名访问来确认证书链是否完整。比如用curl去请求那个域名curl -v https://web.example.com在输出里会显示“SSL certificate verify ok.”说明证书链是受信任的。如果提示unable to get local issuer certificate那就可能是证书链不完整或者Ingress没有配置好中间证书这个后面排查部分会讲。5. 避坑实录常见问题与排查指南5.1 证书一直Pending怎么办Certificate创建后如果长时间READY状态不是True最常见的原因就是底层资源没有推进。这时候不要上来就怀疑cert-manager没装好先按顺序逐层排查kubectl describe certificate web-tls看Events里有没有异常。通常会有类似“Waiting for certificate request to have completed”的信息这说明Certificate一直在等下层完成。接着看CertificateRequestkubectl get certificaterequest | grep web-tls kubectl describe certificaterequest 对应名称如果CertificateRequest一直处于pending去查Orderkubectl get orders -n default kubectl describe order 对应名称大概率能看到“Waiting on 1 challenge”或“Challenge has failed”的提示。最后再查Challengekubectl get challenges kubectl describe challenge 对应名称排错顺序就是Certificate到底层一层层往下。通常问题的根因都会在Challenge的Events里写得比较清楚。遇到的最多的几种错误是Failed to complete ACME challenge: 404 urn:说明CA请求验证URL时未找到内容基本是临时Ingress路由没生效或者验证Pod启动太慢。Timeout waiting for http-01 challenge说明CA发起请求超时可能是安全组没放行80端口或者域名没解析到位。Failed to construct HTTP-01 solver configuration: Ingress v1 CRD not found说明K8s版本太老或者对应资源没启用。在调试过程中我还会手动检查临时资源是否被创建kubectl get pods -n default -l cert-manager.io/acme-http01-solvertrue kubectl get ingress -n default | grep acme正常情况下这些资源会在验证期间短暂存在验证完成后自动删除。如果你根本看不到它们那多半是配置没有生效。5.2 HTTP-01验证总是失败HTTP-01验证失败的原因很多但绝大多数逃不出这三座大山网络不通、路由没到、密钥不匹配。网络不通的意思就是CA服务商从公网访问不到你的80端口。可以用一个公开网络检测工具模拟一下从公网发起http://域名/.well-known/acme-challenge/的请求看返回状态码。如果连接被拒先检查云平台的安全组和防火墙规则确认80端口是开放的。然后再确认集群Node或者Ingress的负载均衡器监听了80端口。我以前遇到过Ingress Controller因为配置了http-port为8080而80没有监听ACME验证自然一直失败。路由没到是说请求虽然进了集群但没有转发到cert-manager创建的临时Pod上。这种情况多出在IngressClass配置不一致。我ClusterIssuer里写的是nginx但集群里IngressClass实际叫nginx-publiccert-manager创建临时Ingress时用的还是nginx类于是这个Ingress完全不会匹配流量。最简单的方法是把ClusterIssuer里的ingressClassName改成和你实际用的IngressClass一致或者干脆给Ingress打上注解让临时Ingress也可以被识别。密钥不匹配通常指Challenge里的Key Authorization和临时Pod返回的内容不一致。这个概率很低但可能发生在多Issuer并存的情况下。比如你同时配置了多个ClusterIssuer而Ingress注解指定的Issuer和实际Challenge使用的Issuer不一致。排错时看Challenge里的Spec.Key和临时Pod返回的内容确认两者一致就能定位。5.3 续期相关细节ACME签发的证书有效期一般是90天cert-manager默认提前30天续期。续期机制其实和首次签发流程差不多只是Order发起时会复用已有的账号私钥和证书私钥。如果续期失败会反复重试并且在Events里记录原因。这里有个经常被忽略的问题Cert-manager默认使用HTTP-01验证续期所以即使证书已经配置好、你平时只通过HTTPS访问服务续期时依然需要80端口可访问。千万不能在证书到期前为了安全把80端口关掉否则就是断了自己的续期路径。我曾经遇到一次评测环境安全策略要求全站强制跳转HTTPS结果把80端口从安全组里删除了证书到期前两天开始续期失败直接导致服务证书过期。所以除非你改用DNS-01验证否则HTTP-01模式下80端口必须长期对外可用。续期是否成功的监控也比较重要可以定期看kubectl get certificate的输出如果发现某个证书的Ready列变成了False或者Renewing列显示重试次数就要及时介入。也可以设置Prometheus告警通过cert-manager的Metrics暴露的过期时间数据来监控。最简单直接的方式是在日历上给所有证书的到期时间设个提前预警但既然用了cert-manager我更推荐看证书状态的Status条件一旦出现IssuanceFailed立刻处理。另外Secret里的证书更新之后如果你的Ingress Controller是nginx入口它需要重新加载配置才能应用新证书。Nginx Ingress Controller会监听该证书Secret的变化一般几秒内自动完成reload。如果你在Service Mesh或者自研的网关里使用证书就要确认是否有相应的reload机制。5.4 其他值得注意的坑再列几个我实际遇到过的和HTTP-01相关的坑方便大家提前避雷。第一个是“私有CA或内部ACME服务”的信任问题。如果你的集群使用的不是公共CA而是内部搭建的ACME服务那么证书签名链里的根证书可能不在客户端的信任库里。这时候你需要在接入层把根证书注入到系统信任库否则调用方会报x509证书未知签发者。此类问题不属于cert-manager本身的问题但排查起来也很费时间。第二个是多个Ingress对象同时使用同一个域名和同一个Secret。HTTP-01验证时cert-manager会尝试在某个Ingress对象上添加临时路由如果这个Ingress没有匹配到该域名对应的规则或者存在多个Ingress互相冲突验证路由就会乱掉。我建议你规范到一个域名只在一个Ingress主体上配置并且确保Cert-manager只用来处理那一个Ingress的TLS段不要跨多个Ingress管理同一份Secret。第三个是ClusterIssuer里的ACME账户邮箱如果是无效邮箱某些CA机构可能拒绝注册账户导致CertificateRequest一直失败。虽然不是硬性校验但填个格式完全不对的邮箱可能会被拒绝。我习惯用一个真实可用的团队邮箱这样CA如果发告警邮件也能收到。第四个是关于证书私钥权限。cert-manager默认生成的私有密钥存放在Secret里Secret的访问权限由RBAC控制。一定要确保没有给ClusterRole绑定管理员权限否则任何Pod都能读到私钥那也就谈不上什么TLS安全了。也可以为敏感Secret开启KMS加密托管的K8s集群通常有原生的加密能力打开它成本并不高。还有一点假设你在同一个集群里需要同时申请多个域名证书比如一张证书包含多个SAN域名那在Certificate对象的dnsNames里写上所有域名即可。HTTP-01验证会为每个域名分别发起一个Challenge逐个验证。只要有一个域名验证失败整张证书签发就会失败这在包含很多历史域名的场景里特别容易出问题。最后说说关于证书链的问题。公共CA签发的证书通常会在Response里带上中间证书。cert-manager会把证书链完整保存到Secret的tls.crt字段中正常情况不需要额外处理。但如果你发现某个客户端总是报证书链不完整可能是证书链在某个环节被截断了。你可以通过openssl来检查openssl s_client -connect web.example.com:443 -servername web.example.com看输出的证书链数量是否完整。如果只显示了叶子证书那就要检查Ingress Controller是否只读取了tls.crt里的第一块证书或者你在Ingress上手动指定了额外的CA Secret干扰了cert-manager自动注入的证书链。我在实际使用中最大的体会是把HTTP-01验证当成一条“隐形的网线”它能不能通取决于域名解析、安全组、IngressClass、临时Pod这几道关卡同时在线。每一道关卡都可以单独验证不要怕报错报错信息在K8s的Event里几乎都写得明明白白。只要按Certificate→CertificateRequest→Order→Challenge这个链条往下查绝大多数问题都能在十几分钟内定位到根因。如果是在多集群或者生产环境大规模使用我建议在ClusterIssuer里配置多个solver作为fallback比如一个HTTP-01为主、一个DNS-01备用这样即使某个验证通道临时不可用证书续期也能走备份方案。不过这个属于进阶用法需要先搞定核心的HTTP-01流程再来加冗余。这套cert-manager ACME HTTP-01的方案现在已经在我的多个项目里稳定运行。日常不需要任何人去关注证书问题除非有新的域名加入或者Ingress结构发生变化。对于想给Kubernetes里的服务直接上HTTPS的同学从这条路走起基本不会错。