浏览器或 API 客户端提示证书错误时,“证书无效”仍然不够具体。TLS 校验至少包含信任链、有效期、目标主机名、证书用途和撤销/策略等维度。证书可以由受信任 CA 正确签发、日期也有效,却因为 Subject Alternative Name(SAN)不包含用户访问的域名而被拒绝。关闭校验或使用 curl -k 只能绕过安全检查,不能证明服务已经修复。
适用范围与结论
本文适用于 Nginx、负载均衡器、CDN 和常见 HTTPS API 的主机名不匹配故障。V2CE 实验使用短期本地 CA、OpenSSL 1.1.1w 与 Nginx 1.28.3。两个服务器证书都由同一个受信任实验 CA 签发,但只有一个证书的 SAN 包含客户端访问的 api.v2ce.test。
诊断时必须同时保存:
- 客户端访问的确切主机名和端口;
- DNS 实际连接地址,以及是否经过 CDN、代理或负载均衡;
- 带正确 SNI 获取到的叶证书、证书链和 SAN;
- 客户端的完整验证错误,而不是只记录“SSL error”;
- 修复后同一验证命令的成功结果。
第一步:用正确 SNI 查看服务器实际返回的证书
同一 IP 可以承载多个 HTTPS 站点。若不发送 SNI,服务器可能返回默认证书,从而把正常配置误判为错误。明确传入目标主机名:
openssl s_client \
-connect api.example.com:443 \
-servername api.example.com \
-showcerts < /dev/null
保存输出中的连接地址、证书链、subject、issuer 和 verify return code。然后单独解析叶证书:
openssl s_client -connect api.example.com:443 -servername api.example.com \
-showcerts < /dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -serial -dates -ext subjectAltName
生产取证不要把私钥复制出来;验证服务器证书只需要公开证书链。
第二步:明确执行主机名与信任链验证
若使用企业或私有 CA,把可信 CA 文件明确提供给 OpenSSL,并要求验证失败时返回非零:
openssl s_client \
-connect 203.0.113.20:443 \
-servername api.example.com \
-CAfile /path/to/trusted-ca.pem \
-verify_hostname api.example.com \
-verify_return_error < /dev/null
Verify return code: 0 (ok) 才表示该次 OpenSSL 校验通过。Hostname mismatch 说明证书身份与目标名称不一致;unable to get local issuer certificate 更偏向链不完整或客户端缺少信任锚;certificate has expired 则是时间有效性问题。它们需要不同修复。
第三步:用 curl 保留 DNS、SNI、Host 和 HTTP 对照
如果怀疑 DNS 或某个边缘节点返回了错误证书,可用 --resolve 固定连接地址,同时保留 URL 中的主机名,因此 Host、SNI 和证书主机名校验仍使用真实域名:
curl -sv --connect-timeout 5 \
--resolve api.example.com:443:203.0.113.20 \
https://api.example.com/health
分别测试每个负载均衡或 CDN 地址,记录哪个地址返回了错误证书。不要用 https://203.0.113.20/ 代替:那会改变 SNI、Host 和验证目标。也不要把 -k/--insecure 当作修复;它会让中间人证书和错误身份也被接受。
第四步:检查 SAN、通配符和访问域名
现代客户端使用 SAN 中的 DNS 标识匹配服务身份。常见错误包括:
- 证书只包含
www.example.com,用户访问的是api.example.com; - 通配符
*.example.com被误认为同时覆盖根域example.com或多级名称a.b.example.com; - 续期系统签发了新证书,但 Nginx/负载均衡仍加载旧文件或旧 secret;
- 多站点 SNI 映射错误,某些边缘节点返回其他站点证书;
- 客户端通过内部别名访问,但别名没有包含在证书身份中。
不要只看证书 subject 的 Common Name。应检查完整 SAN,并以客户端实际 URL 的主机名为准。
第五步:区分边缘证书和源站证书
使用 CDN 或反向代理时,通常存在客户端到边缘、边缘到源站两段 TLS。浏览器看到的是边缘证书;CDN 报源站握手失败时,应检查边缘连接源站时使用的 SNI、源站证书和信任策略。两段证书不能混为一谈。
先画出请求路径并在每段独立验证。公网 DNS、内部 DNS、负载均衡 listener、Kubernetes Ingress secret 和 Nginx server block 都可能决定最终证书。
修复、上线与回滚
修复方式通常是重新签发包含正确 SAN 的证书,或修正 SNI 到证书的映射。部署前确认叶证书与私钥匹配、证书链顺序正确,并检查有效期:
openssl x509 -in fullchain.pem -noout -subject -issuer -dates -ext subjectAltName
openssl pkey -in privkey.pem -pubout -outform pem | sha256sum
openssl x509 -in cert.pem -pubkey -noout | sha256sum
sudo nginx -t
两个公钥摘要应一致。私钥命令只应在受控主机本地运行,输出中不要泄露私钥内容。nginx -t 通过后再平滑重载,并对每个域名、每个边缘地址执行带主机名验证的请求。
回滚时恢复上一套仍在有效期且覆盖正确域名的证书和映射,重新进行语法检查与平滑重载。证书部署应保留到期天数、续期结果和实际线上指纹监控,避免“磁盘已更新、进程仍加载旧证书”。
V2CE 复现实验记录
实验 CA 同时签发了两个有效期内证书。错误证书 subject/SAN 均为 wrong.v2ce.test,客户端却以 api.v2ce.test 访问。OpenSSL 返回验证错误 62 Hostname mismatch,退出码为 1;curl 返回错误 60,明确指出没有匹配目标主机名的 SAN。此时 Nginx 后端实际已经运行,故障发生在发送 HTTP 请求之前的身份校验阶段。
正确证书的 SAN 改为 api.v2ce.test,CA、客户端信任和后端响应保持不变。OpenSSL 返回 Verify return code: 0 (ok),curl 收到 HTTP 200 和正文 tls-hostname-verification-ok。这证明实验故障点是证书身份不匹配,而不是 CA 不受信任、证书过期或 Nginx 未监听。