TLS问题背景
最近从Nginx向Apisix的迁移过程中,遇到一个TLS的问题,从现象来看,部分的内部和外部的用户向我们反馈java应用客户端在请求https链接的时候,出现握手失败,客户端的报错如下次:
javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
该问题不太好排查,仅有部分应用存在这个问题,而且应用都部署在K8S中,不方便进行调试。简单的使用CURL进行调用没办法重现问题,找java后端的同事一起进行了联合调试,即使开启debug,也并没有发现任何有用的信息。
TLS基本概念
在更进一步深入排查这个问题之前,我们先去了解下TLS, 之前写过一篇入门的文章 数据仓库之安全系列TLS基本概念, 主要是关于TLS的一些基本概念以及证书等,我们这次排查的问题主要涉及到TLS握手阶段的问题:
TLS协议在协议栈中处于HTTP之下:
TLS客户端和服务端的握手流程如下:
简单来说握手的过程就是client和server端互say hello,确定TLS版本,加密套件等信息后,然后交换证书,秘钥等信息,最后生成了后面应用层会使用到的秘钥。这个图实际上是一个最完整的握手流程,实际生产环境中比这个流程要简单的多,比如说大多数情况下,服务端并不会去验证客户端证书,有些java 客户端的默认配置甚至都不会去验证服务端的证书。下图将可选的步骤标记了*。
SNI 问题初步排查
经过初步排查,我们和开发的怀疑可能和这篇文章描述的问题相似
https://blog.csdn.net/xiao__jia__jia/article/details/123752327
是因为在客户端没有传递SNI信息导致的,开发那边同事采用了强制关闭SNI的方式配合我们调试来模拟生产环境遇到的问题
System.setProperty("jsse.enableSNIExtension", "false");
我们在Apisix服务端抓包来看看
果然是在Client Hello的请求中没有SNI字段,在APISIX服务端直接返回了Fatal Internal error
SNI基础知识
SNI(Server Name Indication)是TLS协议的扩展,允许客户端在发起SSL握手请求时, 具体说来,是客户端发出SSL请求中的ClientHello阶段, 提交请求的Host信息,使得服务器能够切换到正确的域并返回相应的证书。这样网关服务器就可以在相同的IP地址和TCP端口号上支持不同的HTTPS域名和证书了。
SNI需要客户端和服务端都进行支持,在服务端我们可以很容易的基于Openresty指令进行确认:
~]# /usr/bin/openresty -V|grep SNI
nginx version: openresty/1.21.4.1
built by gcc 9.3.1 20200408 (Red Hat 9.3.1-2) (GCC)
built with OpenSSL 1.1.1s 1 Nov 2022
TLS SNI support enabled
这里还需要注意的是,有的同学问到我们在https 请求的URL中已经有了需要请求的服务器名在请求的Header中也有设置,为什么这里还需要设置, 这里需要理解2个点:
1. TLS协议是在HTTPS层下面的是没办法获取到https层面的相关信息的。
2. 我们看看client和server建立Https连接的过程: 先建立tcp连接 -->经过TLS握手-->实现https通信-->进而发送HTTP请求 , 从请求过程来看在TLS握手阶段, server端'也没有办法获取七层HTTP报文的信息
Apisix SNI缺失的问题解决方案
针对SNI缺失的问题,最简单的解决方案就是修改客户端,升级JDK版本,httpclient的版本等,但我们希望是在网关层面解决这个问题,客户端不需要修改代码。我们找到了一篇雪球写的相关文章
https://blog.csdn.net/singgel/article/details/122701839
通过配置fallback_sni的域名,在客户端没有传递SNI的情况下使用这个作为默认SNI:
fallback_sni: "my.default.domain" # If set this, when the client doesn't send SNI during handshake, the fallback SNI will be used instead
设置之后,经测试, 在TLS1.2的情况下,客户端不设置SNI也是可以完成握手的。
这里顺便研究一下,我们的Nginx之前也没有任何特殊问题,也没有单独设置过fallback sni, 为什么没有问题呢?因为对于Nginx,如果客户端在ssl握手阶段未携带server_name Nginx就会去找默认server,然后使用默认server的ssl证书来响应, 因为我们使用的泛域名证书,默认server的ssl证书也是可以匹配上的。
SSLV2 未解决的问题
SNI的问题解决之后,我们仍有部分客户端调用有问题,通过抓包发现:
客户端在进行client hello握手的时候使用的是SSLV2,通过和开发同事一起调试发现代码中是这样设置的协议
supportedProtocols = {"SSLv2Hello", "SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2"};
SSLv2 hello仅仅是用来发送hello 信息的,也就是我们截图中的这个包,我们在Apisix中协议层面配置了相应的协议
ssl_protocols: SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2 TLSv1.3
测试以后,结果仍然一样。
这个问题暂时没有在Apisix端找到解决方案,和开发同学联调确定的解决方案是在代码中移除过于老的协议的支持,仅保留TLSv1.1, TLS v1.2
supportedProtocols = {"TLSv1.1", "TLSv1.2"};
小结
网关作为我们整个系统的入口,承担着越来越多的作用,其主要目的还是将一些业务应用通用功能提取出来放在一起进行统一处理,比如TLS,跨域,限流,分流等,让后端专注于业务逻辑的处理。网关的很多问题,既涉及前端又涉及到后端,这次TLS问题的就比较曲折,TLS 问题本身涉及到安全,网络协议,在后端能够提供信息有限的情况下,还需要我们自己去抓包去定位问题。
微信扫一扫
关注该公众号