Nginx 已经配置了 HTTPS,浏览器却提示 ERR_CERT_COMMON_NAME_INVALID,这类故障通常不是"证书过期",而是客户端访问的主机名没有出现在服务端实际返回证书的身份列表里。多域名、反向代理和 IPv4/IPv6 并存时,配置看着没问题,返回的却可能是默认站点证书。本文用一个不依赖猜测的顺序,把访问域名、SNI、证书 SAN、Nginx server 块和地址族逐层对上。

一、先把报错翻译成可检查的条件
浏览器会把 URL 中的主机名作为期望身份,再把 TLS 握手中收到的证书拿来匹配。当前者是 api.example.com,而证书的 Subject Alternative Name(SAN)只有 www.example.com,连接即使完成加密,也不能证明对方就是期望的服务。重点是"返回了哪张证书",不是磁盘上"存在某张证书"。现代客户端主要检查 SAN 中的 DNS 名称;只改 Common Name(CN)经常不能解决问题。RFC 9525 给出了 TLS 服务身份校验规则。
二、先用 curl 确认用户看到的结果
bash
curl -vkI https://api.example.com/ 2>&1 | sed -n '/subject:/p;/issuer:/p;/expire date:/p;/SSL certificate verify/p'
curl -4vkI https://api.example.com/ 2>&1 | sed -n '/subject:/p;/SSL certificate verify/p'
curl -6vkI https://api.example.com/ 2>&1 | sed -n '/subject:/p;/SSL certificate verify/p'
第一条看默认解析路径,后两条把 IPv4 与 IPv6 分开。测试机若没有 IPv6,-6 失败并不等于线上用户一定失败;它的价值是暴露 AAAA 指向了另一台没有同步证书的服务器。
三、用 openssl s_client 带上正确的 SNI
bash
openssl s_client -connect 203.0.113.10:443 \
-servername api.example.com -showcerts </dev/null 2>/tmp/tls.err \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
cat /tmp/tls.err | grep -E 'Verify return code|verify error'
多站点 Nginx 会据此选择相应的 server 块;不带 SNI 时拿到默认站点证书,不能据此判断域名配置。
bash
openssl s_client -connect 203.0.113.10:443 \
-servername api.example.com -verify_hostname api.example.com \
-verify_return_error </dev/null
四、读取 SAN,不要只看 CN
bash
openssl x509 -in fullchain.pem -noout -subject -ext subjectAltName
openssl x509 -in fullchain.pem -noout -text \
| sed -n '/Subject:/p;/Subject Alternative Name:/,/X509v3/p'
确认 SAN 中是否有完整的 api.example.com。通配符 *.example.com 通常只覆盖下一层标签,例如可以匹配 api.example.com,不能把根域 example.com 或更深的 a.api.example.com 一并覆盖。需要根域和子域时,把它们作为独立 DNS 名称申请并写入同一张 SAN 证书。
五、检查 Nginx 的三处对应关系
在 Nginx 配置里重点核对三件事:listen 443 ssl 是否覆盖实际地址族;server_name 是否包含访问主机名;ssl_certificate 是否指向当前证书。示例:
nginx
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name api.example.com;
ssl_certificate /etc/nginx/tls/api.fullchain.pem;
ssl_certificate_key /etc/nginx/tls/api.key;
location / { proxy_pass http://127.0.0.1:8080; }
}
同一个端口上如果有多个 server 块,检查有没有重复的 server_name、更早加载的默认站点或遗漏的 IPv6 监听。改完先测试配置,别急着 reload:
bash
nginx -T > /tmp/nginx-effective.conf
nginx -t
systemctl reload nginx
nginx -T 读的是合并后的生效配置,比只打开某个 conf 文件可靠。若宝塔或其他面板管理 Nginx,应确认修改的是实际加载文件,避免改了"看起来正确"的备份文件。
六、排查 DNS、代理和 CDN 的分叉
用多个解析结果核对 A、AAAA 和 CNAME:
bash
dig +short A api.example.com
dig +short AAAA api.example.com
dig +short CNAME api.example.com
for ip in $(dig +short A api.example.com); do
echo "=== $ip ==="
openssl s_client -connect "$ip:443" -servername api.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -ext subjectAltName
done
如果域名经过 CDN、WAF 或负载均衡,客户端拿到的是边缘节点证书;源站证书正确,不代表边缘证书也正确。每个暴露 HTTPS 的入口都要单独验收。
七、按故障现象快速归类
| 现象 | 优先检查 | 常见修复 |
|---|---|---|
| 只访问别名时报错 | SAN 是否含别名 | 重新申请包含全部 DNS 名称的证书 |
| 带域名正常,带 IP 报错 | 证书是否含 IP SAN | 使用域名访问,或申请含 IP 的合规证书 |
| IPv4 正常、IPv6 报错 | AAAA 指向和 IPv6 Nginx 配置 | 同步证书,或修正/移除错误 AAAA |
| openssl 带 SNI 正常、浏览器异常 | 代理/CDN/本地缓存链路 | 逐入口抓取实际证书并核配置 |
八、不要用关闭校验"修复"域名不匹配
curl -k、浏览器添加例外或程序关闭主机名校验,只能让当前客户端继续连接,不能证明身份匹配。诊断阶段可以临时观察 HTTP 层,但最终验收必须恢复默认证书校验,并保留失败输出。
九、修复后的四步验收清单
第一,用目标域名执行 curl -4 和 curl -6;第二,用 openssl s_client -servername 读取每个入口的 SAN;第三,用 nginx -T 确认生效配置和证书路径;第四,从真实应用或浏览器建立新连接。四步都通过,才算解决。
十、把身份检查纳入续期
自动续期后不要只看文件修改时间。保存 SAN,reload 后用目标域名建立新 TLS 连接;多入口环境循环检查 CDN、IPv4、IPv6 和源站。