网上看到这篇文章,这里记录一下。
前段时间我们官网要做一个活动页面, 但是活动页面是用另一个活动页域名, 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(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
如果满足以下条件:
- 您的源服务器支持具有多个 SSL/TLS 证书的多个域名
- 您的分配已配置为将 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 证书的域名的,比如:
所以确实符合上述的第一点条件。
既然是 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; } |
这样子就正常了。
参考资料:
转自:
记一次 nginx 转发代理 https 出现 502 的情况 | Zach Ke's Notes