状态:草案(2026-10-26),已反映实测数据。 实测于 2026-10-26 在 TT Lab 集群(cert-manager 1.21 系列)中通过 只读 方式获取。
kubectl get·logs,通过openssl x509 -noout -dates读取 Secret 的证书日期,以及通过openssl s_client访问公共地址。公开名称和内部对象名称已隐去(<공개 이름 A>,<클러스터 이슈어>)。官方文档和 RFC 在同一天打开原文进行了核对。 编写规则:以实测数据为准,未能测出的记为“未实测”,无法确认的记为“未确认”,推测记为“推测”。
目录
- 1. 人工续期证书时会发生什么
- 2. 五个对象与驱动它们的控制器
- 3. ACME:证明“这个域名确实属于你”的协议
- 4. 谁来签名:SelfSigned、CA、ACME
- 5. 何时续期
- 6. Webhook 与 cainjector:为什么会有三个组件
- 7. 与网关和 Ingress 的对接方式:shim
- 8. 实测:从 TT Lab 集群中读取的数据
- 9. 常见故障与排查顺序
- 10. 总结
- 参考资料
1. 人工续期证书时会发生什么
Let's Encrypt 证书的有效期为 90 天。这意味着每隔三个月,就必须有人查看日历并执行续期命令,而这个人总会有忘记的时候。一旦证书过期,浏览器就会显示警告,服务之间通过 TLS 通信的连接也会完全中断。依赖人工记忆的做法注定终将失败。
cert-manager 用符合 Kubernetes 风格的方式解决了这个问题。用户只需通过对象编写 期望状态,即“此类证书应始终存在于此 Secret 中”。至于其余的工作——比如生成密钥、向证书颁发机构(CA)请求、证明域名所有权、写入 Secret 以及在过期前重新获取——则由控制器持续循环运行来达成。这与编写 Deployment 就会自动创建 Pod、死后会重新拉起的原理如出一辙。
本文将逐层剖析这一“达成状态”的过程。首先了解会按什么顺序生成哪些对象(第 2 节)、与 ACME 服务器交互的内容(第 3 节)、由谁来签名(第 4 节)、何时续期(第 5 节)、Webhook 和 cainjector 为什么独立存在(第 6 节)以及如何与网关对接(第 7 节),最后以 TT Lab 集群的实际读取状态、一次事故(第 8 节)以及常见故障(第 9 节)收尾。
2. 五个对象与驱动它们的控制器
cert-manager 共有六种 CRD。其中用户直接使用的只有两个(Issuer 和 Certificate),其余的都是控制器为了拆分工作而创建的中间对象。
| 对象 | 范围 | 创建者 | 包含内容 |
|---|---|---|---|
Issuer / ClusterIssuer |
命名空间 / 集群 | 用户 | 谁来签名:ACME(Let's Encrypt 等)、CA、SelfSigned、Vault、外部 Issuer |
Certificate |
命名空间 | 用户或 shim | 期望的证书:名称列表、Issuer、Secret 名称、寿命与续期时间点、密钥类型 |
CertificateRequest |
命名空间 | 控制器 | 一张 CSR。带有修订版本号,经批准(Approved)后签名 |
Order |
命名空间 | 控制器(仅限 ACME) | ACME 服务器的一个订单及其 URL |
Challenge |
命名空间 | 控制器(仅限 ACME) | 针对一个名称的一项所有权证明 |
最短的使用示例如下:一个通过 Let's Encrypt 的 HTTP-01 进行验证、并将验证路径通过 Gateway API 的 HTTPRoute 开放的 ClusterIssuer,以及一个期望使用该 Issuer 签发证书的 Certificate。
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata: { name: <클러스터 이슈어> }
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef: { name: <ACME 계정 키 Secret> } # 계정 키는 여기 한 번 만들어 계속 쓴다
solvers:
- http01:
gatewayHTTPRoute:
parentRefs: [{ kind: Gateway, name: <게이트웨이>, namespace: <네임스페이스> }]
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata: { name: <인증서> }
spec:
secretName: <인증서 Secret> # 결과가 쓰일 자리
dnsNames: [<공개 이름 A>]
issuerRef: { kind: ClusterIssuer, name: <클러스터 이슈어> }
状态机:谁来决定“现在是否需要签发”
Certificate 的状态可以通过两个条件(Condition)来读取。 Ready 表示存在 Secret、未过期、密钥对匹配且名称与规范一致。 Issuing 则显示为“正在重新获取”,在需要时附加,签发结束后由 issuing 控制器移除(官方 API 文档)。
控制器并非单一整体,而是分工合作的多个组件。按顺序来看包含以下几个:
- trigger:对比 Certificate 与 Secret,如果判断“需要重新获取”,则附加上
Issuing=True。实测日志中记录的原因有两个:Secret 完全不存在时的DoesNotExist,以及 Secret 的私钥与当前请求不同的SecretMismatch(第 8 节)。当到达续期时间点时,也会在同一位置开始签发。 - keymanager:首先在 临时 Secret 中创建用于下一次证书的私钥。Certificate 的
status.nextPrivateKeySecretName即为其名称,而在实测中,CertificateRequest 的注解cert-manager.io/private-key-secret-name中保留了带有诸如<인증서>-xxxxx这样随机数后缀的名字。不在 Secret 中直接写入密钥的原因是,若签名失败,不至于破坏当前正在使用的密钥与证书对。从 v1.18 版本开始,密钥轮换策略的默认值从Never变更为Always,每次续期都会创建新密钥(1.18 发布说明)。 - requestmanager:使用该密钥生成 CSR,并创建一个 CertificateRequest。名称末尾的数字即为修订版本(
<인증서>-1,-2, …). - approver:为 CertificateRequest 附加
Approved。默认批准者是 cert-manager 自身,也可以通过策略工具(approver-policy)进行更改。未经批准,任何 Issuer 都不会进行签名。 - 各 Issuer 签名器:SelfSigned 和 CA 会当场签名。ACME 则会创建 Order 并等待结果。CertificateRequest 一旦失败便不会重试,重试由 Certificate 侧以新的修订版本发起(官方文档)。
- issuing:将变为 Ready 状态的 CertificateRequest 中的证书与临时 Secret 中的密钥配对写入真正的 Secret 中,并移除
Issuing。 - revisionmanager:清理过期的 CertificateRequest。从 v1.18 开始,
revisionHistoryLimit的默认值为 1,因此只保留最后一个(实测中每个证书也确实仅对应一个 CR)。
由于所有控制器都试图同时修改同一个对象,“由于乐观锁将其重新加入队列”的日志会频繁打印。在单次实测签发中,类似这样的行出现了六次,且全部立即重试成功,没有产生任何问题。这是 Kubernetes 控制器的正常现象,无需惊慌。
3. ACME:证明“这个域名确实属于你”的协议
像 Let's Encrypt 这样的公共 CA 不会去核实人的身份。它们唯一核实的是: 你是否真的能够控制这个域名 。规范该流程的正是 ACME(RFC 8555)。
账户、订单、授权、挑战
- 账户。 客户端创建一个账户密钥(非对称密钥)并将其注册到 ACME 服务器。此后的所有请求都是用此密钥签名的 JWS。在 cert-manager 中,ClusterIssuer 的
privateKeySecretRef即为该密钥,而 Issuer 的Ready条件则表示“账户已注册”(实测:The ACME account was registered with the ACME server). - 订单(order)。 即“我希望为这些域名签发证书”的请求。根据 RFC 8555 §7.1.6,订单初始为
pending,当所有授权都变成valid时变为ready,提交 CSR 后变为processing,签发完成后变为valid。如果在任何阶段失败,则为invalid。 - 授权(authorization)与挑战。 每个域名都有一个授权对象,服务器会提供多种获取该授权的方法(挑战)。实测订单中提供了
http-01·dns-01·tls-alpn-01三种使用相同 token 的方法,而 cert-manager 仅选择 Issuer 中指定的解析方式(此处为 HTTP-01)。cert-manager 使用的方式仅有 HTTP-01 和 DNS-01 两种。 - finalize 与下载。 当所有授权都变为 valid 后,将 CSR 发送到
finalizeURL(§7.4),并获取签发的证书链(§7.4.2)。
key authorization:用一个 Token 证明什么
并不是只需将服务器给出的 token 放到网页上就行。如果那样的话,窥探到 token 的第三方也可以做同样的事情。RFC 8555 §8.1 要求在 token 后面附加 账户密钥的指纹。
keyAuthorization = token + "." + base64url(SHA-256(계정 공개키 JWK 의 정규형)) # 지문은 RFC 7638
HTTP-01: http://<이름>/.well-known/acme-challenge/<token> 이 keyAuthorization 을 돌려준다 (§8.3)
DNS-01 : _acme-challenge.<이름> 의 TXT = base64url(SHA-256(keyAuthorization)) (§8.4)
我们使用 Python 标准库确认了计算结果确实如此。指纹的输入采用 RFC 7638 §3.1 中的示例 RSA 公钥,token 则使用 RFC 8555 示例中出现的值。
import base64, hashlib, json
b64u = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=").decode()
jwk = {"e": "AQAB", "kty": "RSA", "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWh…"} # RFC 7638 예제(줄임)
thumb = b64u(hashlib.sha256(json.dumps(jwk, separators=(",", ":"), sort_keys=True).encode()).digest())
ka = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA" + "." + thumb
print(thumb, b64u(hashlib.sha256(ka.encode()).digest()))
结果(2026-10-26,本地 python3):指纹为 NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs ,与 RFC 7638 中记录的期望值一致,该 Token 的 DNS-01 TXT 值则为 ZTRx1Ckl1-tM05o5zaizTTA0yUy5AGereMgSNWC6Ll8 。由于指纹来源于账户密钥,因此只有持有账户密钥的一方才能生成正确的响应。
HTTP-01 与 DNS-01,该如何选择
| HTTP-01 | DNS-01 | |
|---|---|---|
| 验证位置 | 端口 80 的 /.well-known/acme-challenge/ |
_acme-challenge.<이름> TXT 记录 |
| 是否需要从外部访问 | 是。必须能够从公网通过端口 80 访问 | 否。只需公开 DNS 即可 |
泛域名(*.example.com) |
不支持 | 支持 |
| 集群应具有的权限 | 开放单个路径的权限(解题 Pod、路由) | DNS 提供商 API 密钥(通常可以更改该域的整个范围) |
| 容易出问题的地方 | 防火墙、重定向、集群内发夹式路由(Hairpin) | 传播延迟、委托(CNAME)设置 |
通配符和端口 80 条件已在 Let's Encrypt 官方文档(挑战类型)中确认。由于 DNS-01 的 API 密钥意味着“能够彻底更改 DNS 域的钥匙”将进入集群内部,因此安全成本很高。如果只有几个公开名称且端口 80 已开放,则 HTTP-01 会更简单。
cert-manager 如何解决 HTTP-01
Challenge 控制器为每个名称 解题 Pod(acmesolver 镜像)、Service 以及用于打开该路径的路由(Ingress 或 Gateway API 的 HTTPRoute)。然后,它不会立即通知 ACME 服务器“已准备就绪”,而是进行 自我检查(self check) 。它直接在集群内部使用该公开名称进行 GET 尝试,并且仅在返回正确的 key authorization 时才发出通知。这是一种旨在避免在 ACME 服务器上积累失败的机制。因为验证失败被限制为每个名称/账号每小时 5 次(Let's Encrypt 速率限制)。相反,如果阻止了从集群内部流出并返回到其自身公共地址的路径(发夹式路由),则自我检查将永远无法通过。我们将在第 9 节中再次探讨这一点。
4. 谁来签名:SelfSigned、CA、ACME
公开 CA 并不是唯一的 Issuer。对于像集群内的 Webhook 服务器这样 只需信任我们这一侧客户端 的证书,私有 CA 就足够了。
- SelfSigned:使用自己的密钥对自己的证书进行签名。由于接收方相信的依据只有证书本身,因此它通常用于首次创建 CA 的用途。
- CA:使用 Secret 中包含的 CA 密钥进行签名。官方文档推荐的引导形态是:“使用 SelfSigned ClusterIssuer 创建
isCA: true根 Certificate,然后由 CA Issuer 使用该 Secret”。
kind: ClusterIssuer
metadata: { name: bootstrap }
spec: { selfSigned: {} }
---
kind: Certificate
metadata: { name: root-ca }
spec:
isCA: true
commonName: example-root-ca # 기본은 빈 subject 이니 이름을 준다(공식 문서의 경고)
secretName: root-ca
issuerRef: { kind: ClusterIssuer, name: bootstrap }
---
kind: ClusterIssuer
metadata: { name: internal-ca }
spec: { ca: { secretName: root-ca } }
私有 CA 真正的难点不在于签名,而在于 信任分发。必须将根证书放入所有客户端的信任列表中,并且在更换根证书时,必须使它们同时信任新旧证书。官方文档为此目的引入了 trust-manager,并警告说运营 PKI 需要轮换、分发和恢复计划。还有一种 csi-driver 可以将证书直接挂载到 Pod 中而无需 Secret。
5. 何时续期
spec.duration 的默认值为 90 天, spec.renewBefore 的默认值为 寿命的 1/3 。也就是说,如果未填写任何内容,则会在 寿命的 2/3 节点处进行续期。从 v1.16 开始,还支持按比例编写的 renewBeforePercentage (官方文档,1.16 发行说明)。如果由 CA 确定寿命(如 ACME),则它不是根据请求的 duration 计算,而是根据 实际收到的证书的 notBefore 和 notAfter 计算。实测值将原样显示出来。
| 证书(2026-10-26 读取) | notBefore → notAfter | 寿命 | renewalTime | 距过期剩余时间 |
|---|---|---|---|---|
| 公共证书 A(Let's Encrypt,无设置) | 09-09 12:09:29 → 12-08 12:09:28 | 90 天 − 1 秒 | 11-08 12:09:28 | 30 天(1/3) |
| 公共证书 B(Let's Encrypt,无设置) | 10-08 23:15:09 → 01-06 23:15:08 | 90 天 − 1 秒 | 12-07 23:15:08 | 30 天(1/3) |
自签名(duration: 2160h·renewBefore: 360h 中填写的) |
08-23 08:45:34 → 11-21 08:45:34 | 90 天 | 11-06 08:45:34 | 15 天 |
| 自签名(用于 Webhook,无设置) | 10-18 → 01-16 | 90 天 | 12-17 | 30 天(1/3) |
所有时间均为 UTC,且 kubectl get certificate -o custom-columns=… 的 status.notBefore·notAfter·renewalTime 与 Secret 的 tls.crt 在 openssl x509 -noout -startdate -enddate 读取的日期彼此相同。经计算,公共证书的 renewalTime 正好是 notAfter − 30일 。
公共 CA 侧的变化
Let's Encrypt 于 2025-12-02 公布了缩短寿命的时间表。默认配置文件将在 2027-02-10 变为 64 天,在 2028-02-16 变为 45 天,并且权限重用期也将从 30 天缩短至 10 天,再缩短至 7 小时。公告断言,像“每 60 天一次”这样以固定间隔续期的方式已不再足够。按比例(2/3)续期的 cert-manager 默认值顺应了这一变化,但 renewBefore: 720h 等 编写了固定值的地方,在 45 天证书中续期余量将减少至 15 天。 还有由 CA 告知“何时续期”的 ARI (RFC 9773)。cert-manager 在 v1.21 中将其置于 ACMEUseARI 功能门控之后进行实验性引入,默认处于关闭状态。
6. Webhook 与 cainjector:为什么有三个组件
安装 cert-manager 后会生成三个 Deployment。在 TT Lab 集群中,controller、webhook 和 cainjector 也分别在各自运行。
- controller:第 2 节中的控制器都包含在此进程中。
- webhook:这是 API 服务器在存储 cert-manager 对象之前调用的服务器。官方文档列举了三项工作:规范验证(彻底阻止存储错误的 Certificate)、填充默认值以及在 API 版本之间进行转换。由于 Webhook 使用 TLS 与 API 服务器通信,因此 Webhook 自身也需要证书。为了避免先有鸡还是先有蛋的问题,webhook 不使用 cert-manager 的签发路径,而是自行创建 CA 并将其置于 Secret 中(实测:该 CA 从 2026-08-19 起为期 1 年)。
- cainjector:填充 Webhook 设置(Validating/MutatingWebhookConfiguration)、CRD 的转换 Webhook 以及 APIService 的
caBundle字段。如果在目标对象上附加了cert-manager.io/inject-ca-from: <네임스페이스>/<Certificate>注释,它将读取并注入该 Certificate 的 CA,并在证书更改时重新注入。在 TT Lab 集群中,各种组件的验证/变更 Webhook 设置和 CRD 也通过此注释获取了 CA。
如果没有 cainjector,在 Webhook 服务器证书更新当天,API 服务器将无法信任新证书,导致 该类对象的创建全部失败。这就是 Webhook 证书更新极易成为静默故障的原因。
7. 如何与 Gateway/Ingress 衔接:shim
无需逐个编写 Certificate,也可以在 Gateway 侧仅添加一个注释。
- ingress-shim:
cert-manager.io/cluster-issuer(或issuer)查看带有注释的 Ingress,并为每个tls.secretName创建 Certificate。 - gateway-shim:查看带有相同注释的 Gateway,并通过 HTTPS 监听器的
hostname和certificateRefs创建 Certificate。此功能默认关闭。它在 v1.15 中进入 Beta 并通过--enable-gateway-api启用,而在 v1.21 中则通过配置文件中的gatewayAPI.enabled启用(旧名称enableGatewayAPI虽然仍能运行但已被弃用)。我没有找到表明其为 GA 的官方表述。
在 TT Lab 集群的 controller 配置文件中包含旧名称 enableGatewayAPI: true ,而 Gateway 上则有 cert-manager.io/cluster-issuer: <클러스터 이슈어> 注释。读取其结果可以很好地展现 shim 的特性。
- 公共证书 A 的 Certificate 的
ownerReferences是 Gateway 。这意味着它是 shim 创建的对象,如果删除 Gateway,它也会随之删除。 - 公共证书 B 是人工通过
kubectl apply单独创建的 Certificate。由于同一个 Gateway 上存在 B 的监听器,因此 shim 也试图用该 Secret 名称创建 Certificate,但在签发期间留下了“不修改无主 Certificate”的日志且未予干涉(实测,2026-10-09 日志 10 行)。这是防止 shim 覆盖人工创建内容的的安全机制。不过,由于这表明正在通过两条路径管理同一个名称,因此最好将其精简为一端。
Gateway 何时能察觉证书的变更
Gateway(基于 Envoy)会监视 Secret 并在其更改时重新读取。它不会重启代理。经实测确定的路径如下。TT Lab 集群的 Cilium Gateway 将所引用的 TLS Secret 复制到其专属命名空间中作为带有哈希名称的副本。公共证书 A 的副本于 2026-08-19(首次签发)创建,但内部包含的是在 2026-09-09 重新获取的证书。它没有创建新副本,而是在原地进行了续期。从外部通过 openssl s_client 进行连接确认时,公共地址提供的证书的 notAfter 也与 Dec 8 12:09:28 2026 相同。
相比之下,将 Secret 作为文件挂载且 仅在启动时读取一次 的应用程序则有所不同。官方文档指出,对于此类应用,新证书不会被立即使用,并推荐使用 kubectl rollout restart 或 Reloader 等工具。这是第 9 节中陷阱最常见的一种。
8. 实测:从 TT Lab 集群中读取的内容
环境:cert-manager 1.21 系列(Helm Chart,各 1 个 controller·webhook·cainjector)。读取日期 2026-10-26,所有时间均为 UTC。
| 项目 | 值 |
|---|---|
| Issuer | 用于公共证书的 ClusterIssuer(ACME、HTTP-01、Gateway API 解题)以及各组件的 SelfSigned Issuer |
| Certificate | 几个公共证书以及多个用于 Webhook/内部 TLS 的自签名证书 |
| Order·Challenge | Order 对于每个公共证书都以最后一个 valid 的形式保留,而 Challenge 不存在 —— 成功后会被删除 |
| 公共证书的密钥 | RSA 2048,签名 sha256WithRSA。签发者为 Let's Encrypt 的中间 CA |
首次获取一个新名称时:6 分 5 秒
首次创建公共证书 B(2026-10-09)的 controller 日志保留在上一个容器日志(kubectl logs --previous)中。按时间顺序列出这些行如下。
| 时间 | 发生的事情 |
|---|---|
| 00:07:36.141 | trigger: Certificate must be re-issued,原因 DoesNotExist(无 Secret)→ Issuing |
| 00:07:36.332 | 创建 CertificateRequest 修订版本 1·Approved |
| 00:07:38.209 | 创建解题 Pod |
| 00:07:38.274 | 创建用于解题的 HTTPRoute(以 Gateway 为父级) |
| 00:07:48 – 00:13:28 | 重复检查解题 Pod·Service·HTTPRoute 是否存在(前 10 秒,后 20 秒间隔,共 23 次) |
| 00:13:41.037 | certificate issued,CR Ready |
| 00:13:41.075 | Certificate Ready=True |
准备工作在 2 秒内完成,其余 6 分钟都在等待。至于在等待什么,即自我检查为何通过较晚,并未留在该日志阶段(--v=2)中,因此属于 无法确认。由于这是一个新创建的公开名称,因此推测可能是在等待 DNS 传播。订单对象中原样保留了服务器提出的三个挑战,并且权限的初始状态为 pending 。
再次获取同一个名称时:4 秒
公开证书 A 的修订版本 2 于 2026-09-09 13:07:57 生成 CR,并在 13:08:01 变为 Ready。 4秒。查看订单对象就能找到原因。权限的初始状态为 initialState: valid ,挑战列表中只有 http-01 一个。我们在 21 天前(首次签发)已经获取过该名称的权限,且 Let's Encrypt 会将权限重用 30 天,因此既不需要测试 Pod 也不需要进行自检。如果按照上述计划将这个重用期限缩短到 10 天 7 小时,这种捷径将几乎不复存在。至于修订版本 2 为什么会生成,由于没有残留日志而 无法确认,我们只知道 Certificate 的 generation 为 3,说明其规范曾经被修改过。
notBefore 早于签发时间
两个公开证书的 notBefore 都比 CR 变为 Ready 的时间准确早了 58 分 32 秒(A:13:08:01 对比 12:09:29,B:00:13:41 对比 23:15:09)。关于为了防止时钟较慢的客户端将新证书拒绝为“尚未生效”,Let's Encrypt 会将 notBefore 提前约 1 小时的解释,仅在 Let's Encrypt 社区论坛的回复中找到,在官方文档中未能确认。
事故:一个 Secret 有两个主人会导致数千次重新签发
集群内某个组件所使用的 Webhook 证书(自签名)的修订版本号超过了 1 万。从 2026-10-18 Certificate 被创建到死循环停止, 在 73 分钟内重新签发了约 1 万次(平均每秒约 2.7 次)。剩下的日志只有最后 1 分钟左右,仅在该时间段内就重复了 300 多次相同的行。
"Certificate must be re-issued" … reason="SecretMismatch"
message="Secret contains a private key that does not match the current CertificateRequest"
签发 → 写入 Secret → 随即出现“Secret 的 Key 与当前请求不符” → 再次签发,这是一个死循环。 这意味着有人在覆盖 cert-manager 写入的 Secret。情况是这样的:当天该组件从“自己的控制器自行创建 Webhook 证书”的安装方式过渡到了“由 cert-manager 创建”的方式,并且 这两种方式使用了相同的 Secret 名称。 可以推测,以旧配置启动的控制器继续将自己的证书写入同一个 Secret,从而导致了死循环。由于死循环停止的时间与该控制器重启的时间相差几分钟,因此无法确认究竟是什么中断了死循环。由于是自签名证书,所以没有触及外部限制,但 如果是 ACME 签发机构,在头几分钟内就会触及 Let's Encrypt 的每周限制(同一名称集合 5 张),从而导致长达一周左右无法签发。
教训很简单: 一个 Secret 只能有一个主人。 另外, revision 通过监控诸如数字或 CertificateRequest 生成速度等“平时几乎不怎么变动的值”,可以提早发现这种死循环。
9. 常见故障与排查顺序
① 挑战无法完成。 从上到下逐一排查 describe 。在哪一个对象处停滞,故障原因就在哪里。
kubectl -n <ns> describe certificate <이름> # Issuing 이 붙어 있나, 메시지
kubectl -n <ns> get certificaterequest,order,challenge # 어디까지 생겼나
kubectl -n <ns> describe challenge <이름> # Reason: 자체 점검 실패 문구가 여기 있다
curl -v http://<이름>/.well-known/acme-challenge/<token> # 바깥에서 직접
HTTP-01 常见的三个原因是:80 端口在外部被阻断、将所有 HTTP 强制重定向到 HTTPS 的规则先生效于验证路径、以及 回环(Hairpin)。最后一种情况是,集群内部通往自己公网地址的流量被路由器或负载均衡器阻断,导致从外部可以访问,但 cert-manager 自身的检查失败。在路由器后的小型集群中尤为常见。如果是 DNS-01,则首先检查 TXT 传播延迟和委派(CNAME)设置。
② 触发了速率限制(rate limit)。 Let's Encrypt 当前的限制是:每个注册域名 7 天 50 张、 同一名称集合 7 天 5 张、每个名称/账户验证失败每小时 5 次、每个账户新订单 3 小时 300 个(官方 rate limit 文档)。同一名称集合的限制每 34 小时才恢复一张。Let's Encrypt 和 cert-manager 的教程都建议在测试配置时必须先使用 staging 服务器。如果第 8 节中的死循环发生在 ACME 上,就会直接触发此限制。
③ 证书已更换,但应用仍在使用旧证书。 代理会自动重新加载,但在启动时仅创建一次密钥库的应用(例如在启动脚本中创建 Java PKCS12 密钥库的应用等)必须重启。请同时记住,如果将 Secret 以 subPath 挂载,kubelet 是不会更新文件的。解决方法是使重启自动化(如使用 Reloader 等),或者让应用监听文件变动并重新加载。
④ 一个 Secret 有两个主人。 即第 8 节中的事故。检查其他工具(Chart 自带的证书生成器、手动创建的 Secret、其他控制器)是否使用了相同的名称。 SecretMismatch 如果不断重复且 revision 持续上升,基本上就是这个问题。
⑤ Webhook 证书更新日的故障。 如果 cainjector 停止运行或目标对象的注解错误,caBundle 就会停留在旧的 CA,导致所有经过该 Webhook 的请求都因 TLS 错误而失败。对比 Webhook 配置中的 caBundle 指纹与 Secret 的 ca.crt 即可一目了然。
⑥ shim 与手动创建的 Certificate 发生冲突。 如第 7 节所述,shim 不会干涉没有主人的 Certificate。如果“添加了注解但证书没有改变”,请检查是否存在同名的手动创建的 Certificate。
10. 总结
- cert-manager 是一个由多个控制器分工协作,将记录了“所需证书”的 Certificate 转换并同步为 Secret 的系统。中间对象(CertificateRequest·Order·Challenge)既是工作阶段,也是故障发生时指示卡在哪里的路标。
- ACME 通过密钥授权(令牌 + 账户密钥指纹)来证明“能够控制该名称”。HTTP-01 虽简单但受限于 80 端口和回环,而 DNS-01 虽然支持泛域名,却需要付出 DNS API 密钥的代价。
- 默认情况下,续期发生在生命周期的 2/3 节点。由于公共 CA 的寿命缩短到了 64 天和 45 天,请找出写死了固定期限的配置并进行修改。
- 实测显示,签发新名称耗时 6 分 5 秒,重用权限重新签发耗时 4 秒;而当两个控制器分别将同一个 Secret 视为自己的主人时,则产生了在 73 分钟内重新签发约 1 万次的死循环。
蒂蒂小贴士。 证书就像是网球拉力赛。如果传球的人有两个,比赛就无法结束。一个 Secret 只能有一个主人!而且必须在“生命周期的三分之二”(而不是“每三个月”)时进行交接,这样即使球的飞行时间变短了(45 天!)也不会漏接。
参考资料
- RFC 8555, Automatic Certificate Management Environment (ACME). §7.1.3·§7.1.6 (Order Status), §7.4 (finalize), §8.1 (key authorization), §8.3 (HTTP-01), §8.4 (DNS-01). https://www.rfc-editor.org/rfc/rfc8555.html
- RFC 7638, JSON Web Key (JWK) Thumbprint. §3.1 Example. https://www.rfc-editor.org/rfc/rfc7638.html
- RFC 9773, ACME Renewal Information (ARI). https://www.rfc-editor.org/rfc/rfc9773.html
- cert-manager 文档: Certificate (Renewal Defaults & Restart Guide) https://cert-manager.io/docs/usage/certificate/ · API Reference https://cert-manager.io/docs/reference/api-docs/ · CertificateRequest https://cert-manager.io/docs/concepts/certificaterequest/ · ACME Orders and Challenges https://cert-manager.io/docs/concepts/acme-orders-challenges/ · Webhook https://cert-manager.io/docs/concepts/webhook/ · CA Injector https://cert-manager.io/docs/concepts/ca-injector/ · Ingress https://cert-manager.io/docs/usage/ingress/ · Gateway https://cert-manager.io/docs/usage/gateway/ · SelfSigned https://cert-manager.io/docs/configuration/selfsigned/ · trust-manager https://cert-manager.io/docs/trust/trust-manager/
- cert-manager 发行说明 1.15 (Gateway API Beta) · 1.16 (renewBeforePercentage) · 1.18 (Key Rotation Default Always, revisionHistoryLimit 1) · 1.21 (ARI Experimental, Configuration Name). https://cert-manager.io/docs/releases/
- Let's Encrypt: Challenge Types https://letsencrypt.org/docs/challenge-types/ · Rate Limits https://letsencrypt.org/docs/rate-limits/ · Staging https://letsencrypt.org/docs/staging-environment/ · “From 90 to 45” (2025-12-02) https://letsencrypt.org/2025/12/02/from-90-to-45
- 关于 notBefore 提前的说明(非官方文档):Let's Encrypt 社区论坛 https://community.letsencrypt.org/t/certificate-not-valid-before-dated-1-hour-prior-to-generation/47246
- 所有原文均于 2026-10-26 打开并核实。