nginx证书链问题根源在于服务端是否一次性发送完整且顺序正确的中间证书(不含根),ios/安卓严格依赖此链,而部分linux/windows可本地补全;需用openssl s_client验证实际发送内容,并人工检查fullchain.pem的结构与顺序。

排查 Nginx 证书链在不同操作系统上的信任差异,核心是理解各系统对证书链完整性和顺序的校验逻辑不同:iOS 和安卓严格要求服务端在 TLS 握手时**一次性发送终端证书 + 所有中间证书(不含根)且顺序正确**;而部分 Linux 发行版或旧版 Windows 可能依赖本地 CA 存储自动补全,掩盖问题。真正的差异点不在“信任”,而在“是否收到完整链”。
用 openssl s_client 直接看 Nginx 实际发了什么
这是唯一可信的起点——不看浏览器、不看 SSL Labs,只看服务器此刻对外发送的内容:
- 运行:
openssl s_client -connect your-domain.com:443 -servername your-domain.com -showcerts 2>/dev/null - 观察输出中
-----BEGIN CERTIFICATE-----出现几次:1 次 → 链缺失;2 次或以上 → 至少发出了终端+一级中间;3 次 → 可能含二级中间(如 Let’s Encrypt 的 R3 → E1) - 再加过滤:
| grep "subject=",确认第一行含CN=your-domain.com,第二行是中间 CA 名(如CN=Let's Encrypt R3),第三行应为更上一级中间(如CN=ISRG Root X1是错的,CN=DigiCert TLS RSA SHA256 2020 CA1才对)
检查 fullchain.pem 是否真含多级中间且顺序合规
Nginx 不解析、不重组、不补全,它只原样发送 ssl_certificate 指向的文件。所以必须人工核对该文件:
- 执行:
cat /path/to/fullchain.pem,确认每段都以-----BEGIN CERTIFICATE-----开头,且数量 ≥2 - 第一段 subject 必须是你域名;第二段 issuer 必须等于第一段 subject(即签发关系成立);第三段 issuer 必须等于第二段 subject —— 这才是合法的多级链
- 常见错误:把根证书(如 ISRG Root X1)也拼进去;或顺序颠倒(中间证书在前、域名证书在后);或两段之间有空行/乱码/BOM
- 正确拼法示例:
cat domain.crt intermediate1.crt intermediate2.crt > fullchain.pem(注意无空行、无私钥)
验证多级链是否被各系统本地信任库识别
即使 Nginx 发出了完整链,客户端仍需能回溯到自己信任的根。差异常出现在中间证书是否预置:
- iOS/macOS:信任 Apple 根证书库,对 Let’s Encrypt R3/E1 支持好,但对某些私有 CA 或较新中间(如 ZeroSSL 的 ECC 中间)可能未预置
- 安卓:各厂商定制差异大,Android 7+ 基本支持主流中间,但部分国产 ROM 会删减 CA 列表;若链中某级中间未预置,就会报 “CERT_TRUST_UNKNOWN”
- Linux(curl/wget/Python requests):默认读取
/etc/ssl/certs/ca-certificates.crt,若该文件不含某级中间,校验即失败 - 验证方法:
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt /path/to/domain.crt,返回 OK 表示本地信任链可通
交叉测试真实终端行为,避开缓存干扰
不同系统表现不一,单靠一个工具容易误判:
- iOS:Safari 访问后若报错,进「设置 → Safari → 清除历史记录与网站数据」;也可用
chrome://net-internals/#hsts移除域名 HSTS 缓存 - 安卓:Chrome 无痕模式访问;或用 Termux 执行
curl -v https://your-domain.com看底层报错(如unable to get issuer certificate) - Windows:PowerShell 运行
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}; Invoke-WebRequest https://your-domain.com可绕过校验快速定位是否为链问题 - 关键提醒:不要用浏览器图标判断——它可能缓存旧链;务必 reload Nginx 后立刻重跑
s_client,再测终端











