中间证书丢了会怎样:浏览器能开、curl 却报错的证书链问题
服务器只发叶子证书而不带中间证书时,浏览器常因缓存了中间证书而显示正常,curl、Java 客户端却会握手失败。本文解释证书链的构成、为什么不同客户端表现不一致,以及如何排查和修复。
本文目录

线上最常见的一种"灵异"TLS 故障长这样:浏览器打开网站一切正常,锁标志也在,但监控系统、curl 或者某个 Java 服务调用同一个域名就报 unable to get local issuer certificate。运维第一反应往往是"客户端有问题",其实锅通常在服务端——证书链没配全,少了中间证书。
证书链到底是什么

CA 不会拿根证书直接给你的域名签发。根证书私钥被锁得很严实,日常签发用的是中间 CA 的证书。所以一条完整的信任链通常是:
- 你的叶子证书(域名证书)
- 一张或多张中间证书
- 根证书(预置在操作系统或浏览器的信任库里)
TLS 握手时,服务器应该把叶子证书和所有中间证书一起发给客户端。根证书不用发,客户端自己就有,发了也没用。

为什么浏览器"看起来没问题"
现代浏览器会缓存访问过的网站发过来的中间证书,也会通过证书里的 AIA(Authority Information Access)扩展去 CA 的地址把缺失的中间证书下载回来。所以即使你的服务器只发了叶子证书,Chrome 往往也能自己把链补全,用户毫无感知。
curl、OpenSSL、Java、Python 的多数 HTTP 库没这么勤快。它们拿到一张叶子证书,本地信任库里只有根,中间缺一环,链构不出来,直接报错。

这就是为什么"浏览器能开"不能作为证书配置正确的判据。用 openssl s_client -connect example.com:443 -servername example.com 看握手输出里的 Verify return code,或者数一数服务器返回了几张证书,才是靠谱的验证方式。
怎么修
大多数 Web 服务器要求你把叶子证书和中间证书按顺序拼接成一个文件:叶子在前,中间证书依次在后。Let's Encrypt 的客户端直接给你两个文件:cert.pem 只有叶子,fullchain.pem 才是拼好的完整链。Nginx 的 ssl_certificate 指向的应该是 fullchain.pem,指成 cert.pem 就是这个故障的典型成因。

Apache 旧版本有单独的 SSLCertificateChainFile 指令,新版本也推荐直接合并进 SSLCertificateFile。各家的写法略有差异,但原则一致:服务器要发送叶子加全部中间证书。
改完配置记得重新握手验证一次,别只看浏览器。
在 CertbotX 中实践

CertbotX 通过 ACME 自动签发证书时,拿到并部署的是包含中间证书的完整链,部署到 Agent、EdgeOne 或 CDN 的记录会留档可审计。对多节点部署的场景,这能避免某台机器上手工拼接漏掉中间证书、却又因为浏览器表现正常而长期无人发现的情况。证书到期前的自动重新签发同样按完整链处理。
核验参考
- Let's Encrypt 的证书链说明(含中间证书列表):了解 Let's Encrypt 当前使用的中间证书和链结构。
- RFC 8555(ACME 协议):证书下载接口返回的即包含完整链的 PEM。
- Nginx HTTPS 服务器配置文档:其中明确说明
ssl_certificate需要包含中间证书的拼接顺序。