证书过期致 HTTPS 全站中断:一次续期事故排查

凌晨两点,监控群炸了:用户反馈打开网站直接报”您的连接不是私密连接”,证书已过期。一个本来该自动续期的 Let’s Encrypt 证书,悄悄失效了——而没人收到任何告警。这类故障最阴险的地方在于:它不报警、不报错、不影响进程,只是让 TLS 握手在客户端被浏览器拦下。本文复盘这次事故,给出从发现到根治的完整四步法,帮你把”证书过期”从定时炸弹变成可控项。

事故现场:全站变”不安全”

表现很典型:后端服务、Nginx、数据库一切正常,curl 内网也 200,但只要走公网 HTTPS,Chrome 就弹出红色拦截页,App 接口集体 5xx。最初容易误判成”服务器挂了”去重启,纯属白忙——真正坏掉的是那张 90 天有效期的证书。它和SSL 证书的工作原理绑定:证书一旦到期,服务端虽能完成 TCP 与 TLS 握手的前半段,却在证书校验环节被客户端拒绝。

为什么证书会”悄悄”过期

两类证书:CA 签发 vs 自签

理解它为什么静默,才能对症下药。证书过期不会抛异常、不写错误日志,只是让客户端校验失败——所以只盯进程健康度的监控完全抓不到它。

线上公开服务用的是 CA 签发证书(如 Let’s Encrypt,90 天有效期、可自动续期);内网常偷懒用自签证书(有效期更长但需手动信任)。无论哪种,有效期都是硬截止,不会因为你”忘了”就宽限一天。

自动续期为何失效

Let’s Encrypt 靠 certbot renew 自动续期,但它依赖两件事:① 续期脚本被定时触发(cron 或 systemd timer);② 续期时的 HTTP-01 挑战能临时写通 80 端口校验文件。只要 cron 被改挂、Nginx 把 80 端口重定向到 HTTPS、或磁盘满导致写不进校验文件,续期就会静默失败,而失败邮件常淹没在垃圾箱里。

还有一类容易踩的坑是通配符与多域名:一张 *.example.com 只覆盖一级子域,覆盖不到 a.b.example.com;多域名 SAN 证书则必须确保续期时把所有 alt_name 都带上,否则续出来的证书会缺域名,边缘站点照样报不安全。续期脚本里写死的域名列表,最该纳入变更评审。

排查四步:从浏览器到服务端

别急着重启,先确认是不是证书问题。两步命令即可定性:

# 1) 看远端证书有效期(notBefore / notAfter)
echo | openssl s_client -servername fsdata.site \
  -connect fsdata.site:443 2>/dev/null | openssl x509 -noout -dates

# 2) 看握手过程与证书报错
curl -vI https://fsdata.site 2>&1 | grep -i "expire\|SSL certificate"

如果 notAfter 早于今天,基本坐实。注意要用 -servername 带上 SNI,否则多域名虚拟主机可能拿到错误的证书。这一步和Nginx 反向代理配置里的多 server_name 是同一套域名逻辑。

如果本地 openssl 查到没过期、用户却报过期,多半是 CDN 或反代边缘节点缓存了旧证书——这时要顺藤摸瓜查边缘层的证书,而不是只盯源站。证书链路里任何一跳都可能独立过期,排障要沿整条链走一遍。

根因:cron 里的续期脚本静默失败

本次事故的根因很朴素:续期原本挂在一条 cron 上,某次迁移后 cron 服务没起来,certbot renew 再没跑过。等 90 天一到,证书自然过期。定位时直接手动跑续期看报错最直接:

# 先 dry-run 看能不能续(不真正签发)
sudo certbot renew --dry-run

# 确认无误后强制续期
sudo certbot renew --force-renewal

# 看 timer / cron 的执行痕迹
journalctl -u certbot.timer -n 50
systemctl status certbot.timer

常见失败原因集中在两点:HTTP-01 挑战写不通(Nginx 把 /.well-known/acme-challenge 也重定向到了 HTTPS,而那时候证书已坏、形成死循环),以及 webroot 路径配错导致校验文件 404。修好路径、补上 80 端口的明文校验路由,续期立马通过。

更深一层,这次事故暴露的是无人盯续期:既没有续期成功与否的指标,也没有证书剩余天数的告警,全靠运气和用户的抱怨。把证书当成一种会过期的资源来运营,而不是配一次就忘的配置,是认知上的关键转变。

止血与根治

应急先恢复访问,根治再杜绝复发:顺序不能乱,先止血保业务,再从容查根因,避免边救火边改配置引发二次故障。

动作目的做法
止血尽快恢复 HTTPScertbot renew --force-renewal 后立即 reload Nginx
根治-触发别再靠 cron改用 systemd timer,失败可重试且能被 journalctl 看到
根治-监控提前预警监控证书剩余天数,<15 天告警
根治-校验挑战可达Nginx 对 /.well-known/acme-challenge 放行 80 明文
# 用 systemd timer 替代脆弱的 cron
# /etc/systemd/system/certbot.timer
[Unit]
Description=Certbot 续期

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target

把续期从”藏在 cron 里碰运气”变成”有日志、可重试、失败可见”的 systemd 服务,是这次事故最大的收获。它和服务器安全加固里”让异常可被观测”的思路一脉相承。

防复发清单

  • 监控先行:用 Prometheus 或外部拨测监控证书剩余天数,别等浏览器报错才发现。
  • 续期可观测:systemd timer 替代裸 cron,失败进 journalctl、进告警。
  • 挑战路由放行:Nginx 对 /.well-known/acme-challenge 走 80 明文,别被全局 HTTPS 重定向吞掉。
  • 多域名逐一核:SNI 下每个 server_name 都要确认证书匹配,别只看默认站点。
  • 演练续期:把 certbot renew --dry-run 纳入上线检查,确保挑战链路一直通。

小结

证书过期故障的共性,是”静默“二字:不报警、不报错,只在某个清晨让全站 HTTPS 集体失灵。根因往往不是证书机制,而是续期链路脆弱(裸 cron、挑战路由被重定向、无监控)。把续期改成 systemd 定时、给挑战路由放行、对剩余天数做监控,三件套就能把这类事故彻底关进笼子。和Nginx 限流防刷一样,边界层的可靠性,靠的是可观测、可重试、可预警,而不是”应该会自动好”。证书到期从不是小概率事件,而是确定性会发生的时间点——把它排进运维日历,比任何应急预案都管用。

上一篇 大模型上下文工程实战:长上下文提示设计
下一篇 研发效能度量:用 DORA 指标驱动团队提速