SNI

SNI是TLS协议扩展, 在握手的开始标识其尝试连接的主机名, 当多个HTTPS服务部署在同一IP地址上,客户端就可以通过这个标识指定它将使用哪一个服务, 同时服务端也无需使用相同的证书,它在概念上相当于HTTP/1.1基于名称的虚拟主机。SNI扩展最早在2003年的RFC 3546中出现。

HTTP服务通过 Http header “Host”, 来选择指定服务, HTTPS服务就是通过这个SNI来区分。通过可以通过nginx来验证一下

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
error_log  logs/error.log  debug;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}

http {
    access_log logs/access.log ;

    server {
        listen 8443 ssl ;
        server_name test;

        ssl_certificate     certs/cert.crt;
        ssl_certificate_key certs/cert.key;

        location / {
            return 200 "https-test\r\n";
        }
    }

    server {
        listen 8443 ssl ;
        server_name garlic;

        ssl_certificate     certs/cert2.crt;
        ssl_certificate_key certs/cert.key;

        location / {
            return 200 "https-garlic\r\n";
        }
    }

    server {
        listen 8443 ssl ;
        server_name 10.10.10.10;

        ssl_certificate     certs/cert3.crt;
        ssl_certificate_key certs/cert.key;

        location / {
            root   html;
            index  index.html index.htm;
        }
    }

    server {
        listen 9443 ssl ;
        server_name snitest;

        ssl_certificate     certs/cert4.crt;
        ssl_certificate_key certs/cert.key;

        location / {
            root   html;
            index  index.html index.htm;
        }
    }


    server {
        listen 8080 ;
        server_name http-test;

        location / {
            return 200 "http-test\r\n";
        }
    }

    server {
        listen 8080 ;
        server_name http-garlic;

        location / {
            return 200 "http-garlic\r\n";
        }
    }
}

 

http使用curl 通过后-H 增加header可以选择不同的主机

[garlic@dev nginx-quic]$ curl -H "Host: http-test" http://127.0.0.1:8080/
http-test
[garlic@dev nginx-quic]$ curl -H "Host: http-garlic" http://127.0.0.1:8080/
http-garlic

 

https服务可以使用openssl sclient来验证

[garlic@dev nginx-quic]$ openssl s_client -connect 127.0.0.1:8443 -servername test
CONNECTED(00000003)
depth=0 C = CN, ST = BEIJING, L = beijing, OU = organizationalUnitName, CN = test
verify error:num=18:self-signed certificate
verify return:1
depth=0 C = CN, ST = BEIJING, L = beijing, OU = organizationalUnitName, CN = test
verify return:1
---
Certificate chain
 0 s:C = CN, ST = BEIJING, L = beijing, OU = organizationalUnitName, CN = test
   i:C = CN, ST = BEIJING, L = beijing, OU = organizationalUnitName, CN = test
。。。。

[garlic@dev nginx-quic]$ openssl s_client -connect 127.0.0.1:8443 -servername garlic
CONNECTED(00000003)
depth=0 C = CN, ST = SHENZHEN, L = ShenZhen, OU = organizationalUnitName, CN = garlic
verify error:num=18:self-signed certificate
verify return:1
depth=0 C = CN, ST = SHENZHEN, L = ShenZhen, OU = organizationalUnitName, CN = garlic
verify return:1
---
Certificate chain
 0 s:C = CN, ST = SHENZHEN, L = ShenZhen, OU = organizationalUnitName, CN = garlic
   i:C = CN, ST = SHENZHEN, L = ShenZhen, OU = organizationalUnitName, CN = garlic


[garlic@dev nginx-quic]$ openssl s_client -connect 127.0.0.1:8443 -servername 10.10.10.10
CONNECTED(00000003)
depth=0 C = CN, ST = CN, L = BEIJING, OU = TEST, CN = 10.10.10.10
verify error:num=18:self-signed certificate
verify return:1
depth=0 C = CN, ST = CN, L = BEIJING, OU = TEST, CN = 10.10.10.10
verify return:1
---
Certificate chain
 0 s:C = CN, ST = CN, L = BEIJING, OU = TEST, CN = 10.10.10.10
   i:C = CN, ST = CN, L = BEIJING, OU = TEST, CN = 10.10.10.10

可以看到servername传送的不同, 选择的ssl服务也不同。特殊的如果servername 是一个ip, 处理也是一样的,创

这里servername与证书中的common name设置为一样,如果要支持多个域名或ip可以通过SAN配置。

 

