nginx 代理 https,temporarily disabled while SSL handshaking to upstream 出现 502 的情况

网上看到这篇文章,这里记录一下。

前言

        前段时间我们官网要做一个活动页面, 但是活动页面是用另一个活动页域名, activity.example.com, 但是运营人员需要对外展示的落地页是以官网 www 的域名来处理,所以这时候就会需要在官网的 nginx 指向那边进行页面的代理转发:

1
2
3
4
location  /promo/student-discount {
    resolver 8.8.8.8;
    proxy_pass https://activity.example.com/promo/student-discount;
}

        但是实测的过程中, 却发现代理转发的时候,报了一个 502 的错误

1
2
2022/08/16 11:58:22 [error] 2293#0: *213338285 SSL_do_handshake() failed (SSL: error:1408F10B:SSL routines:ssl3_get_record:wrong version number) while SSL handshaking to upstream, client: 14.xxx.1.86, server: www.example.com, request: "HEAD /promo/student-discount HTTP/1.1", upstream: "https://13.xxx.xxx.101:443/promo/student-discount", host: "www.example.com"
2022/08/16 11:58:22 [warn] 2293#0: *213338285 upstream server temporarily disabled while SSL handshaking to upstream, client: 14.xxx.1.86, server: www.example.com, request: "HEAD /promo/student-discount HTTP/1.1", upstream: "https://13.xxx.125.101:443/promo/student-discount", host: "www.example.com"

        看了一下,应该是 nginx 在进行代理请求的时候,就报错了, 应该是 ssl 的握手的错误 SSL_do_handshake()

SNI介绍

        SNI(Server Name Indication)是为了解决一个服务器使用 多个域名 和证书的TLS扩展,主要解决一台服务器只能使用一个证书的缺点。

        开启SNI后,允许客户端在发起SSL握手请求时就提交请求的域名信息, 负载均衡 收到SSL请求后,会根据域名去查找证书,如果找到域名对应的证书,则返回该证书;如果没有找到域名对应的证书,则返回缺省证书。负载均衡在配置HTTPS监听器支持此功能,即支持绑定多个证书。

        SNI(Server Name Indication)是TLS的扩展,用来解决一个服务器拥有多个域名的情况。

        在客户端和服务端建立HTTPS的过程中要先进行TLS握手,握手后会将HTTP报文使用协商好的密钥加密传输。

        在TLS握手信息中并没有携带客户端要访问的目标地址。这样会导致一个问题,如果一台服务器有多个 虚拟主机 ,且每个主机的域名不一样,使用了不一样的证书,该和哪台虚拟主机进行通信?

        和HTTP协议用来解决服务器多域名的方案类似,HTTP在请求头中使用Host字段来指定要访问的域名。TLS的做法,也是加Host,在TLS握手第一阶段ClientHello的报文中添加。

        SNI在TLSv1.2开始得到支持。从OpenSSL 0.9.8版本开始支持。所以基本市场上的终端设备都支持。

分析

        因为我这个活动页程序是部署在 aws 的 s3 上的,并且使用 cloudfront 来做 cdn。 所以去查了一下 cloudfront 的文档,确实有对应的文档:

如果您使用自定义源并将 CloudFront 配置为要求 CloudFront 与源之间使用 HTTPS,则问题可能在于域名不匹配。在源上安装的 SSL/TLS 证书的公用名字段中包含一个域名,使用者备用名称字段中可能包含更多域名。(CloudFront 支持证书域名中的通配符。) 证书中必须有一个域名与下面的一个或两个值匹配:

您为分配中适用源的源域名指定的值。

  • 如果您将 CloudFront 配置为将 Host 标头转发到您的源,则为该 Host 标头的值。有关将 Host 标头转发到源的更多信息,请参阅根据请求标头缓存内容。
  • 如果域名不匹配,SSL/TLS 握手将失败,CloudFront 将返回 HTTP 状态代码 502(无效网关)并将 X-Cache 标头设置为 Error from cloudfront。

要确定证书中的域名是否与分配或 Host 标头中的 Origin Domain Name 匹配,可以使用在线 SSL 检查程序或 OpenSSL。

AmazonCloudFrontdocs.aws.amazon.com/zh_cn/AmazonCloudFront/latest/DeveloperGuide/http-502-bad-gateway.html

他的文档中也有提供用 openssl 的测试方式:

