根本原因是TLS握手初期SNI字段未正确传递:Nginx默认用proxy_pass中的IP或内网域名作为SNI值,导致后端无法匹配证书而断连;必须成对配置proxy_ssl_server_name on和proxy_ssl_name(动态用$host或静态写死),并确保与Host头一致。

根本原因不是证书“放错了位置”,而是 TLS 握手初期,Nginx 没收到、没传对、或没用对那个关键的域名字段——SNI(Server Name Indication)。它不依赖 HTTP 层的 Host 头,而是在加密通道建立前就决定该用哪张证书。一旦 SNI 信息丢失、错配或被覆盖,Nginx 就只能 fallback 到第一个 server 块的证书,造成“所有域名都显示同一张证书”的碰撞现象。
客户端没发 SNI:老旧设备兜底逻辑触发
Android 4.4 以下、Windows XP 的 IE、部分嵌入式设备等,压根不发送 SNI 字段。此时 Nginx 无法识别目标域名,会直接把请求交给监听 443 端口的第一个 server 块处理:
- 这个块若绑的是 site-b.net 的证书,那么访问 site-a.com 也会返回 site-b.net 的证书
- 解决方法不是禁用旧设备,而是显式声明兜底:在最常用站点的 server 块中加 listen 443 ssl http2 default_server;
- 避免把测试站、临时站放在配置文件最前面,否则它们会意外成为默认证书提供者
服务端没启用或没传对 SNI:反向代理场景高频失联
当 Nginx 作为反向代理(比如对接微服务、CDN 回源、内部 HTTPS 网关),它不仅要接收客户端的 SNI,还要把正确的域名再发给上游——但默认不会这么做:
- 仅配置 proxy_pass https://10.0.0.100; → Nginx 默认用 10.0.0.100 当作 SNI 值发给后端,而非用户访问的 shop-a.com
- 必须成对启用:proxy_ssl_server_name on; + proxy_ssl_name $host;
- 若经 CDN(如 Cloudflare、阿里云全站加速),Host 头常被重写为 origin.example.com,此时 $host 失真,应改用 proxy_ssl_name $http_x_forwarded_host; 并确保 CDN 开启“透传原始 Host”
配置结构破坏 SNI 匹配机制:多个域名塞进一个 server 块
SNI 匹配发生在 SSL 上下文加载阶段,依赖每个 server 块独立、完整、精准地声明自身能力。常见破坏性写法:
- 把 site-a.com 和 site-b.net 全写在一个 server_name 行里 → Nginx 不会为它们分别加载两套证书,只加载第一个匹配的证书路径
- 在 http 块里全局写 ssl_certificate → 所有 server 块共用这张证书,SNI 完全失效
- 多个 server 块共用同一组 ssl_certificate 路径 → 后续块证书路径出错时静默失败,访问时握手中断或证书不匹配
验证必须绕过浏览器,直击 TLS 层
浏览器缓存、HSTS、中间代理都会干扰判断。真正确认 SNI 是否生效,要用 OpenSSL 模拟原始握手:
- openssl s_client -connect your.ip:443 -servername site-a.com -showcerts 2>/dev/null | openssl x509 -noout -subject → 应输出 CN = site-a.com
- 换 -servername site-b.net → 必须返回另一张证书的 subject,不能重复
- 去掉 -servername 参数 → 应返回 default_server 或第一个块的证书,用于验证兜底行为是否符合预期

















