背景

最近在写一个管理后台,在参考阿里云CDN交互的时候,一个叫回源SNI的参数项引起了我的好奇

image-20210513190524400

好奇的原因倒不是因为不知道SNI是什么,而是因为工作中反向代理后端用Https的情况并不少,但从来没留意过有相关需要特别配置的,于是查了下Nginx对应的参数是啥(这里的有个背景就是阿里的CDN后边是Tengine也就是Nginx)经过一番查询发现是proxy_ssl_server_name这个参数

1
2
3
4
5
Syntax:	proxy_ssl_server_name on | off;
Default:	
proxy_ssl_server_name off;
Context:	http, server, location
This directive appeared in version 1.7.0.

Enables or disables passing of the server name through TLS Server Name Indication extension (SNI, RFC 6066) when establishing a connection with the proxied HTTPS server.

解释很直白就是当端服务器为Https,反向代理时是否开启SNI,但是,但是它的默认值竟然是off,没错默认为关闭。这就有点不解了,默认是关闭状态那后端服务器是怎样判断请求的是哪个vhost呢,也就有可能使用不匹配的证书,现实是工作中好像并没有出现这种情况。思绪凌乱中引起了我对这个参数默认值的怀疑,难道是文档不对还是或者逻辑不对。

于是我从github上下了一份nginx源码,虽然不能全看懂但不影响分析程序逻辑,查了下SNI的处理必调用SSL_set_tlsext_host_name函数,所以过滤下SSL_set_tlsext_host_name就能找到关键信息

image-20210513194354048

源码推翻了我的质疑,参数默认关闭状态,开启时就会启用SNI,在SSL握手的时候会把Host信息带给后端服务器,后端服务就能知道用哪个证书文件。

测试

既然是这样,平常在使用的过程中并没有特别启用这个参数那为什么也能正常使用呢?只能再做实验了。

1
2
3
4
5
6
7
    server {
        listen       80;
        server_name  www.dianduidian.com;
        location / {
          proxy_pass https://www.taobao.com;
        }
    }

访问能正常显示淘宝首页

换成ip再来

1
2
3
4
5
6
7
    server {
        listen       80;
        server_name  www.dianduidian.com;
        location / {
          proxy_pass https://27.211.197.172;
        }
    }

image-20210513201517282

这时显示403,很好理解,因为Host不对,指定下Host

1
2
3
4
5
6
7
8
    server {
        listen       80;
        server_name  www.dianduidian.com;
        location / {
          proxy_pass https://27.211.197.172;
          proxy_set_header Host www.taobao.com;
        }
    }

image-20210513201711121

竟然能正常打开,这时就有点怀疑:反向代理的时间不验证证书?

继续查找资料,发现还真是,

1
2
3
4
5
6
7
Syntax:	proxy_ssl_verify on | off;
Default:	
proxy_ssl_verify off;
Context:	http, server, location
This directive appeared in version 1.7.0.

Enables or disables verification of the proxied HTTPS server certificate.

默认不验证证书情况。

那我们启用下看看会发生什么?

先获取下CA文件,

1
 curl  https://curl.se/ca/cacert.pem  -o /etc/nginx/conf.d/cacert.pem

修改配置文件如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    server {
        listen       80;
        server_name  www.dianduidian.com;
        location / {
          proxy_pass https://27.211.197.172;
          proxy_set_header Host www.taobao.com;
          proxy_ssl_verify on;
          proxy_ssl_trusted_certificate /etc/nginx/conf.d/cacert.pem;          
        }
    }

image-20210513202330194

502了,喜闻乐见,说明Nginx反向代理后端服务器时默认不验证证书。

理下思路:这时因为没有开启SNI,所以淘宝的服务器不知道是哪个域名,所以服务器会返回一个默认vhost绑定的证书,证书域名不匹配,证书验证失败,日志中会打印upstream SSL certificate verify error

1
2021/05/13 20:23:08 [error] 17427#0: *12037 upstream SSL certificate verify error: (20:unable to get local issuer certificate) while SSL handshaking to upstream, client: 43.227.255.34, server: www.dianduidian.com, request: "GET / HTTP/1.1", upstream: "https://27.211.197.172:443/", host: "www.dianduidian.com"

image-20210513202603592

此时如果开启SNI,理论上就应该可以正常访问,修改配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    server {
        listen       80;
        server_name  www.dianduidian.com;
        location / {
          proxy_pass https://27.211.197.172;
          proxy_set_header Host www.taobao.com;
          proxy_ssl_verify on;
          proxy_ssl_trusted_certificate /etc/nginx/conf.d/cacert.pem; 
          proxy_ssl_server_name on;
        }
    }

并不行,依然502

1
2
3
dian.com"
2021/05/13 20:29:22 [notice] 17451#0: signal process started
2021/05/13 20:29:23 [error] 17452#0: *12039 upstream SSL certificate verify error: (20:unable to get local issuer certificate) while SSL handshaking to upstream, client: 43.227.255.34, server: www.dianduidian.com, request: "GET / HTTP/1.1", upstream: "https://27.211.197.172:443/", host: "www.dianduidian.com"

再来分析下,这时虽然启用了SNI,SSL握手的时候会传Host,但这里Upstream我们写的IP,有理由怀疑传的Host不对,所以后端服务器没有返回正确的证书,证书验证失败。那当启用了SNI,反向代理时默认传的Host是什么呢?能不能改呢?经过查询文档发现需要另外一个参数proxy_ssl_name,

1
2
3
4
5
6
7
8
9
Syntax:	proxy_ssl_name name;
Default:	
proxy_ssl_name $proxy_host;
Context:	http, server, location
This directive appeared in version 1.7.0.

Allows overriding the server name used to verify the certificate of the proxied HTTPS server and to be passed through SNI when establishing a connection with the proxied HTTPS server.

By default, the host part of the proxy_pass URL is used.

默认为$proxy_host,也就是27.211.197.172, 那我们手动指定为www.taobao.com,最终配置如下

1
2
3
4
5
6
7
8
        location / {
          proxy_ssl_server_name on;
          proxy_ssl_name www.taobao.com;
          proxy_ssl_verify on;
          proxy_ssl_trusted_certificate /etc/nginx/conf.d/cacert.pem;
          proxy_set_header Host www.taobao.com;
          proxy_pass https://106.117.214.225;
        }

OK,没问题,可以正常打开了。

总结

  1. Nginx反向代理时默认不验证后端服务器的证书(我推测是反射代理的后端服务通常为自己内部服务,所以并不太关心证书是谁颁发的,是否有效,重点是数据能加密就行了。)。
  2. Nginx反向代理(向上游建联)时默认不启用SNI(推测跟1有关,因为默认不验证证书所以也就没有发送SNI的必要)。
  3. CDN这种回源时需要走公网,对安全性要求较高的场景通常都会选择全程Https,回源时也用Https,回源这段需要走公网,为了避免中间人攻击需要验证证书就需要开启SNI了,推测这就是阿里云CDN控制台中有这个配置选项的目的。

参考

http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ssl_name

https://stackoverflow.com/questions/5113333/how-to-implement-server-name-indication-sni

https://serverfault.com/questions/394815/how-to-update-curl-ca-bundle-on-redhat

https://blog.csdn.net/u011130578/article/details/77979325

http://nginx.org/en/docs/