证书自动续期成功了,为什么线上还是老的
ACME 自动续期跑通了,浏览器却依旧提示证书即将过期。这类问题多数不在签发环节,而在部署与生效环节。本文按现场排查顺序梳理签发成功、但线上未更新的常见原因和核验方法。
本文目录

凌晨两点,告警群弹出一条"证书还有 7 天过期"。你打开 ACME 客户端的日志,续期任务明明三天前就跑成功了,新证书也安安静静躺在磁盘上。再看线上,浏览器拿到手的还是那张旧证书。
这是证书自动化里最常见的一类误会:签发成功不等于部署生效。ACME 协议的职责到"把新证书给你"为止,之后怎么让 nginx、负载均衡器或者 CDN 真的用上它,是另一段路。排查时别盯着续期日志反复看,按下面这条链路走一遍,问题基本能定位。
先看磁盘上的证书是不是新的

拿到证书文件,直接用 openssl 看一眼有效期:
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -dates -subject确认 notAfter 确实是续期后的日期。如果这里还是旧的,那问题在签发侧:可能是续期条件没触发、验证失败但任务没报错,或者你看错了证书路径——有些客户端续期后会写入新的目录,软链没跟上。把 ACME 客户端当次运行的完整输出翻出来,重点看域名验证(challenge)那几步是否真的通过。
再看线上实际返回的是哪张证书

磁盘是对的,不代表线上是对的。直接从外部视角核验:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -dates -fingerprint-servername 不能省。一台机器上挂了多个域名时,不带 SNI 看到的往往是默认 server 块的那张证书,容易造成"明明换了为什么没变"的假象。对比这一步和本机证书的指纹:不一致,说明服务根本没加载新文件。
最常见的坑:reload 没发生

证书是 nginx 启动或 reload 时读进内存的。文件换了,进程没 reload,它就一直用着旧的那张,稳如老狗。
所以 ACME 客户端的续期钩子(deploy hook / post hook)里必须有 reload 动作,比如 nginx -s reload 或 systemctl reload nginx。排查时确认三件事:
- 钩子里写的路径和续期输出的证书路径是同一个;
- 钩子真的被执行了——很多客户端只在"实际发生了续期"时才触发钩子,手动
--force-renewal之外的空跑不会触发; - reload 的用户权限够不够,容器场景里宿主机上的钩子能不能触达容器内的进程。
顺手补一个加固动作:reload 之前先 nginx -t,避免新证书路径写错导致 reload 失败、而旧进程又因为别的原因退出,那就从"证书没更新"升级成"站点直接挂了"。

证书不在源站时,链路更长
如果 TLS 终止在 CDN、负载均衡或网关上,源站 nginx 看到的证书和访客看到的完全是两回事。这时续期脚本跑在源站,做得再对也影响不到边缘节点——新证书必须上传到对应平台的证书管理接口,并确认已绑定到正确的域名或监听器。多节点、多region 的平台还要留意生效是否有延迟,别刚传完就下结论。
在 CertbotX 中实践
上面这段链路——签发、reload、上传、确认——本质上是把三件事串成一个闭环。CertbotX 的做法是:通过 DNS-01 完成域名验证(支持把 _acme-challenge 用 CNAME 委托到专用解析域,主域 DNS 不用给自动化程序开权限),ACME 签发后在到期前自动重新签发,然后把新证书自动推送到 Agent、EdgeOne 或 CDN 等部署目标。每次部署都有记录可审计,排查"到底是哪一环没生效"时,先看部署记录里对应目标的状态,比挨个登机器敲 openssl 快得多。

一个便宜的兜底监控
自动化做得再好,也建议留一条独立的外部检查:定期从站外拉一次证书有效期(就是上面那条 s_client 命令的脚本化版本),剩余天数低于阈值就告警。它验证的是"用户实际看到的证书",和内部链路任何一环的故障都解耦。签发侧、部署侧、平台侧哪边出问题,这条告警都兜得住。
参考文档: