Nginx回源HTTPS时如何正确传递和记录SNI?

来源:JavaScript教程作者:三上悠亚头衔:网络博主
导读:本期聚焦于三上悠亚创作的《Nginx回源HTTPS时如何正确传递和记录SNI?》,敬请观看详情。HTTPS回源链路中,SNI的传递经常被忽略,直到源站因为证书选择错误返回421或握手失败。Nginx作为反向代理时,默认并不会在回源TLS握手中携带SNI扩展,这会导致同一个源站IP上部署多张证书时无法命中正确证书。要让回源请求携带期望的SNI,需要显式开启proxy_ssl_server_name,并通过proxy_ssl_name指定主机名。与此同时,日志里通常只能看到客户端请求SNI和上游地址,回源阶段实际使用的SNI没有直接变量,需要借助自定义变量和log_format才能落盘。本文将拆解Nginx回源SNI传递机制,给出可复用的配置片段,并说明如何把请求SNI、回源SNI、上游状态等字段记录到access_log,方便排查证书选择与回源链路问题。

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

Nginx回源HTTPS时如何正确传递和记录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_nameproxy_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、$hostproxy_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回源前使用mapset把旧域名映射到新域名,或者直接设置固定的回源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_namelog_format,实现请求SNI、回源SNI、上游节点等信息的统一记录。这样在出现421、证书不匹配、源站选错证书等问题时,不用反复修改配置或抓包,直接查看日志对比变量即可快速定位。

NginxSNI回源修改时间:2026-08-28 02:59:59

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。