证书悄悄过期:一次 HTTPS 服务雪崩的生产复盘
证书悄悄过期:一次 HTTPS 服务雪崩的生产复盘
一、故障开始的那几分钟
周三凌晨 02:14,监控告警集中爆发:支付回调失败率从 0 飙到 100%,App 端大面积提示“网络异常”。但服务器 CPU、内存、数据库连接数全都正常——典型的“服务在线、业务挂掉”。
最先定位到异常的是网关日志里密密麻麻的 SSL handshake failed。一查证书有效期,根因一目了然:核心域名的两张证书在前一天夜里 23:59 同时到期,而我们一直以为它们还有 30 天——因为监控看的是另一张早已废弃的旧证书。
二、为什么“还有 30 天”是假的
这是一个很典型的认知偏差:
- 监控对象错了。我们的证书过期告警绑定的是测试环境的一张证书,生产域名的证书从来没进过监控列表。
- 多证书并存。同一个域名历史上换过 CDN 厂商,旧的证书文件残留在服务器上,运维脚本扫到的恰好是那张“还活着”的旧证。
- 中间证书缺失。即便主证书续上了,部署时漏了中间证书(Intermediate CA),部分老浏览器和移动端直接判定链不完整,表现为“部分用户能访问、部分不行”。
这三件事叠加,让“还有 30 天”的错觉维持了整整一个月。
三、止血与恢复
- 立即回滚到上一版证书(CDN 侧有历史版本),先让流量恢复。
- 补发完整证书链:用
fullchain而非单张cert,避免中间证书缺失。 - 逐端验证:不止 curl,还要用真机(iOS / Android / 旧版 Chrome)打开核心页面,确认没有
NET::ERR_CERT_*。 - 临时加宽告警窗口:在到期前 30/14/7/3/1 天分档告警,直到自动化接管。
从告警爆发到全量恢复,耗时约 47 分钟——其中 30 分钟花在“为什么线上和本地表现不一致”的排查上。
四、把这次故障变成规则
复盘后我们落地了几条硬规矩:
- 证书监控必须按“域名 + 部署位置”双维度登记,而不是只看某一张文件。
- 续证脚本必须做链完整性校验,签发后用
openssl verify -CAfile自测,失败不许上线。 - 每次部署后跑一次端到端 TLS 探针,把“真机可访问”作为发布门的硬性条件。
- 证书到期前 30 天起,进入“盯防期”,钉钉/邮件双通道提醒,到期前 7 天未续则升级到值班负责人。
五、可以顺手做的小事
如果你也用 Let's Encrypt,把续期做成定时任务只是第一步。更稳的做法是:续期成功后自动触发“部署 + 探针 + 通知”一条龙,而不是等人工发现到期。证书本身免费,但因为它导致的那次雪崩,代价远超想象。
浙公网安备 33010602011771号