SSL 证书链不完整:静默的 HTTPS 杀手
你的 SSL 证书是有效的,到期日还在几个月之后,openssl 也说签名没问题。但访客在 Chrome 中看到"您的连接不是私密连接",在 Firefox 中看到"警告:潜在的安全风险",在 Safari 中看到吓人的红色页面。到底怎么回事?
90% 的情况下答案是:你缺少中间证书。
理解证书链
SSL 证书不是孤立存在的。它处于一条信任链中:
根 CA(浏览器内置信任)
└── 中间 CA(由根 CA 签发)
└── 你的服务器证书(由中间 CA 签发)
浏览器内置了数百个根证书。但它们没有中间证书。如果你的服务器只发送了叶子证书而没有附带中间证书,浏览器就无法构建一条完整的路径回到受信任的根 —— 于是直接拒绝连接。
修复方法很简单:让服务器在握手时把中间证书一起发给客户端。
步骤 1:使用 OpenSSL 诊断
连接你的服务器,检查它发回的证书链:
openssl s_client -connect example.com:443 -showcerts
按 Enter,等待输出。寻找证书块 —— 每个块以 -----BEGIN CERTIFICATE----- 开始,以 -----END CERTIFICATE----- 结束。配置正确的服务器应该至少发送两个证书:
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:/O=ISRG Root X1 # <-- 这是根证书(通常不发送)
---
如果只看到 0 s:/CN=example.com 而 1 为空或不存在,说明服务器只发送了叶子证书 —— 链不完整。
步骤 2:检查缺失了什么
运行以下命令确认根本原因:
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt example.com.crt
如果输出 error 20 at 0 depth lookup: unable to get local issuer certificate,说明服务器证书因缺少中间证书而无法验证。
你也可以查看服务器证书的签发者来确定需要哪个中间证书:
openssl x509 -in example.com.crt -noout -issuer
输出类似 issuer = /C=US/O=Let's Encrypt/CN=R3,告诉你需要的中间证书是哪个。
步骤 3:获取中间证书
从你的证书颁发机构(CA)下载中间证书。以下是常见来源:
- Let's Encrypt:通常 Certbot 会自动处理。如果没有,从 letsencrypt.org/certificates 下载。常用中间证书是
R3(ECDSA)或R10/R11(RSA)。 - DigiCert: DigiCert 根证书与中间证书
- Sectigo (Comodo): Sectigo 中间 CA 证书
- GlobalSign: GlobalSign 证书库
- Cloudflare Origin CA:源证书是 Cloudflare 自签的 —— 如果在 Cloudflare 代理后面则不需要中间证书。如果没有代理,请使用正规 CA。
将中间证书保存为文件,例如 intermediate.crt。
步骤 4:合并为完整链文件
关键是顺序很重要。服务器证书在前,中间证书在后,根证书通常不需要包含。将它们拼接:
# 服务器证书在前,然后是中间证书
cat example.com.crt intermediate.crt > fullchain.crt
# 如果有多个中间证书(较少见):
cat example.com.crt intermediate1.crt intermediate2.crt > fullchain.crt
验证合并后的文件顺序正确:
grep "BEGIN CERTIFICATE" fullchain.crt | cat -n
应该看到每个证书按顺序编号:
1 -----BEGIN CERTIFICATE----- # 你的服务器证书
2 -----BEGIN CERTIFICATE----- # 中间证书
步骤 5:更新服务器配置
将 Web 服务器指向完整链文件,而不是仅指向服务器证书。
Nginx
# /etc/nginx/conf.d/example.com.conf 或类似路径
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/ssl/certs/fullchain.crt; # <-- 完整链
ssl_certificate_key /etc/ssl/private/example.com.key;
# ... 其他配置
}
测试并重载:
nginx -t
nginx -s reload
Apache
# /etc/apache2/sites-available/example.com-le-ssl.conf
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /etc/ssl/certs/fullchain.crt # <-- 完整链
SSLCertificateKeyFile /etc/ssl/private/example.com.key
# 使用 fullchain.crt 时无需 SSLCertificateChainFile
# ... 其他配置
</VirtualHost>
测试并重载:
apachectl configtest
systemctl reload apache2 # 或: systemctl reload httpd
步骤 6:使用在线工具验证
重载后再次运行 openssl 确认:
openssl s_client -connect example.com:443 -showcerts </dev/null 2>/dev/null | grep "s:/" | head -3
同时使用外部工具做完整验证:
- FixWikiHub SSL 检查器: /zh/tools/ssl-checker.html —— 验证链完整性、到期时间和协议支持。
- SSL Labs (Qualys): ssllabs.com/ssltest —— TLS 分析的行业标准。
- KeyCDN SSL Checker: tools.keycdn.com/cert —— 快速可视化证书链。
SSL Labs 评级为 "A" 或更高,说明链完整、协议现代、没有信任问题。
常见 CA 及中间证书下载地址
| 证书颁发机构 | 中间证书下载地址 | 备注 |
|---|---|---|
| Let's Encrypt | letsencrypt.org/certificates | 通常由 Certbot 自动处理;使用 R3/R10/R11 |
| DigiCert | digicert.com/kb/digicert-root-certificates.htm | 有多个世代的中间证书;需匹配你证书的签发者 |
| Sectigo (Comodo) | sectigo.com/knowledge-base/ssl-certificates/intermediate-ca-certificates | 使用 "Sectigo RSA Domain Validation" 中间证书 |
| GlobalSign | globalsign.com/en/repository/roots-and-intermediates | 根据 OV 还是 DV 选择正确的中间证书 |
| Amazon Trust Services | aws.amazon.com/certificate-manager/resources/ | ACM 证书由 ACM 自动处理 |
进阶提示:如果你使用 Let's Encrypt 的 Certbot,--fullchain参数会自动生成合并文件。检查/etc/letsencrypt/live/example.com/fullchain.pem—— 它已经包含了中间证书。