去年写了一篇 域前置 相关文章 原文 https://www.freebuf.com/articles/web/271046.html ,最后的 CDN厂商如何禁止域前置方面没有写清楚。最近又看了一下,补充一下nginx相关配置。

从一个例子入手

有如下nginx配置:

server {
    listen 443 default_server ssl;
    ssl_reject_handshake on;
    return 200 "ok$ssl_server_name$host$server_name" ;
}
server {
    listen 443 ssl;
    server_name www.* ;
    ssl_certificate     /etc/nginx/conf.d/a.cert;
    ssl_certificate_key /etc/nginx/conf.d/a.key;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    return 200 "not ok$ssl_server_name$host$server_name";
} 

有如下访问:

curl https://www.baidu.com:10443 --resolve www.baidu.com:10443:127.0.0.1 -k -H "host:baidu.com" -v

curl https://www.baidu.com:10443 --resolve www.baidu.com:10443:127.0.0.1 -k  -H "host:baidu.com" -v
* Added www.baidu.com:10443:127.0.0.1 to DNS cache
* About to connect() to www.baidu.com port 10443 (#0)
*   Trying 127.0.0.1...
* Connected to www.baidu.com (127.0.0.1) port 10443 (#0)
* Initializing NSS with certpath: sql:/etc/pki/nssdb
* skipping SSL peer certificate verification
* SSL connection using TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
* Server certificate:
*       subject: CN=www.baidu.cn,O="BeiJing Baidu Netcom Science Technology Co., Ltd",postalCode=,ST=Beijing,C=CN
*       start date: Dec 03 10:10:33 2022 GMT
*       expire date: Nov 30 10:10:33 2032 GMT
*       common name: www.baidu.cn
*       issuer: CN=www.baidu.cn,O="BeiJing Baidu Netcom Science Technology Co., Ltd",postalCode=,ST=Beijing,C=CN
> GET / HTTP/1.1
> User-Agent: curl/7.29.0
> Accept: */*
> host:baidu.com
> 
< HTTP/1.1 200 OK
< Server: nginx/1.22.0
< Date: Sat, 03 Dec 2022 11:34:51 GMT
< Content-Type: application/octet-stream
< Content-Length: 24
< Connection: keep-alive
< 
* Connection #0 to host www.baidu.com left intact
okwww.baidu.combaidu.com

tls握手阶段使用了第二个server的配置,但最后访问到的依然是default_server,因已经过了SSL握手阶段,也没有走到ssl_reject_handshake on

为什么会这样?

http://nginx.org/en/docs/http/server_names.html 中描述的相关匹配逻辑如下:

First, a connection is created in a default server context. Then, the server name can be determined in the following request processing stages, each involved in server configuration selection:

  • during SSL handshake, in advance, according to SNI
  • after processing the request line
  • after processing the Host header field
  • if the server name was not determined after processing the request line or from the Host header field, nginx will use the empty name as the server name.

在ssl握手过程中,server_name字段来自于sni,也就是www.baidu.com。在获取到header中的Host后,server_name又重新赋值为了baidu.com 。正因为这里server_name取值的重复性,所以导致了域前置的产生。

解决方案

nginx 1.7.0 引入的变量 可以获取到 sni 的值,

$ssl_server_name returns the server name requested through [SNI](http://en.wikipedia.org/wiki/Server_Name_Indication) (1.7.0);

所以需要在每个server block里面添加以下逻辑:

 if ($ssl_server_name != $host){
       return 444; #具体返回什么无所谓,看具体业务安排。
 }

完整配置如下:

server {
    listen 80 default_server;
    listen 443 default_server ssl;
    ssl_reject_handshake on;
    return 444;
}
server {
    listen 80;
    listen 443 ssl;
    server_name www.domain.com ;
    ssl_certificate     /etc/nginx/conf.d/a.cert;
    ssl_certificate_key /etc/nginx/conf.d/a.key;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    if ($ssl_server_name != $host){
        return 444;
    }
    #巴拉巴拉 
} 
server {
    listen 80;
    listen 443 ssl;
    server_name qqq.domain.com ;
    ssl_certificate     /etc/nginx/conf.d/a.cert;
    ssl_certificate_key /etc/nginx/conf.d/a.key;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    if ($ssl_server_name != $host){
        return 444;
    }
    #巴拉巴拉 
} 

设置default_server的同时也可以解决使用IP直接访问时导致的默认证书泄露问题。

设置以后有可能导致无sni的反向代理无法正常访问,需要为原有的proxy_pass设置sni:

proxy_pass https://backend;
proxy_ssl_name $host; 
proxy_ssl_server_name on;