Nginx 代理启用 SNI 的 HTTPS 后端时返回 502
浏览器直连 HTTPS 后端正常,Nginx 代理却在 TLS 握手时被重置。对照带 SNI 与不带 SNI 的请求,配置正确的上游名称、HTTP Host 和证书验证。
浏览器直接访问后端域名正常,经过 Nginx 反向代理却返回 502。后端如果要求 TLS 握手携带 SNI,可能在收到 Nginx 的 ClientHello 后直接重置连接。Nginx 错误日志会把失败位置标在 while SSL handshaking to upstream,具体错误文字取决于后端和 TLS 库。
IIS 等按域名区分 HTTPS 站点的后端可能出现这种差异:独立端口的绑定可以用,换成同一个 443 端口、要求 SNI 的绑定后就失败。此时应检查 Nginx 发往后端的 TLS 请求,而不仅是浏览器访问 Nginx 时使用的证书。
Nginx 的 proxy_ssl_server_name 默认是 off。设置了 HTTPS 的 proxy_pass,不等于已经向后端发送 SNI。但 502 和握手失败都不是缺少 SNI 的专属错误;只有在上游要求 SNI、且代理未发送正确名称时,本文的调整才对应问题。
下文使用 Nginx HTTP 代理模块,相关 SNI 指令从 1.7.0 起提供;诊断命令按 OpenSSL 3.0 的选项编写。IP、域名和 CA 文件路径都是占位值,需替换成自己的环境。
HTTP Host 不能代替 TLS 中的服务器名称
请求至少涉及三个不同的值:连接哪个 IP 和端口、TLS 握手里的服务器名称、握手结束后 HTTP 请求里的 Host。
proxy_set_header Host backend.example.com; 只设置 HTTP 请求头。后端在 TLS 阶段已经拒绝连接时,还没有机会读取这个请求头。因此,只改 Host 可能完全没有效果。
Nginx 用两个指令控制上游 SNI:
proxy_ssl_server_name on;
proxy_ssl_name backend.example.com;
第一行启用发送,第二行指定名称。proxy_ssl_name 的默认值是 $proxy_host,取自 proxy_pass URL 的主机部分。直接代理到正确的后端域名时,这个默认值通常合适;如果 URL 中写的是 IP 或 upstream 组名,就需要确认实际应该发送的域名,不能把组名当成证书名称。
不要直接套用浏览器请求的 $host。公开入口可能是 www.example.com,后端的 TLS 站点却叫 backend.example.com。两者是否相同,应由后端绑定和证书决定。
对同一个后端比较带 SNI 与不带 SNI 的握手
先从错误日志确认实际连接的后端 IP 和端口。在能够访问该后端的本地测试环境中,以同一个地址做对照。下面 203.0.113.20 是示例 IP,/path/to/backend-ca.pem 表示你确认可信的 CA 证书集合,文件使用 PEM 格式。
先明确不发送 SNI:
openssl s_client -connect 203.0.113.20:443 \
-noservername \
-verify_hostname backend.example.com \
-verify_return_error \
-CAfile /path/to/backend-ca.pem </dev/null
再向同一地址发送后端要求的名称:
openssl s_client -connect 203.0.113.20:443 \
-servername backend.example.com \
-verify_hostname backend.example.com \
-verify_return_error \
-CAfile /path/to/backend-ca.pem </dev/null
两次测试保留相同的目标、信任配置和名称校验,只改变是否发送 SNI。OpenSSL 会将部分结果写到标准错误,不要只保存标准输出。
如果第一条被重置,或拿到不符合目标名称的证书,第二条能完成握手且验证通过,说明这个后端对 SNI 有不同处理,应继续核对 Nginx 的发送配置。-verify_hostname 检查证书名称,-verify_return_error 让验证错误成为失败;只看到服务器打印出了证书不能当作验证成功。
如果两条都失败,先按具体错误检查连接、信任链、证书名称、协议兼容性或客户端证书要求。不要仅凭 502 就认定补一条 SNI 指令能修好。两条都成功也不能证明 Nginx 已配置正确,还要比较它实际使用的地址和名称。
在代理配置中指定后端名称
下面是放在 Nginx http 上下文中的完整示例片段。它用回环地址的 8080 端口做本机验证入口,后端仍使用 HTTPS。它不包含公网入口的 TLS 配置,也不应直接替换已有整份配置。
upstream app_tls {
server 203.0.113.20:443;
}
server {
listen 127.0.0.1:8080;
server_name localhost;
location / {
proxy_pass https://app_tls;
proxy_ssl_server_name on;
proxy_ssl_name backend.example.com;
proxy_set_header Host backend.example.com;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /path/to/backend-ca.pem;
proxy_ssl_verify_depth 3;
}
}
示例连接的是 app_tls 中的 IP,发送的 SNI 和 HTTP Host 则都是 backend.example.com。后端证书必须覆盖这个名称,后端也必须存在相应的站点绑定。实际应用若要求不同的 HTTP Host,可以按应用路由调整,但 TLS 名称仍须与后端 TLS 配置匹配。
proxy_ssl_verify 默认同样是 off。启用 SNI 与启用证书验证是两件事,示例显式打开验证,并指定可信 CA 文件。不要把从未知服务器下载的一张证书直接当作可信根;使用公有 CA 时选择系统维护的可信 CA 集合,内部 CA 则使用经过确认的组织信任链。文件路径以 Nginx 实际运行环境为准,容器中的路径也必须真实存在且可读。
这里把验证深度设为 3,实际应根据合法证书链及本地策略确定。出现链不完整或名称不匹配时,应修正后端证书、信任配置或名称,不能用关闭验证来掩盖错误。这个示例也不处理后端要求客户端证书的双向 TLS 场景。
验证代理请求,并保留回退配置
先备份将修改的配置,在本地验证配置语法和引用文件可读性。对于直接使用 Nginx 命令管理、且默认配置路径就是目标实例的环境,可以使用:
sudo nginx -t
检查通过后,再按该实例原有的管理方式重新加载。直接管理的同一实例可使用:
sudo nginx -s reload
如果实际实例使用自定义配置路径、容器或服务管理器,测试和加载必须针对同一份配置。不要在宿主机测试了一份文件,却让容器继续读取另一份。
重新请求此前失败的页面,确认代理返回的是预期后端内容,并核对同一时刻的 Nginx 和后端日志:握手重置应消失,且不能出现新的证书验证错误。nginx -t 通过仅说明配置检查通过,不证明上游 TLS 连接成功。
若上游组有多个后端,逐个核对相同的 SNI 名称、证书及站点绑定;只验证一个 IP 不足以排除间歇性失败。调整后出现回归时,恢复备份的配置,检查后重新加载,并重新验证受影响请求。不要通过关闭后端的 SNI 要求或改变其他站点绑定来绕过代理配置问题。


