问题背景
公司对外业务网关 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,说明不是服务挂了,而是证书本身失效。 - 关键特征:故障是"突然全量爆发"而非渐进,且与当天发布无关——当天没有任何上线操作。
排查过程
第一步,确认是不是证书过期。 直接看对外域名的证书:
|
|
咦,浏览器说过期,openssl 说没过期?这说明 443 上跑的证书根本不是我们以为的那张——前面还有一层 CDN / 负载均衡(SLB)在终结 TLS,客户端拿到的是 CDN 的证书,而源站 Nginx 证书是另一张。于是分别检查两端:
|
|
真相浮现:CDN 回源到源站用的是一张独立签发的源站证书,这张证书昨天 23:59 到期,而 CDN 对外证书是正常的。由于"全站 HTTPS 强制 + CDN 回源也强制 HTTPS",一旦源站证书过期,CDN 回源握手失败,整站雪崩。
第二步,为什么自动续期没生效? 查 certbot 续期日志与 cron:
|
|
原来上季度新上线了一个内部运维平台,默认占用了 80 端口做 Web 管理,而 certbot 的 HTTP-01 校验需要临时监听 80 端口响应 ACME 挑战。新服务把 80 抢走了,certbot 续期连续两三个月都在凌晨静默失败,cron 的 stdout 被丢弃没人看,证书就这样悄悄走向过期。
第三步,确认影响面。 不只 www 和 api,所有经 CDN 回源到源站 HTTPS 的域名(共 5 个)全部中招,因为源站证书是通配符 *.example.com 单张,一损俱损。
解决方案
紧急止血(5 分钟):
- 临时让 CDN 回源改用 HTTP(关闭回源 HTTPS 校验),先把业务跑起来,代价是回源链路明文(内网可接受,限时)。
- 同时把抢占 80 端口的运维平台改到 8080,腾出 80 给 certbot。
彻底修复(30 分钟):
|
|
验证:
|
|
根因分析
根因是"单点证书 + 续期依赖被悄悄破坏 + 缺乏过期监控"三件事叠加:
- 全站回源强依赖一张源站证书,无冗余,证书一过期即全局故障——这是架构上的脆弱性。
- certbot HTTP-01 校验强依赖 80 端口,而 80 被新服务抢占后,续期失败但无任何告警,形成"静默降级"。
- 没有任何对证书剩余有效期的监控,过期前无提醒,只能等故障发生才发现。
预防措施
- 监控优先:在 Prometheus/黑盒监控里加入证书有效期探针(ssl_exporter 或定期
curlcheck),剩余< 21天自动告警到 IM/电话。 - 续期去端口化:改用 DNS-01 校验(
certbot的--dns插件),不再依赖 80 端口,避免被其他服务干扰;或用 CNAME 把_acme-challenge指向 certbot 托管区。 - 解耦回源:CDN 回源可短暂容忍自签/内部 CA 证书,或源站证书与对外证书分离并各自监控,避免单张证书拖垮全站。
- 续期结果可见:cron 里
certbot renew的退出码要接入监控,非 0 即告警,杜绝静默失败。 - 缩短排障半径:明确"CDN 对外证书 ≠ 源站证书",建立证书台账(域名、签发方、到期日、责任人)。故障第一步先
openssl x509 -enddate比对客户端与实际终结点拿到的证书,而不是凭浏览器报错下结论。
总结
这次故障最"坑"的地方在于:现象是浏览器报证书过期,但 openssl 直连却显示正常——因为中间隔了一层 CDN,客户端看到的证书和源站证书是两回事。排查 HTTPS 类问题,第一性原则是"分清 TLS 在哪一层被终结"。
另外,证书自动续期从来不是"配一次就高枕无忧"。只要续期链路上的任何依赖(端口、DNS、权限)被改动,它就会静默失效。把"证书有效期监控 + 续期结果告警"作为标配,远比依赖 cron 不报错来得靠谱。