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.