为什么浏览器能打开、curl 却报证书错:聊聊中间证书与完整链
服务器只发叶子证书时,浏览器常靠缓存和 AIA 补齐中间证书,看起来一切正常;但 curl、Java、移动端 SDK 不会替你补,于是报 chain incomplete。本文解释证书链的构成、AIA 的作用,以及如何检查和修对。
本文目录

线上最常见的 TLS 灵异事件大概是这样:Chrome 里站点开得好好的,小绿锁一切正常,结果监控探针、curl、或者某个 Java 客户端突然报 unable to get local issuer certificate。运维第一反应是"客户端有问题",但多数时候恰恰相反——是服务器证书链没配全,浏览器只是替你兜了底。
证书链到底缺了什么
CA 几乎不会用根证书直接签你的站点证书。标准做法是:根证书签一个中间证书,中间证书再签你的叶子证书。TLS 握手时,服务器有义务把叶子证书加上所有中间证书一起发给客户端,根证书不用发——客户端的信任库里本来就有。

问题出在只发叶子这一步。证书本身没问题、没过期、域名也对,但客户端手里只有叶子,又找不到签发它的中间证书,链就断了。
为什么浏览器"看起来没事"
现代桌面浏览器会做两件客户端库通常不做的事:

- 缓存中间证书。你访问过的其他站点如果用过同一个中间证书,浏览器会记住它,下次直接拿来补链。
- AIA 抓取。叶子证书里通常带一个 AIA(Authority Information Access)扩展,里面有 CA 提供的中间证书下载地址。浏览器可以自己去下载补齐。
所以同一条残缺的链,Chrome 能开,是因为它偷偷帮你跑了一趟腿;而 OpenSSL 的默认行为、curl、很多语言的标准库不做 AIA 抓取,于是当场报错。这不是客户端挑刺,是服务器欠了债、有人帮你垫付了而已。靠浏览器缓存"碰巧能开"更危险——换个新设备、清个缓存,现象就变了,排查起来像闹鬼。
怎么确认和修对

最直接的检查方式是握手时看服务器实际发了几张证书:
openssl s_client -connect example.com:443 -servername example.com -showcerts输出里 Certificate chain 一节如果只有一张证书(0 s: 之后没有 1 s:),就是缺中间证书。也可以用 SSL Labs 之类的在线检查,它会直接标出 "Chain issues: Incomplete"。

修法因服务器而异,但原则一致:把 CA 给你的中间证书(通常叫 chain 或 intermediate 文件)拼进服务器配置。Nginx 下常见坑是 ssl_certificate 只指向了叶子证书文件,正确做法是把中间证书追加在同一个文件里,叶子在前、中间在后。Apache 老版本用单独的 SSLCertificateChainFile,新版本同样合并进 SSLCertificateFile。改完记得用上面的命令复验,别只刷新浏览器——浏览器恰恰是最不可靠的验证工具。
在 CertbotX 中实践
链不完整这类问题,很多时候出在"证书是签出来了,但部署时只拷了叶子"。在 CertbotX 里,ACME 签发的完整链(含中间证书)会作为一个整体向 Agent、EdgeOne 或 CDN 自动部署,不依赖人工拼接文件,每次到期前重新签发后也会再次完整部署,避免续期后链退回残缺状态。部署记录可以在审计日志里回查,出问题时能确认当时推到服务器上的到底是哪个版本的链。

什么时候你会再遇到它
- 新换了一家 CA 或中间证书换过(CA 偶尔会轮换中间证书),旧缓存失效后问题暴露。
- 接入新的客户端类型:IoT 设备、移动 SDK、gRPC 服务间调用,这些环境基本都没有 AIA 补链能力。
- 做 mTLS 或内部 PKI,自建中间 CA 时链配置全靠自己,更容易漏。
一句话记住:浏览器能开不代表链是对的,curl 报错才接近真相。