事故回放:周六早上九点的 502
事故回放:周六早上九点的 502
去年冬天的一个周六,值班群里跳出一条告警:某个内部管理后台大面积 502。我登录跳板机,先用 curl -vI https://admin.internal.example.com 探了一下,返回的不是网关错误,而是 TLS 握手失败。换成 openssl s_client -connect 10.20.3.11:443 -servername admin.internal.example.com,输出里明明白白写着 verify error:num=10:certificate has expired。
问题定位花了不到三分钟,但真正让人难受的是:这张证书已经过期 11 个小时,期间没有任何人、任何系统发出过提醒。我们一直以为自己「有证书监控」。
盲区一:黑盒探测绕过了 TLS 校验
翻出 Prometheus 的 blackbox 配置才发现问题。探测任务用的是这样的写法:
- target 填的是
https://10.20.3.11/healthz,然后在 relabel 里塞 Host 头; - blackbox 模块里配了
insecure_skip_verify: true,理由是「内网自签证书太多,校验会误报」; - 告警表达式只判断
probe_success == 0。
结果就是:探测请求一路畅通(因为跳过了校验),probe_ssl_earliest_cert_expiry 这个指标虽然被采集了,却从来没人基于它写告警。监控「绿」得毫无意义。
盲区二:只盯着主域名和公网入口
我们的证书清单是人工维护的,覆盖了 12 个公网域名,全部由 cert-manager 自动续期,跑了一年多没出过事。出问题的这张证书属于一个 2022 年新建的内部项目,域名不在清单里,证书是同事手工用 openssl 签的,签发人早就离职了。
这类「影子证书」在内部系统里非常常见:gRPC 服务间 mTLS、Kafka broker、内部 webhook、测试环境的泛域名,都不会出现在公网资产表里。它们的共同点是——没有纳入自动续期,也没有纳入监控采集范围。
排查时真正有用的几条命令
事后我把排查路径整理成了清单,出问题时按顺序执行,比翻文档快:
kubectl get certificate -A看所有 Certificate 资源的READY列,任何False都值得点进去;kubectl describe certificate <name> -n <ns>,重点看 Events 里 challenge 卡在哪一步(DNS01 的 webhook 挂掉是最常见原因);kubectl get secret <name> -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -dates,直接读实际下发的证书,别信 CR 的状态;cmctl renew <name>强制续期,比删 Secret 让控制器重建更可控。
补齐的三层监控
修复之后我们做了三件事,成本都不高:
- 黑盒层:为每个 TLS 端点单独建探测任务,模块里去掉
insecure_skip_verify,改用内部 CA bundle 校验;告警基于probe_ssl_earliest_cert_expiry - time(),按 30/14/7/3 天分四级,30 天走周报,7 天走电话。 - 配置层:用
certmanager_certificate_expiration_timestamp_seconds覆盖所有 cert-manager 管理的证书,同时对certmanager_certificate_ready_status == 0单独告警——续期失败往往比过期更早发生。 - 业务层:在网关侧统计 TLS 握手错误码(
ssl_handshake_failure、certificate_expired)的速率,这条曲线是最后一道兜底,不依赖任何资产清单。
另外补了一份自动生成的证书资产表:每周用脚本扫一遍内网 CIDR 的 443 端口,抓取证书的 CN、签发者和到期时间,和人工清单做 diff。第一次跑就扫出了 7 张没登记的证书。
写在最后
证书过期本身是个无聊的故障,无聊到没人愿意为它写告警。但它暴露的问题很典型:监控指标被采集了,不等于被使用了;自动化跑了很久没出事,不等于它还在正常工作。任何「配置好就不用管」的东西,都需要一个独立于它的观测点。后来我们把这条写进了值班手册:凡是带 expiry 语义的指标,必须有对应的告警规则,否则视为未监控。
- 点赞
- 收藏
- 关注作者
评论(0)