代理中转链路出现SSL handshake
代理中转链路里的 SSL handshake failed 报错,最常见原因是中转服务与上游服务器支持的 TLS 版本不一致,比如服务端只开了 TLS 1.2,而客户端默认尝试 TLS 1.3,或者反向代理把低版本 TLS 拒掉了。
本文会带你从报错日志入手,逐步定位 TLS 版本兼容问题,并给出 Nginx、HAProxy 两种常见中转场景的调优配置和验证方法。
整个过程不需要很深的基础,照着执行即可。
先确认报错来自哪一段链路
中转链路通常分两段:客户端到中转服务器、中转服务器到上游。SSL handshake failed 可能发生在任一段。
建议先把日志级别调高,再分别测试。
以 Nginx 为例,检查错误日志:
tail -f /var/log/nginx/error.log
看到类似 SSL_do_handshake() failed 或 tls_process_client_hello:no suitable signature algorithm 时,说明是客户端与 Nginx 之间握手失败。
如果日志里写的是 upstream SSL certificate verify failed,则问题出在 Nginx 与上游之间。
再用 OpenSSL 命令直接验证上游支持的 TLS 版本:
# 替换成你的上游域名和端口
openssl s_client -connect upstream.example.com:443 -tls1_2
openssl s_client -connect upstream.example.com:443 -tls1_3
如果某条命令报错或超时,说明该 TLS 版本没有被上游支持。
这是判断兼容范围最快的方式。
调整 TLS 版本策略,优先用现代版本
确认哪一段出问题后,调整对应服务端的 TLS 协议版本。
注意不要盲目关闭旧版本,先擦清客户端是什么系统、什么库发的请求。
Nginx 中在 server 块里配置:
server {
listen 443 ssl;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
}
如果客户端比较老,必须兼容 TLS 1.0/1.1,可以临时加上,但建议只在内部链路使用:
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
HAProxy 中转场景则在 bind 行指定 TLS 版本范围:
frontend relay_in
bind *:443 ssl crt /etc/haproxy/certs/relay.pem alpn h2,http/1.1
ssl-default-bind-options no-sslv3
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384
修改配置后,务必先检测语法再重载服务:
nginx -t
systemctl reload nginx
haproxy -c -f /etc/haproxy/haproxy.cfg
systemctl reload haproxy
避开这几个容易踩的坑
调 TLS 版本时,很多人改了 ssl_protocols 却没重启服务,或者只改了中转服务器,忽略了上游服务也限制 TLS 版本。
还有几种情况特别容易误判:
- 证书链不完整:上游返回的证书缺少中间证书,也会导致握手失败,但不一定是 TLS 版本问题。用
openssl s_client -showcerts检查证书链。 - SNI 缺失:某些中转配置没有转发 SNI,导致上游无法匹配证书。Nginx 里需要设置
proxy_ssl_server_name on;,HAProxy 里用use-server或确保req.ssl_sni生效。 - OpenSSL 版本过老:系统自带的 OpenSSL 如果太旧,即使配置了
TLSv1.3也不生效。运行openssl version确认支持情况。 - 防火墙或安全组放行:中转服务器如果只放行了 80 端口,443 握手包被丢弃也会报同样的错。用
telnet 域名 443或nc -vz 域名 443测试连通性。
高频疑问快速排查
有读者问:为什么两边都开了 TLS 1.2,还是握手失败?
这种情况通常是密码套件不匹配。
服务端启用了 ssl_prefer_server_ciphers on,但客户端不认列表里的加密套件,就会握手失败。
可以用 openssl ciphers -v 'ECDHE-ECDSA-AES128-GCM-SHA256' 验证本地是否支持该套件。
也有人问:能不能直接关闭 TLS 1.0 和 1.1 来避免老版本漏洞?
可以。
如果业务客户端都是新版系统或浏览器,关闭 TLS 1.0/1.1 是安全基线要求。
判断方法很简单,观察日志里客户端所用的版本,或者用抓包工具确认。
还有一类情况是上游源站是 CDN,CDN 边缘节点 TLS 策略频繁调整。
此时建议把中转配置里的 TLS 版本范围放宽一点,先用 TLSv1.2 TLSv1.3 测试,再逐步收紧。
用真实请求验证调优结果
配置完成后,不要只看日志为空,直接用实际请求验证。
如果你用的是 curl,可以指定 TLS 版本测试:
curl -I --tlsv1.2 https://your-relay-domain.com
curl -I --tlsv1.3 https://your-relay-domain.com
两条命令都返回 HTTP/2 200 之类的状态码,说明对应 TLS 版本握手正常。
如果某个版本失败,再执行:
curl -v --tlsv1.2 https://your-relay-domain.com 2>&1 | grep -i error
同时关注 Nginx 错误日志是否还出现 SSL handshake failed。
持续观察 5-10 分钟,确认没有新的报错后,再把它加入监控告警。
代理中转链路出现 SSL 握手失败并不可怕,多数原因是 TLS 版本或密码套件配置不一致。
按本文步骤先分段定位,再调整 ssl_protocols、ssl_ciphers 和相关 SNI 参数,最后用 curl 验证,就能稳定解决。
如果你在处理过程中遇到其他异常,优先查看中转服务器与上游的完整握手日志,通常都能找到明确提示。