用 openssl s_client 定位「证书链不完整」导致的部分客户端报错
部分手机或 API 客户端报证书错误、浏览器却正常,多半是中间证书没配全。本文演示用 openssl s_client 和在线验证确认问题,并给出 Nginx 下补全证书链的具体做法。
本文目录

从一个常见怪现象说起
运维群里隔三差五有人问:网站在 Chrome 里一切正常,但某台旧安卓、某个 Java 服务或者 curl 调用却报「unable to get local issuer certificate」。换了几台电脑试都没事,于是怀疑是客户端的问题。
其实锅多半在服务端:你只配置了服务器证书,漏掉了中间证书。桌面浏览器会从缓存或 AIA 扩展里把中间证书补回来,所以「看起来正常」;而很多嵌入式客户端、Java 程序、部分手机应用不会自己去补链,握手时就直接失败了。
前置条件
- 一台已配置 HTTPS 的服务器(以 Nginx 为例),有 shell 权限
- 本机装有 openssl(Linux/macOS 自带,绝大多数发行版无需安装)
- 域名已能正常提供 HTTPS 服务

第一步:用 openssl s_client 看服务端到底发了什么
openssl s_client -connect example.com:443 -servername example.com重点看输出里的这段:
Certificate chain
0 s:CN = example.com
i:C = US, O = Let's Encrypt, CN = R3
1 s:C = US, O = Let's Encrypt, CN = R3
i:C = US, O = Let's Encrypt, CN = ISRG Root X1编号从 0 开始。0 是你的服务器证书,1 及之后是服务端主动发出来的中间证书。如果链里只有编号 0,说明服务端只发了叶子证书——问题坐实。
-servername 不能省。没有它,SNI 不生效,一台 IP 上挂了多个证书时你看到的可能是默认证书,排查方向直接跑偏。

再看输出末尾的验证结果:
Verify return code: 0 (ok)如果显示 21 (unable to verify the first certificate),说明连 openssl 都无法靠收到的链构建信任路径,基本可以确认中间证书缺失。
第二步:确认链缺了哪一环
不中断连接执行一个完整校验更直观:
openssl s_client -connect example.com:443 -servername example.com \
-verify_return_error </dev/null
链不完整时,它会在握手后立即断开并报错,而不是让你看到 HTTP 层。
另外可以用 Let's Encrypt 推荐的思路交叉验证:把叶子证书的 issuer 和你手里 fullchain.pem 中第一张中间证书的 subject 对比,二者必须一致。不一致说明拼接时张冠李戴了——这在手动管理证书时并不少见。
第三步:在 Nginx 里补全证书链
Nginx 的 ssl_certificate 指令要求文件里按顺序包含:服务器证书在前,中间证书依次跟在后面。用 ACME 客户端签发的证书一般有两个文件:
cert.pem:仅叶子证书fullchain.pem:叶子 + 中间证书

正确做法是直接用 fullchain.pem:
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;如果你是从 CA 后台手动下载的证书(通常一个 .crt 加一个中间证书文件),需要自己拼:
cat server.crt intermediate.crt > fullchain.crt顺序必须是叶子在前。拼反了 Nginx 启动不一定会报错,但客户端收到的链是乱的,排查起来更费劲。
改完先 nginx -t,再 reload。然后重跑第一步的 s_client 命令,链里应该能看到完整的 0、1(甚至 2)级证书,且 Verify return code: 0 (ok)。

几个容易踩的小坑
- 换了浏览器验证不算数。 Chrome 缓存过中间证书后,哪怕服务端永远不发它也能验证通过。排查这类问题要以 openssl 的原始输出为准。
- OCSP stapling 失败有时也是链不全。 Nginx 做 stapling 时需要
ssl_trusted_certificate指向签发链,链不全 stapling 会静默失效。 - 证书续期后忘了重拼。 手动拼接的
fullchain.crt不会自动更新,续期换了新的叶子证书后必须重新拼接,否则 90 天后准时出事。
在 CertbotX 中实践
这类「续期后链没跟上」的问题,本质上是手动流程的副产品。CertbotX 通过 ACME 自动签发时直接交付完整的 fullchain,续期在到期前自动完成,签发出的证书会按记录自动部署到 Agent、EdgeOne 或 CDN,避免人工拼链和漏 reload 这类操作失误。如果域名 DNS 不在同一账号下,还可以用 DNS-01 的 CNAME 委托把验证权交给专用解析区域,签发和部署全程有部署记录可审计,出问题能查到每次签发和推送的具体时间点。
参考核验
- Nginx 官方 HTTPS 服务器配置文档——确认
ssl_certificate对证书链顺序的要求 - Let's Encrypt 文档——了解其证书层次结构与签发链说明