TT Lab
开始
学习 学习路径 课程

证书已经续期,浏览器却还显示旧证书

签发正常、只在续期时出错的配置

在 TT Lab 中继续学习

一句话总结

ACME 是这样一种协议:通过 HTTP 或 DNS 证明“我控制着这个域名”,CA 就会签发证书,而 cert-manager 在集群内代替我们完成这种证明和续期。有一种配置错误是签发一切正常,却只在续期时出问题,我在家庭实验室中就真的遇到过。

为什么需要它

证书靠人工续期的年代,到期事故频发。答案是缩短有效期并自动续期,而要做到这一点,CA 需要一种不靠人就能确认域名控制权的方法。RFC 8555(ACME)就是这套规约。问题在于,自动化会造出看起来像成功的失败。首次签发很顺利。之后改了一项网关配置。没有任何症状——因为证书还有两个月才到期。直到续期的时候,挑战才会失败,而那时已经没有人会想起那次配置变更。

工作原理

ACME 客户端用账户密钥向 CA 下订单(order),CA 为每个域名提供一个挑战(challenge)。HTTP-01 要求 GET http://<도메인>/.well-known/acme-challenge/<token>(占位符为域名)时,正文必须与密钥授权(key authorization)——token || '.' || base64url(Thumbprint(accountKey))——相同。RFC 规定这个请求要发往 TCP 80,并写明验证服务器可以(SHOULD)跟随重定向。按 Let's Encrypt 的说明,实际实现最多跟随 10 层重定向,但只会前往 http:、https: 的 80、443 端口,而且在被 HTTPS 弹回时不验证证书。DNS-01 是把由账户密钥推导出的值放进 _acme-challenge.<도메인>(占位符为域名)的 TXT 记录中,而通配符证书只能通过 DNS-01 获取。

GET /.well-known/acme-challenge/zk3r9Qm...   Host: www.lab.internal
200 OK
zk3r9Qm....Q7pVw2x...          ← token.thumbprint, 이것이 본문 전체

cert-manager 把这一流程做成了控制器。Issuer/ClusterIssuer 持有与 CA 的约定(账户密钥、solver),Certificate 资源则声明想要的证书——secretName(存放结果的 Secret)、dnsNames、issuerRef(要在其他命名空间中使用,就用 kind: ClusterIssuer)、duration(默认 90 天)、renewBefore。按文档所述,duration 和 renewBefore 是 Go 的时间字符串,所以只能使用 h、m、s,不能使用 d(天),并且 duration 必须大于 renewBefore。如果不写 renewBefore,就会在所签发证书寿命的 2/3 处续期,而实际寿命可能比请求的更短,所以比起绝对值,更推荐使用 renewBeforePercentage。HTTP-01 solver 在挑战期间会创建临时的 Ingress 或 HTTPRoute,把挑战路径导向 solver Pod,而文档写明,使用 Gateway API 时,这个路由必须挂在带有 80 端口监听器的 Gateway 上。

在现场相遇的样子

这是我在家庭实验室中遇到的案例。我在网关(Cilium Gateway)上开启了 HTTP→HTTPS 重定向。网站运行得很好。可是这个重定向把 /.well-known/acme-challenge/ 请求也弹回了 HTTPS,而 HTTPS 监听器中没有 solver 路由,所以结果是 404。签发早已完成,因此没有症状,直到距到期 30 天的续期时才会悄悄失败。“solver 路由的路径更具体,不是应该胜出吗”,我真的测了一下——启动一个模仿 solver 的 Exact 路径 HTTPRoute,访问那个路径,结果是重定向胜出(在 Cilium 1.20.1 上的实测)。路径的具体程度并不优先。修复的办法是把重定向交给应用来做,并把挑战路径作为例外,而常态检查只有一行——如果 curl -sI http://<도메인>/.well-known/acme-challenge/x | head -1(占位符为域名)返回 301,续期就被阻塞了,返回 404 或 200 则正常。因为没有文件而返回 404 没关系。只要不是重定向就行。

还有一点。由于这个配置还从未经历过续期,所以我没有等到到期,而是在 Certificate 的 status 中设置 Issuing 条件,强制触发了续期。几秒钟后新的 CertificateRequest 就生成了,判定依据是用 openssl s_client 取回的证书的日期发生了变化。自动化必须有实际的签发历史,才值得信赖。

下一项实验要做什么

在 Pod 内启动同时带有 HTTP 和 HTTPS 监听器的边缘服务(/opt/app/edge.py)和 HTTP-01 验证服务器(/opt/app/acme_va.py)。放好密钥授权文件,让验证以 VALID 通过;然后开启重定向,重现 301 → 404 导致 INVALID 的事故;再只把挑战路径设为例外,保持重定向的同时重新得到 VALID。编写用来查看续期是否被阻塞的 probe.sh,用评分器的服务器接受双向检查,并用 duration、renewBefore、ClusterIssuer 编写 cert-manager 的 Certificate 清单。