续期明明显示成功,浏览器里却还是旧证书:从 deploy hook 缺失排查起
证书自动续期返回 success,线上证书却没换,这是自动化运维里最常见的"假成功"。本文梳理从证书文件、Web 服务加载状态到 renew 钩子配置的完整排查路径,并说明如何把重载动作固化进续期流程,避免每 90 天踩一次。
本文目录

凌晨三点,告警群弹出一条消息:某域名证书还有 5 天到期。你登录服务器一看,Let's Encrypt 的续期任务上周就跑过了,日志里白纸黑字写着 Congratulations, all renewals succeeded。再看浏览器,证书有效期确实没变。
这个场景坑过不少人。问题几乎从来不在 ACME 签发那一侧,而在"签发完之后发生了什么"。
先分清三件事
排查前把链路拆开,别混在一起猜:
- ACME 服务器有没有签发新证书;
- 新证书文件有没有落到磁盘上;
- Web 服务(Nginx、HAProxy 等)有没有重新加载它。

renew succeeded 只保证前两件。第三件如果没配置,进程内存里持有的还是启动时读进去的旧证书——文件是新的,服务用的是旧的,两边都对,就是拼不到一起。
排查路径
先确认磁盘上的文件是不是真的换了。直接看 live 目录下证书的时间戳和有效期:
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -dates
ls -l /etc/letsencrypt/live/example.com/如果 notAfter 是新的日期,说明签和写都正常。接着验证线上实际在用什么:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates两边日期对不上,结论就清楚了:服务没 reload。这时候 systemctl reload nginx(或对应服务)一次,线上立即恢复——但这只是止血,不是修复。
一个小坑:如果站点前面还挂着 CDN 或负载均衡,s_client 探到的是边缘节点的证书,不是源站的。这种情况下源站证书续了、边缘证书没同步,排查时很容易误判方向。先确认请求链路里每一层的证书归属再动手。
把 reload 固化进续期流程
Certbot 支持 deploy hook,只在证书真实更新后执行,比全局 post hook 更干净:
certbot renew --deploy-hook "systemctl reload nginx"
注意这会写进该证书的 renewal 配置(/etc/letsencrypt/renewal/example.com.conf),之后每次续期自动生效,不需要每次手动加参数。也可以把脚本放进 /etc/letsencrypt/renewal-hooks/deploy/ 目录,对所有证书生效。
改完之后务必干跑一次验证整条链路:
certbot renew --dry-rundry-run 会走真实的 ACME staging 流程,能提前暴露 hook 报错、权限不足这类问题。别等 90 天后线上告警才发现脚本里写错了路径。
另一个容易忽略的点:reload 失败的兜底。如果 Nginx 配置里还有别的语法错误,reload 可能静默失败或让进程停在旧配置上。deploy hook 脚本里最好加一步 nginx -t 前置校验,失败时让续期任务以非零状态退出,让监控系统能抓到,而不是假装一切正常。

在 CertbotX 中实践
如果域名 DNS 解析本身不在你能直接操作的账号下(比如 DNS 托管在另一个团队的 Cloudflare 账号里),CertbotX 支持 DNS-01 的 CNAME 委托:把 _acme-challenge 记录 CNAME 到一个你控制的验证域,续期时自动完成 TXT 记录写入,不需要每次人工加记录。签发完成后,CertbotX 会在到期前自动重新签发,并把新证书部署到 Agent 托管的服务(含执行 reload 类动作)、EdgeOne 或其他 CDN,部署动作和结果都有记录可查。这对前面说的"源站续了、CDN 没换"场景尤其有用——部署链路是同一个系统串起来的,少了人肉同步这一环。当然,首次接入时仍建议手动验证一次端到端效果,确认每一层都换上了新证书。
一个判断标准
证书自动化做得好不好,不看续期成功率,看的是:续期到生效之间,有没有人必须记得做某个动作。只要还存在"记得 reload 一下""记得同步到 CDN"这种依赖记忆的步骤,它就迟早会在某个凌晨失效。把每一步都写进 hook 或部署流水线,让记忆失业,这事才算完。
---