证书明明续签成功,线上却还是过期了:一次 deploy hook 漏配的复盘
一次典型的 TLS 故障复盘:证书按时续签,告警却照样响。根因不在签发,而在证书落盘后服务没有重载,以及节点间部署不同步。记录排查路径和后续的预防做法。
本文目录

事情发生在周一早上。监控告警说某个域名证书还有三天过期,但运维同事翻了一下记录:证书明明上周就自动续签成功了,新文件就躺在 /etc/letsencrypt/live/ 下面,时间戳很新。签发系统没问题,出问题的是签发之后的那段路。
症状先确认清楚
第一反应是怀疑监控误报,所以直接在线上敲:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates
返回的 notAfter 确实只剩三天。再对比本地磁盘上的证书文件:
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -dates磁盘上是新证书,线上服务给出去的却是旧的。到这里,问题范围已经从"证书签发"缩小到"证书生效"。这是个很重要的分界——很多人排查时会一头扎进 ACME 日志,其实方向从一开始就偏了。
根因:重载没发生

这台机器跑的是 nginx。TLS 证书是进程启动或 reload 时读进内存的,磁盘文件换了,nginx 自己不会察觉。自动化脚本用 certbot 续签,renewal 配置里缺了 --deploy-hook(或者 hook 存在但某次机器迁移后丢了),于是整个流程变成了:证书续签成功 → 文件更新 → 没有 nginx -s reload → 老证书继续服务到期。
排查时有两个细节值得记下:
certbot renew --dry-run是成功的。这很误导人——dry-run 只验证签发链路,不会真的执行 deploy hook 后的完整生效路径,至少不能假设它覆盖了你线上的部署动作。systemctl reload nginx如果配在 post hook 里,要注意 hook 只在实际发生续签时执行,平时 renew 命令静默退出,hook 不会跑。这本身没错,但容易让人误以为"hook 一直在工作"。
更麻烦的是第二层问题:这个域名后面还有两台边缘节点,证书是靠 rsync 定时同步过去的。主节点证书更新了,同步任务因为一次目录结构调整静默失败,边缘节点还在用旧证书。所以即使主节点 reload 了,部分用户依然会打到旧证书上——这正是为什么有人刷新页面看到的证书"时好时坏"。证书问题一旦出现"偶发",优先怀疑多节点不同步,而不是怀疑浏览器缓存。
修复和预防

当天的修复很朴素:手动 reload nginx,修好同步脚本,逐台节点用上面的 s_client 命令验证一遍。事后做了三件事:
- 续签流程里显式配置 deploy hook,重载动作和证书更新绑定在同一条链路上,不依赖人肉记得。
- 监控不只盯"证书快过期",还加了"线上证书序列号是否等于磁盘最新证书序列号"这类一致性检查。过期告警只能告诉你结果,一致性检查能在用户受影响前几天就发现部署断了。
- 把多节点的证书同步从定时拉取改成续签后主动推送,成功失败都有记录可查。
在 CertbotX 中实践
这类"签好了没生效"的故障,CertbotX 的处理思路是把签发和部署做成一条流水线:ACME 自动签发和到期前重新签发之后,证书会直接推送到 Agent、EdgeOne 或 CDN 侧,不依赖运维手动同步或补 hook。每次部署都有记录,可以回查哪台目标在什么时间换上了哪张证书——复盘时不用再靠翻 rsync 日志拼时间线。如果域名走 DNS-01 验证,CNAME 委托也能把 _acme-challenge 的处理隔离开,签发环节本身基本不需要人介入。

工具能消除的是"步骤被遗忘"这类问题,但线上证书与实际部署一致性这一点,仍建议保留自己的监控做兜底。自动化和人各看一层,比单层信任都可靠。
一点收尾的判断
证书故障里,签发失败的反而是少数,多数是"签发成功了但后面断了":没 reload、没同步、负载均衡后面有漏网的节点。排查顺序也应该是倒着来的——先看线上在发什么证书,再对比磁盘,最后才看签发日志。从用户实际看到的东西往回推,比从流程开头往后顺推快得多。
延伸阅读,供核验:
- certbot 官方文档不在允许来源内,这里以 ACME 协议本身为准:RFC 8555,可核对签发与续签的协议行为。
- nginx 官方 HTTPS 配置文档,确认证书加载与 SNI 行为。
- Let's Encrypt 文档,核对续签周期与验证方式。