要帮助纠正来自 CloudFront 的 HTTP 502 错误,您可以使用 OpenSSL 尝试与源服务器建立 SSL/TLS 连接。如果 OpenSSL 无法建立连接,则可能表明源服务器的 SSL/TLS 配置出错。如果 OpenSSL 能够建立连接,它将返回有关源服务器证书的信息,包括证书的公用名称(Subject CN 字段)和使用者备用名称(Subject Alternative Name 字段)。

使用以下 OpenSSL 命令测试与源服务器的连接(将源域名 替换为源服务器的域名,如 example.com):

openssl s_client -connect origin domain name:443

如果满足以下条件:

  1. 您的源服务器支持具有多个 SSL/TLS 证书的多个域名
  2. 您的分配已配置为将 Host 标头转发到源

然后,将 -servername 选项添加到 OpenSSL 命令中,如以下示例所示(将 CNAME 替换为分配中配置的 CNAME):

openssl s_client -connect origin domain name:443 -servername CNAME

AmazonCloudFrontdocs.aws.amazon.com/zh_cn/AmazonCloudFront/latest/DeveloperGuide/http-502-bad-gateway.html

而我们的官网的原服务器,其实是有存在多个 tls 证书的域名的,比如:

  • www.example.com
  • example.at
  • example.info

所以确实符合上述的第一点条件。

使用 openssl 实验

既然是 ssl 的握手错误,那就可以用 openssl 尝试握手一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[kbz@VM-16-9-centos ~]$ openssl version
OpenSSL 1.0.2k-fips  26 Jan 2017


[kbz@VM-16-9-centos ~]$ openssl s_client -connect activity.example.com:443
CONNECTED(00000003)
139703956457360:error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol:s23_clnt.c:794:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 7 bytes and written 289 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : 0000
    Session-ID: 
    Session-ID-ctx: 
    Master-Key: 
    Key-Arg   : None
    Krb5 Principal: None
    PSK identity: None
    PSK identity hint: None
    Start Time: 1660726052
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
---

发现确实连证书都不给返回。 跟文章说的效果一样, 接下来我们加上 -servername:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[kbz@VM-16-9-centos ~]$ openssl s_client -connect activity.example.com:443 -servername activity.example.com
CONNECTED(00000003)
depth=3 C = US, O = "The Go Daddy Group, Inc.", OU = Go Daddy Class 2 Certification Authority
verify return:1
depth=2 C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", CN = Go Daddy Root Certificate Authority - G2
verify return:1
depth=1 C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", OU = http://certs.godaddy.com/repository/, CN = Go Daddy Secure Certificate Authority - G2
verify return:1
depth=0 CN = *.example.com
verify return:1
---
Certificate chain
 0 s:/CN=*.example.com
   i:/C=US/ST=Arizona/L=Scottsdale/O=GoDaddy.com, Inc./OU=http://certs.godaddy.com/repository//CN=Go Daddy Secure Certificate Authority - G2
 1 s:/C=US/ST=Arizona/L=Scottsdale/O=GoDaddy.com, Inc./OU=http://certs.godaddy.com/repository//CN=Go Daddy Secure Certificate Authority - G2
   i:/C=US/ST=Arizona/L=Scottsdale/O=GoDaddy.com, Inc./CN=Go Daddy Root Certificate Authority - G2
 2 s:/C=US/ST=Arizona/L=Scottsdale/O=GoDaddy.com, Inc./CN=Go Daddy Root Certificate Authority - G2
   i:/C=US/O=The Go Daddy Group, Inc./OU=Go Daddy Class 2 Certification Authority
 3 s:/C=US/O=The Go Daddy Group, Inc./OU=Go Daddy Class 2 Certification Authority
   i:/C=US/O=The Go Daddy Group, Inc./OU=Go Daddy Class 2 Certification Authority
---
Server certificate
-----BEGIN CERTIFICATE-----

发现确实是可以了。

解决

既然知道问题了,其实就很好解决,就是在 nginx 转发代理的时候,加上 proxy_ssl_server_name on

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.

        事实上,只要是需要针对 SNI 返回 server name 的后端服务,都会需要 nginx 代理转发的时候携带这个配置。

所以最后改成:

1
2
3
4
5
location  /promo/student-discount {
    resolver 8.8.8.8;
    proxy_ssl_server_name on;
    proxy_pass https://activity.example.com/promo/student-discount;
}

这样子就正常了。


参考资料:

  • CloudFront 与自定义源服务器之间的 SSL/TLS 协商失败
  • nginx反向代理https访问502
  • proxy_ssl_server_name

转自:

记一次 nginx 转发代理 https 出现 502 的情况 | Zach Ke's Notes

你可能感兴趣的:(Nginx,iphone,ios)