证书明明没过期却大面积报错:一次缺失中间证书的故障复盘
证书链中间证书缺失时,桌面浏览器往往靠缓存"假装正常",而 API 客户端和部分移动端直接握手失败。本文复盘这类故障的典型症状、根因定位方法,以及如何用完整链和部署审计避免复发。
本文目录

凌晨的告警很诡异:监控显示部分地区的 HTTPS 探测全部失败,但你用自己电脑打开网站,一切正常,小绿锁亮得很稳。证书到期时间还有两个月,openssl 本地验证也过了。运维群里开始有人怀疑是 CDN 节点抽风。
这种情况十有八九不是 CDN 的锅,而是证书链断了。
症状为什么"一半人没事"
TLS 握手时,服务器应该把自己证书加上完整的中间证书链一起发出去。如果只发了叶子证书,客户端就得自己补齐中间证书。
桌面 Chrome、Firefox 常访问各大网站,中间证书早就在本地缓存里躺着,断链时它能从缓存里补上,页面照常打开。但 curl、Java 的 HttpClient、很多移动端 WebView 没这个习惯——拿不到中间证书就直接握手失败,报 "unable to verify the first certificate" 之类。
于是你看到的现场就是:监控探针(通常是 curl 类工具)全红,研发同事的浏览器一片祥和。两边都对,也都让人怀疑人生。
根因定位

这次故障的根因其实很常见:换证书时只拷贝了 cert.pem(叶子证书),漏掉了 chain.pem 或忘了用 fullchain.pem。Nginx 配置里指向的证书文件不完整,服务器自然只发出一张叶子证书。
排查一条命令就能看清楚:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl crl2pkcs7 -nocrl -certfile /dev/stdin | openssl pkcs7 -print_certs -noout或者直接看输出里 Certificate chain 那一段:如果只列出一个 subject,链就是断的。正常情况应该能看到叶子证书加至少一张中间证书,比如 Let's Encrypt 的 R 系列中间证书。
对比工具也有用:curl -vI https://example.com 在你本地报错的同时,换一台刚装好的测试机(没有证书缓存)再试一次,两边都失败基本就坐实了链的问题,而不是某个客户端的个例。
修复与预防
修复就是把完整链喂给服务器。Nginx 里 ssl_certificate 指向 fullchain.pem,重启后重跑上面的验证命令确认链数够了。HAProxy、Tomcat 之类同理,只是拼链的文件格式不同。

预防更值得花心思:
- 部署只认 fullchain。 在部署脚本或文档里把"证书文件必须是 fullchain.pem"写成硬性要求,别让操作者每次临场决定拷哪个文件。
- 把链完整性加进监控。 监控别只查"证书快没过期",加一条链校验:
openssl s_client配合-verify_return_error,链断了立刻告警,不用等用户报障。 - 多端点探测。 至少有一个探测点用不带缓存的纯 TLS 客户端(curl 或 Go 小程序),桌面浏览器式的探测会掩盖问题。
- 多节点部署时逐台验证。 负载均衡后面多台机器,一台忘了更新证书文件,就会出现"时好时坏"的玄学报错,比全挂还难查。
在 CertbotX 中实践
链断的根源往往是"手工拷文件"这一步。CertbotX 用 DNS-01(支持 CNAME 委托,方便泛域名和跨账号场景)自动完成 ACME 签发,证书到期前自动重新签发,然后直接部署到 Agent、EdgeOne 或 CDN——部署链路里没有人工挑文件这个环节,全链文件随签发结果一起下发。每次部署都会留下记录,出了问题可以审计:哪台节点、哪个时间点、部署的是哪份证书,一目了然,排查"时好时坏"类故障时尤其省时间。
自动化不是万能的,但它把"忘了拷 chain.pem"这种低级失误从流程里物理消除了。
参考
- Let's Encrypt:证书链说明(Chain of Trust) — 了解中间证书的作用和当前链结构
- Nginx 官方 HTTPS 配置文档 —
ssl_certificate对完整链的要求