证书链中间证书缺失:浏览器没事、curl 报错的原因
很多“证书不受信任”的故障其实只差一个中间证书。本文解释证书链的构成、为什么浏览器能自动补上而 API 客户端不行,以及什么时候必须自己修。
本文目录

有同事遇到过这种场面:网站在 Chrome 里打开一切正常,锁头绿得发亮;同一台服务器,curl 却报 unable to get local issuer certificate,Java 客户端直接握手失败。运维第一反应往往是“证书过期了”或“CA 不被信任”,查半天发现都不是——只是服务器少发了一个中间证书。
证书链到底是怎么回事

你的站点证书(叶子证书)几乎从来不是根 CA 直接签的。Let's Encrypt、DigiCert 这些机构都用中间 CA 签叶子,结构大致是:根 CA → 中间 CA → 你的证书。
验证方的信任库里只有根证书。所以握手时,服务器需要把叶子证书连同中间证书一起发过去,客户端才能沿着链一路验到它认识的根。中间证书少发一个,链就断了。
为什么浏览器看起来没事

主流浏览器会做 AIA fetching:证书里有个 Authority Information Access 扩展,写着中间证书的下载地址。浏览器发现链断了,会自己去把这个中间证书抓回来、缓存起来,页面照常打开。
curl、OpenSSL、大多数 Java 和 Go 的 HTTP 客户端没这个功能。它们只信服务器实际发来的东西。于是就出现了“浏览器没问题、程序全挂”的分裂现象。更坑的是:如果你先用浏览器访问过,某些环境会缓存中间证书,让你误以为问题已经修好。

什么时候你会撞上它
- 手动配置 Nginx/Apache 时,只填了站点证书文件,忘了拼上 CA 给的中间证书(Nginx 的
ssl_certificate需要的是 fullchain,不是只有叶子的那个文件)。 - 给负载均衡、CDN 或 Ingress 上传证书时只传了单个证书。
- 签发机构换了中间 CA(比如交叉签名切换),旧配置还指向老的中间证书。
- 内网服务向合作伙伴开放 API,对方用的是不带 AIA fetching 的客户端。
排查也简单:openssl s_client -connect example.com:443 -servername example.com 看输出里的证书链有几层;或者在线工具跑一次,会直接告诉你 "chain issues: incomplete"。

修法就一条:服务器端把叶子和中间证书按顺序拼成一个文件发出去。根证书不用发——客户端要么本地有,要么有也白搭。
在 CertbotX 中实践

用 CertbotX 做 ACME 自动签发时,签发产物本身就包含完整链,部署到 Agent 或 CDN 时会按目标要求组装证书文件,避免人工上传时漏掉中间证书。每次重新签发的部署记录都有审计可查,出问题能确认当时部署的是不是完整链,而不是靠猜。
核验参考
- Let's Encrypt 官方链与中间证书说明——看它当前用哪些中间 CA、链怎么组
- RFC 8555(ACME 协议)——证书下载接口返回的即是含链的 PEM
- Nginx HTTPS 配置文档——
ssl_certificate对证书链顺序的要求