网上看到这篇文章,这里转载记录一下。踩坑备忘录---Nginx反向代理之server_name与ip | CQ的笔记
你是否遇到过使用 Nginx 反代网站时出现 502 Bad Gateway , 明明正常反代都没问题 , 可是反代就 502 Bad Gateway , 查看错误日志显示 :
*6565 SSL_do_handshake() failed (SSL: error:14094438:SSL routines:ssl3_read_bytes:tlsv1 alert internal error:SSL alert number 80) while SSL handshaking to upstream
初步研究问题发现是由于网站启用了 SNI , Nginx反代默认没有加入 SNI proxy_ssl_server_name on; ,Nginx 无法成功 handshake 上游的 SSL , 导致 502 Bad Gateway.
proxy_ssl_server_name
最近在写一个管理后台,在参考阿里云CDN
交互的时候,其中一个叫回源SNI的参数项引起了我的注意,
虽然知道SNI且工作中Nginx上游也有用Https的情况,但并未留意过需要做特别配置,感觉可能触发到知识盲区,一番查询后发现Nginx有一个proxy_ssl_server_name
参数与此相似。
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
。一个IP绑定多个域名很常见,为什么默认不启用呢?
光看文档我还觉得不够,还去看了下源码,SNI的处理必调用SSL_set_tlsext_host_name
函数,搜索SSL_set_tlsext_host_name
函数就能快速定位相关逻辑。
参数默认关闭,当开启时,在SSL握手的时候会把HostName
传给上游服务器,以便上游服务器知道用哪个证书。
使用已经知道了,接下来来测试下。
将我自己的博客做上游服务器做测试,拓扑如下:
-> www.dianduidian.com -> blog.dianduidian.com
先用最简单的配置
server {
listen 80;
server_name www.dianduidian.com;
location / {
proxy_pass https://blog.dianduidian.com;
}
}
没问题,能正常打开,我们抓包来看看Nginx是如何与上游服务器建立连接的。
可以看到TLS握手阶段,Nginx在向上游服务器发送Client Hello
消息时没有带上SNI信息,由此可以确认Nginx反向代理时,以HTTPS请求上游服务器时默认不启用SNI
。
继续来看看上游服务器返回的证书信息
可以看到服务器返回的不是blog.dianduidian.com域名的证书而是nginx配置的一个默认证书,这是由于没有启用SNI,所以TLS握手的时候,上游服务器不知道用那个域名的证书便使用了默认证书返回。
虽然证书返回的不对,但请求不受影响,我们推测Nginx反向代理时,以HTTPS请求上游服务器时默认不验证上游服务器返回的证书
通过查阅文档得知确实默认不验证上游证书
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
文件,
curl https://curl.se/ca/cacert.pem -o /etc/nginx/conf.d/cacert.pem
修改配置文件如下:
server {
listen 80;
server_name www.dianduidian.com;
location / {
proxy_pass https://blog.dianduidian.com;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/nginx/conf.d/cacert.pem;
}
}
502了,查看下日志,报upstream SSL certificate verify error
错误
2022/05/18 16:52:14 [error] 20325#1337445: *23 upstream SSL certificate verify error: (18:self signed certificate) while SSL handshaking to upstream, client: 127.0.0.1, server: sni.dianduidian.com, request: "GET / HTTP/1.1", upstream: "https://47.100.x.x:443/", host: "sni.dianduidian.com"
可以看到启用上游证书验证后Nginx确实会证书上游返回的证书,但是通过上边我们抓包可以看到上游返回的是一个Nginx配置的一个默认证书,这个证书是我们自签的,CA验证自然通过不了,但是如果CA是合法的,证书的CommonName
不一致又会怎样呢,会不会验证不过呢?这里测试话需要多张合法的证书,比较麻烦,我们直接从源码中寻找答案。
static void
ngx_http_upstream_ssl_handshake(ngx_http_request_t *r, ngx_http_upstream_t *u,
ngx_connection_t *c)
{
long rc;
if (c->ssl->handshaked) {
if (u->conf->ssl_verify) {
rc = SSL_get_verify_result(c->ssl->connection);
if (rc != X509_V_OK) {
ngx_log_error(NGX_LOG_ERR, c->log, 0,
"upstream SSL certificate verify error: (%l:%s)",
rc, X509_verify_cert_error_string(rc));
goto failed;
}
if (ngx_ssl_check_host(c, &u->ssl_name) != NGX_OK) {
ngx_log_error(NGX_LOG_ERR, c->log, 0,
"upstream SSL certificate does not match \"%V\"",
&u->ssl_name);
goto failed;
}
}
c->write->handler = ngx_http_upstream_handler;
c->read->handler = ngx_http_upstream_handler;
ngx_http_upstream_send_request(r, u, 1);
return;
}
if (c->write->timedout) {
ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_TIMEOUT);
return;
}
failed:
ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_ERROR);
}
从源码中可以看到,开启上游证书验证后不仅会验证证书颁发机构的合法性,且会比对证书中commonName
,即验证commonName
的一致性。
下面来打开SNI
server {
listen 80;
server_name www.dianduidian.com;
location / {
proxy_pass https://blog.dianduidian.com;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/nginx/conf.d/cacert.pem;
proxy_ssl_server_name on;
}
}
网站正常打开,这会抓包看看
可以看到TLS握手阶段,Nginx在向上游服务器发送Client Hello
信息中的扩展部分多出来一个server_name
,告诉上游服务器应该用哪个证书信息响应。可以看到这里Server Name
传的是blog.dianduidian.com
,但是上边配置中我们并没有指定传哪个域名,这一块的逻辑是怎样的呢?
查阅文档得知其实是有另外一个参数proxy_ssl_name
控制
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
,我们试着修改下
server {
listen 80;
server_name www.dianduidian.com;
location / {
proxy_pass https://blog.dianduidian.com;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/nginx/conf.d/cacert.pem;
proxy_ssl_server_name on;
proxy_ssl_name www.baidu.com;
}
}
抓包看看
可以看到Server Name
已经修改成www.baidu.com了。
例子1
http {
upstream bff-app {
server my-bff.azurewebsites.net:443;
}
server {
listen 80 default_server;
location /api {
proxy_pass https:/bff-app;
proxy_ssl_server_name on;
# Manually set Host header to "my-bff.azurewebsites.net",
# otherwise it will default to "bff-app".
proxy_set_header Host my-bff.azurewebsites.net;
}
}
}
例子2
upstream abtest_management_api_backend {
server 域名:443;
}
location ^~ /modules/abm/ {
proxy_ssl_server_name on;
proxy_ssl_name 域名;
proxy_set_header Host 域名;
proxy_pass https://abtest_management_api_backend/modules/abm/;
proxy_read_timeout 1800s;
proxy_set_header Origanization-Id qiancheng;
proxy_set_header X-Real-IP $clientRealIp;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_header X-Accel-Buffering;
}
Nginx作用反向代理与上游服务器使用HTTPS建连时,
SNI
,使用proxy_ssl_server_name on;
参数启用;proxy_ssl_verify on;
Nginx 反向代理 htpps 站点 502 排查思路 | 一小步
Module ngx_http_proxy_module
c - How to implement Server Name Indication (SNI) - Stack Overflow
ssl - How to update cURL CA bundle on RedHat? - Server Fault
OpenSSL-SNI_openssl sni_Remy1119的博客-CSDN博客
nginx documentation