您安装了 SSL 证书、重启了网络服务器、并在浏览器中指向了 https:// — 但一些移动设备上的访问者仍然看到安全警告,您的 API 客户端抛出 SSL: CERTIFICATE_VERIFY_FAILED。最可能的原因是 不完整的证书链。了解证书链的工作方式是诊断和永久解决此类 SSL 错误的最快方法。
什么是 SSL 证书链?
SSL(或 TLS)证书链 — 也称为 信任链 — 是数字证书的有序序列,它将您域的服务器证书连接到浏览器和操作系统无条件信任的根证书颁发机构(Root CA)。
标准链有三个层级:
- 根 CA — 预先安装在每个主要操作系统和浏览器的信任存储中的顶级证书(例如 DigiCert 全球根 CA、ISRG Root X1)。根 CA 从不直接向个人网站颁发证书。
- 中间 CA — 由根 CA 签署的证书。中间 CA 是实际颁发您服务器证书的实体。根据 CA 的架构,可能有一个或两个中间级别。
- 服务器证书 — 绑定到您域的证书。它包含您的公钥、涵盖的域名 (SAN) 和有效期。
当浏览器通过 HTTPS 连接时,它执行 链验证:它从您的服务器证书沿着链走向每个中间证书,直到在其信任存储中找到根 CA。如果任何链接丢失或链无法验证,浏览器将显示安全警告。
为什么根 CA 不直接颁发证书
这种设计是一项深思熟虑的安全决策。根 CA 的私钥极其宝贵 — 如果它被盗或滥用,该 CA 在全球范围内颁发的每一个证书都需要被撤销,地球上每台设备都需要更新信任存储。这将是灾难性的。
为了降低这种风险,根 CA 私钥存储在物理上受保护的、隔离的设施内的硬件安全模块 (HSM) 中离线存储。根 CA 仅签署中间 CA 证书,这些证书随后用于日常颁发。如果中间 CA 被破坏,CA 可以撤销该中间证书并颁发替代证书,而根证书保持不变。
中间层的其他好处包括:
- 不同的中间证书可以实施不同的验证策略(DV、OV、EV)
- 更小的撤销范围限制了任何事件的影响范围
- 交叉签名允许通过旧根证书在旧设备上信任较新的根证书
什么是不完整的链,为什么会发生?
当您的网络服务器在 TLS 握手期间仅发送 服务器证书,而不包括浏览器需要验证链直到受信任根证书的中间证书时,就会出现不完整的链。
常见原因:
- 仅安装服务器证书 — 将
certificate.crt上传到您的服务器配置,而不包括相应的ca_bundle.crt或intermediate.crt - 迁移服务器 — 复制
server.key和server.crt但忘记 CA 包 - Let's Encrypt 配置错误 — 将网络服务器配置指向
cert.pem而不是fullchain.pem - 自动更新钩子失败 — 更新钩子更新证书文件,但网络服务器配置仍然引用旧路径
- CA 提供不完整的包 — 某些提供商仅通过电子邮件发送服务器证书,而没有明确指出单独的 CA 包文件
为什么桌面浏览器有时会隐藏问题
现代桌面浏览器会激进地缓存中间证书。当您的浏览器访问使用相同中间 CA 的任何网站时,它会将该中间证书本地存储。稍后,当它访问具有不完整链的您的网站时,它可以从其缓存重建链 — 并显示绿色挂锁。这会创造一个欺骗性的局面:服务器管理员在他们的笔记本电脑上看到绿色,但新设备上的移动用户(缓存为空)会看到警告。这就是为什么从已知干净的环境进行测试至关重要。
使用 OpenSSL 诊断链
OpenSSL 是检查您的服务器实际发送内容的最可靠工具:
步骤 1 — 检查发送的证书数量
openssl s_client -connect yourdomain.com:443 -showcerts 2>&1 | grep -c "BEGIN CERTIFICATE"
结果为 1 表示链不完整。结果为 2 或 3 表示中间证书正在正确发送。
步骤 2 — 验证每个深度级别
openssl s_client -connect yourdomain.com:443 -showcerts 2>&1 | grep -E "depth|verify"
健康输出:
depth=2 C = US, O = DigiCert Inc, CN = DigiCert Global Root CA verify return:1 depth=1 C = US, O = DigiCert Inc, CN = RapidSSL TLS RSA CA G1 verify return:1 depth=0 CN = yourdomain.com verify return:1
步骤 3 — 读取证书详细信息
openssl x509 -in /path/to/certificate.crt -text -noout | grep -E "Issuer|Subject|Not After|DNS:"
步骤 4 — 确认私钥与证书匹配
# 两个 md5 值必须相同 openssl x509 -noout -modulus -in certificate.crt | openssl md5 openssl rsa -noout -modulus -in private.key | openssl md5
| 链状态 | OpenSSL 信号 | 客户端影响 | 修复 |
|---|---|---|---|
| 完整链 | depth=2,所有级别验证正常 | 处处都是绿色挂锁 | 无需修复 |
| 不完整链 | 仅 depth=0;中间证书丢失 | 移动设备和 API 错误,浏览器警告 | 添加 CA 包/中间证书 |
| 顺序错误 | 链存在但顺序错误 | 间歇性客户端错误 | 重新排序:服务器 → 中间证书 → 根证书 |
| 过期中间证书 | 验证错误:证书已过期 | 所有客户端失败 | 从提供商更新 CA 包 |
修复 Apache 上的不完整链
Apache 2.4.8+ 的推荐方法是创建单个 fullchain.pem,包含您的服务器证书和中间证书,按此顺序:
方法 1 — 单个 fullchain.pem(推荐)
# 服务器证书必须在前面
cat certificate.crt intermediate.crt > /etc/ssl/certs/yourdomain.fullchain.pem
# 在您的 VirtualHost 块中
<VirtualHost *:443>
ServerName yourdomain.com
SSLEngine on
SSLCertificateFile /etc/ssl/certs/yourdomain.fullchain.pem
SSLCertificateKeyFile /etc/ssl/private/yourdomain.key
</VirtualHost>
方法 2 — SSLCertificateChainFile(仅 Apache < 2.4.8)
<VirtualHost *:443>
SSLEngine on
SSLCertificateFile /etc/ssl/certs/certificate.crt
SSLCertificateKeyFile /etc/ssl/private/private.key
SSLCertificateChainFile /etc/ssl/certs/ca_bundle.crt
</VirtualHost>
重新加载前始终测试:
apachectl configtest # 必须返回"Syntax OK" systemctl reload apache2
修复 Nginx 上的不完整链
Nginx 需要单个文件合并服务器证书和所有中间证书。顺序很重要 — 服务器证书在前,然后是按降序排列的中间证书:
# 构建 fullchain.pem
cat yourdomain.crt intermediate.crt > /etc/nginx/ssl/yourdomain.fullchain.pem
# 服务器块
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /etc/nginx/ssl/yourdomain.fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/yourdomain.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
}
nginx -t # 必须显示"syntax is ok"和"test is successful" systemctl reload nginx
Let's Encrypt — 使用正确的文件
Certbot 在 /etc/letsencrypt/live/yourdomain/ 内创建四个文件。最常见的单一错误配置是使用错误的文件:
cert.pem— 仅服务器证书(永远不要在网络服务器配置中使用此文件)chain.pem— 仅中间证书fullchain.pem— cert.pem + chain.pem 合并(始终使用此文件)privkey.pem— 私钥
# Let's Encrypt 的正确 Nginx 配置 ssl_certificate /etc/letsencrypt/live/yourdomain/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain/privkey.pem; # 错误 — cert.pem 缺少中间证书 ssl_certificate /etc/letsencrypt/live/yourdomain/cert.pem;
验证自动更新是否正常工作:
certbot renew --dry-run
通过 DirectAdmin 安装(共享主机)
在 AsiaGB 共享主机(由 DirectAdmin 提供支持)上,您可以通过控制面板管理 SSL,无需编辑配置文件:
- 登录 DirectAdmin 并导航到 SSL Certificates
- 选择 Paste a pre-generated certificate and key
- 在 Certificate 字段中粘贴您的 Server Certificate
- 在 Private Key 字段中粘贴您的 Private Key
- 启用 Use a CA Cert 并在 CA Certificate 字段中粘贴
ca_bundle.crt或intermediate.crt的全部内容 - 点击 Save
如果提供商给了您一个 .zip 文件,解压它并查找名为 *ca-bundle*、*intermediate* 或 *chain* 的文件。这就是要在 CA Certificate 字段中粘贴的文件。
专业提示:更新证书链后,始终从从未访问过您网站的设备或计算机进行测试 — 或在不同网络上使用隐私/隐身窗口。桌面浏览器会积极缓存中间证书,因此在自己的机器上进行测试可能会显示有效的挂锁,即使链对于其他所有人来说仍然被破坏。从服务器运行 openssl s_client -connect yourdomain.com:443 -showcerts 2>&1 | grep -c "BEGIN CERT" 可以给出明确的、无缓存的答案。
OCSP Stapling:链质量影响 TLS 性能
正确组装的证书链是启用 OCSP Stapling 的先决条件。OCSP Stapling 让您的服务器预先从 CA 获取已签署的撤销状态响应并将其附加到 TLS 握手中,无需客户端发送单独的 OCSP 请求。这可以减少第一次连接的延迟,并通过防止 CA 了解访问者正在连接的网站来改善用户隐私。
# 在 Nginx 上启用 OCSP Stapling ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/nginx/ssl/yourdomain.fullchain.pem; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s;
# 在 Apache 上启用 OCSP Stapling SSLUseStapling On SSLStaplingCache shmcb:/tmp/stapling_cache(128000)
没有完整的链,服务器无法确定嵌入在中间证书中的 OCSP 响应程序 URL,导致 stapling 静默失败。
常见问题
什么是 SSL 证书链,为什么需要中间 CA?
SSL 证书链是数字证书的序列,它将您的服务器证书通过一个或多个中间 CA 链接到浏览器信任的根 CA。需要中间 CA 是因为根 CA 从不直接向网站颁发证书 — 这样做会暴露根的私钥。如果中间 CA 被破坏,只需要撤销该中间 CA,根 CA 保持安全。
我如何检查我的证书链是否完整?
运行 openssl s_client -connect yourdomain.com:443 -showcerts 并计算 BEGIN CERTIFICATE 块的数量。完整的链显示 2-3 个证书。您也可以在每个深度级别检查 verify return:1。如果您只看到 depth=0,则中间证书丢失,未缓存该证书的客户端将显示安全警告。
我如何修复 Apache 和 Nginx 上不完整的链?
在 Apache 上,将您的服务器证书和中间证书连接到单个 fullchain.pem 文件(服务器证书在前),然后让 SSLCertificateFile 指向该文件。或者对于 Apache 2.4.8 以下版本使用 SSLCertificateChainFile。在 Nginx 上,ssl_certificate 必须引用已包含中间证书的 fullchain.pem。编辑后,重新加载服务前始终运行 apachectl configtest 或 nginx -t。
Let's Encrypt 有证书链问题吗?
Let's Encrypt 提供的 fullchain.pem 已经将服务器证书和中间证书捆绑在一起。最常见的错误是在网络服务器配置中使用 cert.pem(仅服务器证书)而不是 fullchain.pem。在 Nginx 上使用 ssl_certificate 或在 Apache 上使用 SSLCertificateFile 时,始终引用 fullchain.pem。Certbot 的自动更新会自动保留这些路径。