一次SSL/TLS证书过期导致全站HTTPS访问失败与API调用报证书错误的排查实录

CDN对外证书正常、源站证书却已过期,回源HTTPS握手失败引发整站雪崩,根因是续期依赖被悄悄破坏且无过期监控

问题背景

公司对外业务网关 www.example.com 与多个内部微服务之间的相互调用均强制走 HTTPS。证书由 Let’s Encrypt 通过 certbot 自动签发,原本配置了 cron 定时续期,团队一直以为"证书会自动续,不用管"。某周一早晨 8 点多,陆续收到一线同事反馈与监控告警:官网打不开、移动端 App 登录失败、订单回调大面积超时。此时距离上次"证书无感知续期"已过去约 3 个月——而 Let’s Encrypt 证书有效期只有 90 天。这次,自动续期并没有像预期那样静默完成,而是已经连续失败了两三个月。

故障现象

  • 浏览器访问 https://www.example.com 被直接拦截,地址栏红色警告:NET::ERR_CERT_DATE_INVALID,点开详情提示"证书已过期"。
  • 移动端 App 登录接口(https://api.example.com/login)返回网络错误,抓包发现 TLS 握手阶段即被客户端中断。
  • 内部订单服务调用支付回调域名时,Java 侧抛出 sun.security.validator.ValidatorException: PKIX path validation failed: java.security.cert.CertPathValidatorException: timestamp check failed(证书时间校验失败)。
  • Nginx error.log 出现大量 SSL 相关报错,但 Nginx 进程本身正常监听 443,说明不是服务挂了,而是证书本身失效。
  • 关键特征:故障是"突然全量爆发"而非渐进,且与当天发布无关——当天没有任何上线操作。

排查过程

第一步,确认是不是证书过期。 直接看对外域名的证书:

1
2
3
4
echo | openssl s_client -servername www.example.com -connect www.example.com:443 2>/dev/null \
  | openssl x509 -noout -dates -subject
# notBefore=May 12 00:13:42 2026 GMT
# notAfter=Aug 10 00:13:42 2026 GMT  ← 哎?还没过期?

咦,浏览器说过期,openssl 说没过期?这说明 443 上跑的证书根本不是我们以为的那张——前面还有一层 CDN / 负载均衡(SLB)在终结 TLS,客户端拿到的是 CDN 的证书,而源站 Nginx 证书是另一张。于是分别检查两端:

1
2
3
4
5
6
7
# 看客户端真实拿到的(经 CDN)
curl -vI https://www.example.com 2>&1 | grep -i 'expire\|subject'

# 直接打源站(绕过 CDN,用 hosts 或内网 IP)
echo | openssl s_client -connect 10.20.30.40:443 \
  | openssl x509 -noout -enddate
# notAfter=Jul 10 23:59:59 2026 GMT  ← 昨天到期!

真相浮现:CDN 回源到源站用的是一张独立签发的源站证书,这张证书昨天 23:59 到期,而 CDN 对外证书是正常的。由于"全站 HTTPS 强制 + CDN 回源也强制 HTTPS",一旦源站证书过期,CDN 回源握手失败,整站雪崩。

第二步,为什么自动续期没生效? 查 certbot 续期日志与 cron:

1
2
3
systemctl list-timers | grep certbot
certbot renew --dry-run   # 模拟续期
# 报错:Failed to bind to port 80 (HTTP-01 challenge) ... Address already in use

原来上季度新上线了一个内部运维平台,默认占用了 80 端口做 Web 管理,而 certbot 的 HTTP-01 校验需要临时监听 80 端口响应 ACME 挑战。新服务把 80 抢走了,certbot 续期连续两三个月都在凌晨静默失败,cron 的 stdout 被丢弃没人看,证书就这样悄悄走向过期。

第三步,确认影响面。 不只 wwwapi,所有经 CDN 回源到源站 HTTPS 的域名(共 5 个)全部中招,因为源站证书是通配符 *.example.com 单张,一损俱损。

解决方案

紧急止血(5 分钟):

  1. 临时让 CDN 回源改用 HTTP(关闭回源 HTTPS 校验),先把业务跑起来,代价是回源链路明文(内网可接受,限时)。
  2. 同时把抢占 80 端口的运维平台改到 8080,腾出 80 给 certbot。

彻底修复(30 分钟):

1
2
3
4
5
6
# 停掉占用 80 的服务后强制续期
systemctl stop ops-platform
certbot renew --force-renewal
# 成功后立即把新证书 reload 进 Nginx
nginx -s reload
# 恢复 CDN 回源 HTTPS

验证:

1
2
3
4
5
echo | openssl s_client -connect 10.20.30.40:443 \
  | openssl x509 -noout -enddate
# notAfter=Oct 08 00:13:42 2026 GMT  ← 续上,90 天

curl -sI https://www.example.com | head -1  # HTTP/2 200

根因分析

根因是"单点证书 + 续期依赖被悄悄破坏 + 缺乏过期监控"三件事叠加:

  1. 全站回源强依赖一张源站证书,无冗余,证书一过期即全局故障——这是架构上的脆弱性。
  2. certbot HTTP-01 校验强依赖 80 端口,而 80 被新服务抢占后,续期失败但无任何告警,形成"静默降级"。
  3. 没有任何对证书剩余有效期的监控,过期前无提醒,只能等故障发生才发现。

预防措施

  1. 监控优先:在 Prometheus/黑盒监控里加入证书有效期探针(ssl_exporter 或定期 curl check),剩余 < 21 天自动告警到 IM/电话。
  2. 续期去端口化:改用 DNS-01 校验(certbot--dns 插件),不再依赖 80 端口,避免被其他服务干扰;或用 CNAME 把 _acme-challenge 指向 certbot 托管区。
  3. 解耦回源:CDN 回源可短暂容忍自签/内部 CA 证书,或源站证书与对外证书分离并各自监控,避免单张证书拖垮全站。
  4. 续期结果可见:cron 里 certbot renew 的退出码要接入监控,非 0 即告警,杜绝静默失败。
  5. 缩短排障半径:明确"CDN 对外证书 ≠ 源站证书",建立证书台账(域名、签发方、到期日、责任人)。故障第一步先 openssl x509 -enddate 比对客户端与实际终结点拿到的证书,而不是凭浏览器报错下结论。

总结

这次故障最"坑"的地方在于:现象是浏览器报证书过期,但 openssl 直连却显示正常——因为中间隔了一层 CDN,客户端看到的证书和源站证书是两回事。排查 HTTPS 类问题,第一性原则是"分清 TLS 在哪一层被终结"。

另外,证书自动续期从来不是"配一次就高枕无忧"。只要续期链路上的任何依赖(端口、DNS、权限)被改动,它就会静默失效。把"证书有效期监控 + 续期结果告警"作为标配,远比依赖 cron 不报错来得靠谱。

使用 Hugo 构建
主题 StackJimmy 设计