证书明明"自动续期"了,为什么还是到期宕机:一次 reload 缺失引发的复盘

一次典型的证书到期故障:ACME 续期流程本身跑通了,新证书已经躺在磁盘上,但 Nginx 从未 reload,线上跑的还是旧证书。复盘症状、排查路径和三层预防措施。

本文目录
证书明明"自动续期"了,为什么还是到期宕机:一次 reload 缺失引发的复盘

凌晨的告警来得很有迷惑性:监控显示站点 TLS 握手失败,但运维同事第一反应是"不可能,我们配了自动续期"。登录服务器一看,/etc/letsencrypt/live/ 下的证书确实是新的,有效期还有八十多天。Certbot 日志显示续期在三天前成功执行。一切看起来都很健康——除了用户打不开网站。

症状:磁盘上是新的,内存里是旧的

用 openssl s_client -connect example.com:443 -servername example.com 从外部验证,返回的证书 notAfter 是昨天。再用 openssl x509 -in fullchain.pem -noout -dates 看磁盘上的文件,有效期明明还有很久。

证书明明"自动续期"了,为什么还是到期宕机:一次 reload 缺失引发的复盘:DNS ownership verification using a delegated CNAME, shown as a precise handoff between two controlled zones
证书明明"自动续期"了,为什么还是到期宕机:一次 reload 缺失引发的复盘:DNS ownership verification using a delegated CNAME, shown as a precise handoff between two controlled zones查看原图

两个结果对不上,结论就基本锁定了:提供服务的进程还在用旧证书。

Nginx 只在启动和 reload 时读一次证书文件,之后证书内容就常驻在 worker 进程的内存里。文件被 ACME 客户端悄悄换掉,进程毫不知情。旧证书到期那一刻,握手开始失败——这就是典型的"续期成功但部署没完成"。

根因:renew 钩子的最后一公里

排查续期配置,问题出在最常见的地方:renewal 配置里只写了签发参数,没有 deploy hook,或者 hook 里只有 nginx -s reload 之外缺了执行权限、路径写错,再或者之前某次重构把 hook 文件挪走了而没人发现。

证书明明"自动续期"了,为什么还是到期宕机:一次 reload 缺失引发的复盘:an ACME request being validated at separate stations and turned into a new deployable artifact while private key material remains at its origin
证书明明"自动续期"了,为什么还是到期宕机:一次 reload 缺失引发的复盘:an ACME request being validated at separate stations and turned into a new deployable artifact while private key material remains at its origin查看原图

Certbot 官方建议用 --deploy-hook 或 /etc/letsencrypt/renewal-hooks/deploy/ 目录放重载脚本,因为它只在"证书真的换了"时执行,比无脑每次 reload 干净。这次的配置里 deploy hook 存在,但脚本里写的是 systemctl reload nginx,而 systemd unit 改名过一次,命令静默失败,exit code 没人看。

这里有个小坑值得记住:静默失败比报错更可怕。如果续期直接失败,至少 Certbot 会退出非零状态,cron 可能发邮件。但"续期成功、部署失败"这种状态,文件系统层面一切正常,只有到期那天才爆出来,留给你的反应时间是零。

排查路径

当时实际走的排查顺序,供参考:

证书明明"自动续期"了,为什么还是到期宕机:一次 reload 缺失引发的复盘:a new certificate version moving from its source to several edge appliances while the previous working version remains ready for rollback
证书明明"自动续期"了,为什么还是到期宕机:一次 reload 缺失引发的复盘:a new certificate version moving from its source to several edge appliances while the previous working version remains ready for rollback查看原图
  1. 外部 s_client 拿到线上实际提供的证书指纹和日期——确认不是 DNS 或 CDN 层的错觉。
  2. 磁盘上证书文件的日期和指纹——和线上一比对,不一致,排除签发问题。
  3. nginx -T | grep ssl_certificate 确认配置指向的路径和续期输出路径是同一个(软链指向 /etc/letsencrypt/live/,没走错)。
  4. 翻 Certbot 的 renew 日志和 deploy hook 执行记录,发现 hook 命令返回了 unit 不存在的错误。
  5. 手动 reload,握手立刻恢复,验证新证书已生效。

整个过程从告警到恢复不到半小时,但暴露的问题比故障本身更值得修。

预防:三层,别只修一处

证书明明"自动续期"了,为什么还是到期宕机:一次 reload 缺失引发的复盘:an approaching certificate expiry being detected early, followed by a fresh request and a safe replacement before the live connection is interrupted
证书明明"自动续期"了,为什么还是到期宕机:一次 reload 缺失引发的复盘:an approaching certificate expiry being detected early, followed by a fresh request and a safe replacement before the live connection is interrupted查看原图

只修好那个 reload 命令是不够的,因为同类问题有一百种姿势复发。

第一层,把续期和部署绑成一个动作。 续期工具必须原生支持"签发成功后自动执行部署",并且部署失败要让整个流程返回非零状态,能被 cron 或 systemd 的失败通知机制接住。续期和部署拆成两个互不感知的任务,迟早对不上。

第二层,监控实际提供的证书,而不是文件。 从外部探测 TLS 握手返回的 notAfter,剩余天数低于阈值(比如 14 天)就告警。这一条能兜住几乎所有"续期链路某环断了"的故障,包括这次这种。监控磁盘文件的 mtime 只是辅助,它证明不了线上在用什么。

第三层,续期后做主动验证。 hook 里 reload 完之后,顺手用 s_client 验证一次指纹是否变成新证书的,不对就告警。多十行脚本,省一次凌晨爬起来。

证书明明"自动续期"了,为什么还是到期宕机:一次 reload 缺失引发的复盘:a browser-side endpoint independently inspecting the live TLS connection after deployment and comparing the observed endpoint artifact
证书明明"自动续期"了,为什么还是到期宕机:一次 reload 缺失引发的复盘:a browser-side endpoint independently inspecting the live TLS connection after deployment and comparing the observed endpoint artifact查看原图

在 CertbotX 中实践

CertbotX 把上面第一、三层做成了默认行为:ACME 签发成功后直接进入部署阶段,支持推送到 Agent、EdgeOne 或 CDN,部署结果有记录可查,而不是"脚本跑没跑成功靠猜"。到期前会自动重新签发并再次走完整部署链路,每次部署留下审计记录——排查"线上到底装的哪张证书"时,直接翻记录比登机器对比指纹快得多。

参考核验

← 返回资讯中心