在HTTPS反向代理场景中,客户端到Nginx的TLS握手和Nginx到源站的TLS握手是两个彼此独立的会话。源站如果在同一个IP上通过SNI区分多张证书,Nginx回源时是否发送SNI、发送哪个主机名,就决定了源站能否选择到正确证书。不少配置只开启了proxy_ssl_server_name,却没有同步调整proxy_ssl_name,导致请求在Nginx层看起来正常,但源站证书始终不匹配。还有团队希望在access_log中记录回源SNI,却发现这个值并不等于客户端请求SNI。本文从SNI机制出发,给出Nginx传递与记录回源SNI的完整配置,并梳理常见异常。

TLS SNI在回源链路中的作用
SNI,即Server Name Indication,是TLS握手阶段ClientHello消息中的一个扩展字段。客户端在握手时把目标域名明文发送给服务端,服务端根据这个域名从本机证书列表中选出最匹配的一张证书。当源站只在443端口监听一个IP,却要服务多个域名时,SNI就是证书选择的唯一依据。
Nginx作为反向代理时,客户端访问的是Nginx的443端口,Nginx完成客户端侧TLS握手后,会以TLS客户端身份重新向源站发起HTTPS连接。此时Nginx是否发送SNI,由proxy_ssl_server_name指令控制。该指令默认值为off,也就是说,如果没有显式开启,回源TLS握手中不会携带SNI扩展。源站收不到SNI,只能返回默认证书。若默认证书与请求域名不一致,客户端或Nginx就会收到证书校验失败、名称不匹配等错误。
即使开启了proxy_ssl_server_name on,还必须关注proxy_ssl_name的取值。它决定Nginx作为TLS客户端时发送哪个主机名作为SNI。默认值是$proxy_host,这个值在proxy_pass指向域名时通常可用,但如果proxy_pass指向的是IP地址或upstream组名,SNI就会变成IP或组名,源站无法据此选择证书。因此,单纯开启SNI并不够,正确设置SNI内容才是关键。
Nginx回源SNI传递配置
如果回源目标使用固定域名,最直接的方式是在location中同时设置proxy_ssl_server_name和proxy_ssl_name。以下是一个简单示例:
upstream backend_https {
server 10.0.0.10:443;
}
server {
listen 443 ssl;
server_name www.ipipp.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
location / {
proxy_pass https://backend_https;
proxy_ssl_server_name on;
proxy_ssl_name www.ipipp.com;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
在这个配置里,虽然upstream中的真实服务器地址是10.0.0.10:443,但回源TLS握手时Nginx会向源站发送www.ipipp.com作为SNI。源站如果同时托管多个域名的证书,就能根据这个SNI返回正确证书。
如果希望回源SNI直接透传客户端请求中的SNI,可以使用$ssl_server_name变量。但要注意,当客户端使用IP直连或某些旧客户端不发送SNI时,$ssl_server_name可能为空。直接透传空值会导致源站无法选择证书。更稳妥的做法是用map做兜底:
map $ssl_server_name $backend_sni {
"" $host;
default $ssl_server_name;
}
server {
listen 443 ssl;
server_name www.ipipp.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
location / {
proxy_pass https://backend_https;
proxy_ssl_server_name on;
proxy_ssl_name $backend_sni;
proxy_set_header Host $host;
}
}
这里的map逻辑是:如果客户端请求没有SNI,则回源SNI使用$host;如果客户端请求携带了SNI,则原样透传给源站。$host默认来自请求头Host,在正常浏览器访问场景下基本等同于用户访问的域名。如果担心Host头被篡改,也可以把兜底值改为$server_name,使用server_name指令中定义的域名。
如果还需要在回源时校验源站证书,可以继续添加证书校验相关指令。例如:
proxy_ssl_verify on; proxy_ssl_trusted_certificate /etc/nginx/ssl/ca.pem; proxy_ssl_verify_depth 2;
开启证书校验后,SNI的选择会直接影响证书验证是否通过。如果SNI发送错误,源站可能返回另一张证书,Nginx会因为证书名称不匹配而报错。因此回源SNI、回源域名、证书校验三者必须一起考虑。
如何把回源SNI写入access_log
默认的combined日志格式中没有SNI字段,更不会记录回源阶段实际使用的SNI。要排查证书选择问题,就需要自定义log_format。回源SNI并不是Nginx内置变量,但我们可以把自定义变量同时用于proxy_ssl_name和日志格式,从而实现记录。
map $ssl_server_name $backend_sni {
"" $host;
default $ssl_server_name;
}
log_format upstream_sni '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'request_sni="$ssl_server_name" upstream_sni="$backend_sni" '
'upstream="$upstream_addr" response_time="$upstream_response_time"';
access_log /var/log/nginx/access.log upstream_sni;
这个日志格式会记录客户端请求SNI、回源SNI、上游节点地址和上游响应时间。由于$backend_sni是在map中定义的变量,它在整个请求处理过程中保持可用。需要注意的是,日志是在请求处理结束后写入的,如果后续配置里再次修改了$backend_sni,日志中记录的是最后一次赋值后的结果。因此建议把map放在http级别,并避免在location中重复赋值。
如果日志需要被采集到JSON分析系统,可以使用escape=json,让Nginx自动处理引号转义:
log_format json_sni escape=json '{"remote_addr":"$remote_addr","request_sni":"$ssl_server_name","backend_sni":"$backend_sni","upstream":"$upstream_addr"}';
access_log /var/log/nginx/json_sni.log json_sni;
使用JSON格式时,字段值会被正确转义,便于写入日志平台后做结构化检索。在实际运维中,同时记录$ssl_server_name和$backend_sni非常有用。比如客户端请求SNI是www.ipipp.com,但回源SNI因为配置错误变成了10.0.0.10,两条日志一对比就能快速定位问题。
常见问题与排查思路
第一个典型问题是源站返回421 Misdirected Request。这个状态码通常出现在HTTP/2场景下,服务端发现TLS握手时协商的SNI与请求头中的Host不一致,或者与证书主体不匹配,就会拒绝服务。遇到421时,应先核对客户端请求SNI、$host和proxy_ssl_name三者的值。多数情况下,是因为回源SNI被配置成了固定值,而实际请求Host发生了变化。
第二个常见问题是使用IP回源时证书始终不匹配。很多配置把proxy_ssl_server_name打开了,却没有设置proxy_ssl_name。此时Nginx会使用默认的$proxy_host作为SNI。如果proxy_pass中的地址是IP,源站会收到一个IP形式的SNI,自然无法匹配域名证书。解决方法就是显式指定proxy_ssl_name为源站证书对应的域名。
第三个问题是日志中$ssl_server_name为空。这通常不是Nginx的问题,而是客户端本身没有发送SNI。老旧的TLS客户端、使用IP直连的客户端、部分健康检查探针都可能省略SNI扩展。要确认是否客户端未发SNI,可以在Nginx侧抓包查看ClientHello,或者使用openssl s_client模拟测试:
openssl s_client -connect 10.0.0.10:443 -servername www.ipipp.com -tls1_2
如果模拟请求带上了-servername参数后源站返回正确证书,说明源站SNI选择逻辑正常,问题出在客户端或Nginx回源配置。如果模拟请求不带-servername时返回默认证书,则进一步印证了源站依赖SNI进行证书选择。
第四个问题是透传客户端SNI后源站仍然选了错误证书。这通常是因为客户端SNI本身就和源站证书不匹配,例如客户端访问的是旧域名,但源站已经更换成新证书。透传只是把原始SNI原样发送,并不能修正错误。此时应在Nginx回源前使用map或set把旧域名映射到新域名,或者直接设置固定的回源SNI。回源SNI的正确性取决于证书部署,而不是Nginx单方面决定。
总结
Nginx回源HTTPS时的SNI传递,核心配置只有两项:proxy_ssl_server_name on负责开启SNI发送,proxy_ssl_name负责指定SNI内容。默认值$proxy_host在IP回源或upstream组名场景下往往不可用,因此建议显式设置回源SNI。若要透传客户端SNI,可以通过$ssl_server_name变量实现,并配合map为空值做兜底。
日志记录方面,回源SNI没有现成的内置变量,但可以自定义变量,同时用于proxy_ssl_name和log_format,实现请求SNI、回源SNI、上游节点等信息的统一记录。这样在出现421、证书不匹配、源站选错证书等问题时,不用反复修改配置或抓包,直接查看日志对比变量即可快速定位。