处理方式
在 nginx 配置中增加如下配置即可
proxy_ssl_server_name on;
复现现象
通过 nginx 代理 https://abc.nestealin.com/ 至 https://abc.nestealin.com/index.html 时出现 502,配置如下:
server {
listen 80;
server_name abc.nestealin.com ;
rewrite ^(.*)$ https://$host$1 permanent;
}
server {
listen 443 ssl http2;
server_name abc.nestealin.com ;
charset utf-8;
access_log logs/$http_host.access.main.log main;
error_log logs/abc.nestealin.com.error.log error;
ssl_certificate /data/keys/server.cer;
ssl_certificate_key /data/keys/server.key;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 ;
set $origin_root /data/web_static/nes_hexo_blog/;
location = / {
proxy_pass https://abc.nestealin.com/index.html;
}
location / {
root $origin_root ;
if (!-e $request_filename) {
rewrite ^/(.*)$ / redirect;
}
}
}
报错如下:
直接访问 https://abc.nestealin.com/index.html 一切正常
初步判断问题出在正向代理的 proxy_pass 中。
排查过程
日志分析
从访问日志来看:
108.162.215.99:45784 443 - [23/Aug/2021:20:12:55 +0800] "GET / HTTP/2.0" 502 552 588 "https://abc.nestealin.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" 112.94.5.214 104.21.47.158:443, 172.67.148.224:443, [2606:4700:3031::6815:2f9e]:443, [2606:4700:3033::ac43:94e0]:443 502, 502, 502, 502 0.004, 0.025, 0.000, 0.000 0.030 abc.nestealin.com "- - -" "112.94.5.214"
可见 nginx 接收请求后,相继尝试请求
104.21.47.158:443, 172.67.148.224:443, [2606:4700:3031::6815:2f9e]:443, [2606:4700:3033::ac43:94e0]:443
四个节点均被返回 502 状态码。从错误日志来看:
2021/08/23 20:12:55 [error] 10501#0: *14158 SSL_do_handshake() failed (SSL: error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure) while SSL handshaking to upstream, client: 108.162.215.99, server: abc.nestealin.com, request: "GET / HTTP/2.0", upstream: "https://104.21.47.158:443/index.html", host: "abc.nestealin.com", referrer: "https://abc.nestealin.com/"
可见在正向代理请求 104.21.47.158:443 时在 SSL 握手阶段就已经断开。
抓包分析
目前只知道HTTPS握手不通过,接下来我们抓包对比下。
SSL 握手过程
- 方便后续从抓包内容分析握手被拦截的阶段。
请求异常
- 客户端在发出
Client Hello
后就被返回Handshake Failure
,得不到Server Hello
响应就已经结束连接
正常请求
加上
proxy_ssl_server_name on;
配置恢复访问后,继续抓包分析。location = / { proxy_ssl_server_name on; proxy_pass https://abc.nestealin.com/index.html; }
再次请求时,对比发现在
Client Hello
阶段新增了一个扩展类型Extension: sever_name
,内容是一个Server Name Indication extension
即 SNI但此时已经可以通过第一阶段,得到
Server Hello
响应
- 此时,可以大致知道在连接过程中因为请求头没有携带域名,导致请求节点无法匹配证书进行握手交互。
请求验证
如何才能得到Server Hello
通过 openssl s_client 对节点进行请求测试
不携带域名直接请求 Cloudflare 节点的443端口,发现无法匹配证书
/usr/local/openssl111/bin/openssl s_client -connect 104.21.47.158:443 -tls1_2
CONNECTED(00000003) 4639555072:error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure:ssl/record/rec_layer_s3.c:1528:SSL alert number 40 --- no peer certificate available --- No client certificate CA names sent ---
不携带域名直接请求 Baidu 节点的443端口,发现可以正常匹配证书
/usr/local/openssl111/bin/openssl s_client -connect 14.215.177.38:443 -tls1_2
CONNECTED(00000003) depth=2 C = BE, O = GlobalSign nv-sa, OU = Root CA, CN = GlobalSign Root CA verify return:1 depth=1 C = BE, O = GlobalSign nv-sa, CN = GlobalSign Organization Validation CA - SHA256 - G2 verify return:1 depth=0 C = CN, ST = beijing, L = beijing, OU = service operation department, O = "Beijing Baidu Netcom Science Technology Co., Ltd", CN = baidu.com verify return:1 ---
此时基本可以确定节点只以 SNI 方式匹配证书,所以这次携带域名直接请求 Cloudflare 节点的443端口
/usr/local/openssl111/bin/openssl s_client -connect 104.21.47.158:443 -tls1_2 -servername abc.nestealin.com
CONNECTED(00000003) depth=2 O = Digital Signature Trust Co., CN = DST Root CA X3 verify return:1 depth=1 C = US, O = Let's Encrypt, CN = R3 verify return:1 depth=0 CN = *.nestealin.com verify return:1
由上可知,Cloudflare节点需要带上请求主机,就可以进行证书校验。
为什么代理其它节点不会提示502,例如 Baidu、Aliyun
同样从上面试验不难发现,直接请求 Baidu 节点IP 的 443 端口是可以得到默认证书的响应,而 Cloudflare 则匹配不到或不传递域名,则不响应任何证书,这点就取决于各家 SNI 做法( SNI原理参考链接 )。
从而也就说明为何 Nginx 代理 Cloudflare 时会因握手失败导致 502 了。
结论
Cloudflare 节点因为只以 SNI 方式支持证书校验,进行 TLS 握手,导致通过 nginx 做正向代理时,需要加上 proxy_ssl_server_name on;
配置来支持代理请求时可以将请求主机名塞入 SNI 字段,才能进行 TLS 握手。
官方配置说明
- 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.
相关链接
proxy_ssl_server_name 官方文档
HTTPs入门, 图解SSL从回车到握手
wireshark抓包分析SSL/TLS协议
OpenSSL: Check SSL Certificate Expiration Date and More