跳到主要内容

技术教程

TLS 证书受信任却仍报错:用 SNI 与 SAN 定位主机名不匹配

验证与风险信息

适用环境
OpenSSL 1.1.1w / Nginx 1.28.3 / Docker 24.0.7 / short-lived local CA
验证耗时
约 12 分钟
风险与回滚
公开证书与握手读取为低风险;生产私钥不得复制出受控主机,证书替换前验证 SAN、链、密钥匹配和回滚证书
最后复核
2026-08-06
验证证据
同一受信 CA 下,错误 SAN 触发 OpenSSL verify 62 和 curl 60;修正 SAN 后 Verify return code=0,HTTPS 返回 200。

浏览器或 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

诊断时必须同时保存:

  1. 客户端访问的确切主机名和端口;
  2. DNS 实际连接地址,以及是否经过 CDN、代理或负载均衡;
  3. 带正确 SNI 获取到的叶证书、证书链和 SAN;
  4. 客户端的完整验证错误,而不是只记录“SSL error”;
  5. 修复后同一验证命令的成功结果。

第一步:用正确 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 未监听。

参考资料

退出移动版