Nginx处理HTTPS请求时,TLS握手先于HTTP请求发生。客户端在握手的ClientHello消息中通过SNI扩展告知服务器自己访问的域名,Nginx会解析这个扩展并将原始值放入$ssl_server_name变量。这个变量只在TLS会话中使用了SNI扩展时才会被赋值,否则保持为空字符串。理解该变量的生成时机和取值规则,对证书配置、访问控制以及日志分析都有直接影响。

$ssl_server_name 变量的生成时机
SNI扩展由RFC 6066定义,它允许客户端在TLS握手阶段声明自己连接的目标主机名。当Nginx作为HTTPS服务器时,其SSL模块会在握手回调中读取ClientHello里的server_name扩展,并把该值保存为$ssl_server_name变量。因此这个变量的生命周期从TLS握手阶段就开始了,早于任何HTTP请求头解析。
如果客户端没有发送SNI扩展,例如使用IP地址直接访问、使用非常老旧的SSL库,或者客户端主动禁用了SNI,那么$ssl_server_name的值就是空字符串。Nginx从1.7.0版本开始正式提供该变量,早期版本无法直接获取TLS握手中的服务器名称。对于仍然需要兼容旧版Nginx的环境,只能通过$server_name或$host来间接推断域名,但这并不等同于客户端在TLS层声明的名称。
需要特别注意的是,$ssl_server_name只在TLS会话中可用。如果Nginx作为纯HTTP服务器或者TLS终止由上游负载均衡器完成,Nginx自身没有参与SSL握手,那么这个变量自然也不会有值。另外在TLS会话复用的情况下,新连接可能不会重新进行完整握手,但Nginx仍会保留原始握手时获取的SNI信息,因此$ssl_server_name在整个连接生命周期内是稳定的。
$ssl_server_name 与 $host、$server_name 的差异
很多配置中会同时出现$ssl_server_name、$host和$server_name,但它们的数据来源完全不同。$server_name是当前虚拟主机配置中server_name指令的值,属于服务器端的静态配置,不依赖客户端行为。$host来自HTTP请求头中的Host字段,客户端可以随意修改,例如使用curl命令添加自定义Host头就能伪造该值。而$ssl_server_name来自TLS握手阶段的SNI扩展,虽然客户端也可以控制SNI值,但要让服务器完成TLS握手,SNI必须与证书能匹配的部分保持一致,否则连接会在证书校验阶段失败。
可以通过一个对比表直观看出三者的差异。假设服务器同时配置了ipipp.com和www.ipipp.com两个域名,客户端使用命令访问时,$ssl_server_name会是ipipp.com,$host可能是被修改过的other.com,$server_name则取决于请求最终匹配到哪个server块。这就意味着在做基于域名的访问控制时,$ssl_server_name比$host更接近TLS层真实目标,比$server_name更能反映客户端意图。
| 变量 | 数据来源 | 是否可被客户端伪造 | 可用阶段 |
|---|---|---|---|
| $ssl_server_name | TLS SNI扩展 | 难以伪造且需匹配证书 | TLS握手及之后 |
| $host | HTTP Host请求头 | 可随意伪造 | HTTP请求阶段 |
| $server_name | server_name指令 | 不可伪造,是服务器配置 | 整个请求阶段 |
在多域名证书场景中,使用$ssl_server_name来做日志记录和路由判断通常比$host更可靠。举个例子,攻击者可以通过伪造Host头来绕过某些基于$host的访问限制,但要绕过基于$ssl_server_name的限制,他必须在TLS握手中发送一个不匹配的SNI,这会导致证书错误,反而暴露异常。因此如果安全策略依赖域名,优先考虑$ssl_server_name。
使用 $ssl_server_name 的典型配置场景
第一种常见场景是记录TLS握手阶段的域名信息。默认的访问日志格式通常只包含$host,无法反映客户端在TLS层声明的名称。可以通过自定义log_format把$ssl_server_name加入日志,便于排查客户端是否使用了SNI、SNI值与HTTP Host是否一致等问题。下面是一个简单的日志格式示例。
log_format tls_log '$remote_addr - $ssl_server_name - $host - $request - $status'; access_log /var/log/nginx/tls_access.log tls_log;
第二种场景是根据SNI做访问控制。例如有些内部服务只希望特定域名通过HTTPS访问,同时禁止用户直接用IP地址或者未知域名连接。可以在server块中判断$ssl_server_name是否为空或不在白名单内,如果不符合要求直接返回403。下面的配置展示了使用map指令配合if判断的方式,避免在server块中直接写复杂的正则表达式。
http {
map $ssl_server_name $allowed_domain {
default 0;
ipipp.com 1;
www.ipipp.com 1;
}
server {
listen 443 ssl;
server_name ipipp.com www.ipipp.com;
if ($allowed_domain = 0) {
return 403;
}
location / {
proxy_pass http://backend;
}
}
}
第三种场景是反向代理或网关层根据SNI将请求转发到不同后端。虽然Nginx本身可以通过多个server块来区分域名,但在某些动态配置场景下,利用$ssl_server_name配合map可以简化管理。例如将不同子域名映射到不同内部端口,而不需要为每个子域名单独写一个server块。不过需要注意,Nginx的server块选择仍然主要依据server_name指令,$ssl_server_name更多用于辅助判断和日志记录。
常见问题与排错建议
$ssl_server_name为空是最常遇到的问题。可以先检查客户端是否真的发送了SNI。使用openssl命令可以模拟带SNI和不带SNI的握手,观察服务器返回的证书是否与预期一致。如果客户端使用IP地址连接,即使服务器配置了多个域名,Nginx也拿不到SNI,此时会走default_server或第一个匹配的server块。对于不支持SNI的旧客户端,需要明确配置default_server并设置合理的回退证书。
另一个常见问题是SNI值和HTTP Host不一致。这种情况多发生在反向代理链中,比如前面的CDN或负载均衡器修改了Host头,但保留了原始SNI。如果后端Nginx同时使用$ssl_server_name和$host做不同逻辑,就可能出现路由混乱。排错时可以在日志中同时输出这两个变量,快速判断是TLS层还是HTTP层出现了不一致。
最后要留意$ssl_server_name保存的是原始字符串,没有进行大小写标准化或去除末尾点号。如果域名带有尾随点,比如ipipp.com.,它并不会被自动转换成ipipp.com。在做精确匹配或map映射时需要预先考虑这些边界情况。可以在map中使用正则表达式统一处理,例如将变量转为小写并去掉末尾点,避免因为客户端发送格式差异导致匹配失败。理解这些细节后,$ssl_server_name就能成为Nginx HTTPS配置中一个很有价值的工具。
Nginxssl_server_nameSNI修改时间:2026-10-03 05:21:22