对应nginx中sni的判断 ngx_http_find_virtual_server

如果是https服务 被调用两次 针对servername的回调函数, ngx_http_ssl_servername, 第二次调用是ngx_http_set_virtual_server。

所以可以设置SNI来选择证书, 然后通过后Header来重新选择服务。

curl -ksv --resolve garlic:8443:127.0.0.1 https://garlic:8443 -H "Host: test"
。。。。
* Server certificate:
*  subject: C=CN; ST=SHENZHEN; L=ShenZhen; OU=organizationalUnitName; CN=garlic
*  start date: Dec  9 01:45:35 2023 GMT
*  expire date: Dec  6 01:45:35 2033 GMT
*  issuer: C=CN; ST=SHENZHEN; L=ShenZhen; OU=organizationalUnitName; CN=garlic
。。。。
https-test

 

在wiki SNI此词条里还讲到到安全部分, SNI是明文存放的, 所以虽然tls是加密的,其实还是查询出来访问的网站的名称。Encrypted Client Hello (ECH)则是为了解决这个问题。

 

SNI Passthrough 

如果需要将客户端上送SNI传送到下游服务上,对应如下配置:

proxy_ssl_name          $host;
proxy_ssl_server_name   on;

也就是把客户端上送的SNI再发送下游服务器时再设置一下。

详细的可以参考这篇blog https://blog.martdj.nl/2023/11/09/nginx-as-reverse-proxy-and-sni/

作者描述主要问题是针对一个ip托管多个tls服务网站,未开启sni passthrough 无法通过SNI获取的server_name进行路由,无法区分是从哪个站点发起的。

 

SNI Routing

当然获取了SNI后,可以通过他进行选择下游服务器。通过map映射需要转发后端服务, 当然也可以根据需要设置默认使用的证书与key。

http {

    ssl_password_file password.txt;

    map $ssl_server_name $targetBackend {
        test1.example.com  127.0.0.1:8080;
        test2.example.com 127.0.0.1:8081;
    }

    map $ssl_server_name $targetCert {
        test1.example.com cert1/server.cert.pem;
        test2.example.com cert2/server.cert.pem;
    }

    map $ssl_server_name $targetCertKey {
        test1.example.com cert1/server.key.pem;
        test2.example.com cert2/server.key.pem;
    }

    server
    {
        listen 0.0.0.0:443 ssl;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

        ssl_certificate     $targetCert;
        ssl_certificate_key $targetCertKey;

        location / {
            proxy_pass http://$targetBackend;
        }

    }

}
stream {

    ssl_password_file password.txt;

    map $ssl_server_name $targetBackend {
        test1.example.com  127.0.0.1:8080;
        test2.example.com 127.0.0.1:8081;
    }

    map $ssl_server_name $targetCert {
        test1.example.com cert1/server.cert.pem;
        test2.example.com cert2/server.cert.pem;
    }

    map $ssl_server_name $targetCertKey {
        test1.example.com cert1/server.key.pem;
        test2.example.com cert2/server.key.pem;
    }


    server {
      listen 8443 ssl;
      ssl_protocols       TLSv1.2;
      ssl_certificate     $targetCert;
      ssl_certificate_key $targetCertKey;

      proxy_pass $targetBackend;
    }
}

可以参考下面的链接。https://gist.github.com/kekru/c09dbab5e78bf76402966b13fa72b9d2

nginx plus 可以配置lazy load, 配置更方便一些。

server {
   listen 443 ssl;

   ssl_certificate      /etc/ssl/$ssl_server_name.crt; # Lazy load from SNI   
   ssl_certificate_key  /etc/ssl/$ssl_server_name.key; # ditto                    
   ssl_protocols        TLSv1.3 TLSv1.2 TLSv1.1;
   ssl_prefer_server_ciphers on;

   location / {
       proxy_set_header Host $host;
       proxy_pass http://my_backend;
   }
}

https://www.infoq.com/news/2019/04/nginx-plus-release-18/

openresty 可以通过 ssl_certificate_by_lua_block 进行配置, 可以参考下面的链接。

https://github.com/openresty/lua-resty-core/blob/master/lib/ngx/ssl.md#raw_client_addr

 

参考及引用

https://jvns.ca/blog/2016/07/14/whats-sni/

https://en.wikipedia.org/wiki/Server_Name_Indication#Security_implications

 

图片from李華欽

 

 

 

Comments are closed.