服务器和代理

SSL handshake failed(SSL 握手失败):按客户端和服务端排查

作者: · 最后核实

报错信息SSL handshake failed

“SSL handshake failed” 的意思是:客户端和服务器没能把 TLS 连接建立起来,没有任何内容被加密,请求也没有发出去。 这是一个笼统的提示:Cloudflare 把它显示为 Error 525,各种库和日志里也常打印这句话,每个浏览器和 App 还有自己更具体的错误代码。修之前先弄清是谁报的错、握手卡在哪一步。

先分清你是访客还是站长

访客先确认是不是网站的问题,站长先找到具体报错和同一时间的服务器日志。

普通访客: 先用 SSL 检测 从外部测一下网站。外部也失败,说明是网站的问题,只能等站长修;外部正常,就依次试换手机流量、换浏览器、关闭杀毒软件的 HTTPS 扫描,哪一步之后好了,原因就在哪。

站长: 先看用户报的是哪种具体错误,再看服务器错误日志在同一时间有没有记录。只有部分用户失败,多半和他们的设备、网络有关;所有人都失败,基本是证书、端口或协议配置的问题。最近改过配置、续过证书、换过 CDN 的,优先从这些改动查起。

App 开发者: 把完整的异常信息记下来,尤其是冒号后面的具体原因和底层错误码。同一个接口,浏览器正常而 App 失败,最常见的原因是服务器缺中间证书,或者 App 运行环境的信任库和电脑不一样。

握手可能在哪一步失败

TLS 握手分几个步骤,每一步都有典型的失败方式,详细过程见 TLS 握手过程详解。

步骤 发生了什么 典型失败
1. 建立连接 和 443 端口建立 TCP 连接 端口没开、防火墙拦截,或者端口上跑的是明文 HTTP
2. ClientHello 客户端报出支持的版本、加密套件和 SNI 域名 没有共同的版本或套件;没有这个域名的证书
3. 服务器证书 服务器发送证书链 过期、不受信任、域名不对、缺中间证书
4. 客户端证书(可选) 服务器要求客户端出示证书 没有客户端证书,或者证书被拒绝
5. Finished 确认密钥,开始加密通信 代理或杀毒软件干扰、连接被重置

发现问题的一方通常会发送一条警报。RFC 9846 定义的警报包括 handshake_failure(40)、protocol_version(70)、unrecognized_name(112)、certificate_required(116)等。

客户端这边的报错

客户端报的具体代码能直接指出握手卡在哪一步,按下表找到对应页面。

浏览器

报错 对应步骤 详细页面
ERR_SSL_PROTOCOL_ERROR(此网站无法提供安全连接) 1–2 ERR_SSL_PROTOCOL_ERROR
ERR_SSL_VERSION_OR_CIPHER_MISMATCH 2 协议或套件不匹配
ERR_SSL_UNRECOGNIZED_NAME_ALERT 2 unrecognized name 警报
SSL_ERROR_HANDSHAKE_FAILURE_ALERT 2 或 4 Firefox 握手失败警报
SSL_ERROR_NO_CYPHER_OVERLAP 2 没有共同加密算法
SSL_ERROR_RX_RECORD_TOO_LONG 1 record too long
PR_END_OF_FILE_ERROR 1–5 PR_END_OF_FILE_ERROR
ERR_BAD_SSL_CLIENT_AUTH_CERT 4 客户端证书被拒绝
NET::ERR_CERT_AUTHORITY_INVALID、DLG_FLAGS_INVALID_CA 3 签发机构不受信任、IE 模式
Safari 无法建立安全连接 1–2 Safari

第 3 步的证书问题,浏览器一般显示的是 您的连接不是私密连接 这类警告页,而不是单纯的握手错误。

国内的 360、QQ 等双核浏览器:极速模式用 Chromium 内核,报错代码和 Chrome 相同;兼容模式用 IE 内核,证书问题显示为 IE 风格的页面,代码是 DLG_FLAGS_* 这一类。

App、编程语言和命令行工具

报错 详细页面
NSURLErrorDomain Code=-1200 iOS 安全连接失败
NSURLErrorDomain Code=-1202 iOS 证书不受信任
Flutter HandshakeException ... CERTIFICATE_VERIFY_FAILED Flutter
安卓 Trust anchor for certification path not found 安卓信任锚
javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure Java handshake_failure
Python SSLEOFError / WRONG_VERSION_NUMBER EOF occurred in violation of protocol、wrong version number
curl: (35) curl 35
curl: (60) curl 60

基于 OpenSSL 的工具里,服务器发来的 handshake_failure 警报在 OpenSSL 1.1.1 和 3.0 中显示为 sslv3 alert handshake failure,更新的版本写成 ssl/tls alert handshake failure。

服务端、代理和 CDN 这边的报错

服务端的报错写在 Web 服务器、代理和 CDN 的日志或错误页里,同样能对应到具体原因。

位置 报错 详细页面
Cloudflare Error 525: SSL handshake failed Cloudflare 525
Cloudflare Error 526: invalid SSL certificate Cloudflare 526
nginx SSL_do_handshake() failed、no shared cipher、peer closed connection in SSL handshake 见下文
Apache 启动时的 AH02572、AH01909 AH02572、AH01909
IIS A specified logon session does not exist IIS 绑定证书
Windows Schannel 事件 Schannel 错误

nginx 把很多由客户端引起的握手失败,包括 no shared cipher、不支持的协议版本和收到的警报,都记在 info 级别。默认的 error_log ... error; 下完全看不到。排查时临时调高:

error_log /var/log/nginx/error.log info;

宝塔面板的站点日志也是 nginx 的错误日志,同样需要改日志级别才能看到这些记录。用了阿里云、腾讯云 CDN 的网站,CDN 用 HTTPS 回源时和源站之间也要握手,源站证书或协议配置有问题,表现为 CDN 节点报错而不是浏览器报错,排查思路和 Cloudflare 525 一样。

从外部诊断

从外部跑三条 openssl 命令,就能判断失败出在端口、协议、证书还是 SNI。在出问题的网络之外的机器上执行:

openssl s_client -connect example.com:443 -servername example.com -brief </dev/null
openssl s_client -connect example.com:443 -servername example.com -tls1_2 -brief </dev/null
openssl s_client -connect example.com:443 -servername example.com -tls1_3 -brief </dev/null
  • 出现 wrong version number 或 packet length too long:端口上跑的不是 TLS(第 1 步)。
  • 收到 handshake failure、protocol version 之类的警报:版本、套件或客户端证书问题(第 2、4 步)。
  • 握手完成但 Verify return code 不是 0:证书问题(第 3 步)。
  • 不带 -servername 能成功、带上反而失败,或者反过来:SNI 配置问题。

SSL 检测 会做同样的测试,并显示支持的协议和证书链。协议和套件配置从 TLS 配置生成器 开始改;证书链问题见 缺少中间证书。

怎么确认已经修好

三条 openssl 命令都完成握手、原来报错的客户端恢复正常,就算修好。

  1. 上面三条 openssl s_client 命令都能完成握手,或者只在你有意关闭的版本上失败。
  2. 原来报错的客户端在原来的网络里能正常连接。
  3. info 级别日志开一段时间,服务器日志里不再出现来自真实客户端的握手错误,然后把日志级别恢复原样。

常见问题

SSL 握手失败一定是证书的问题吗?

不一定。证书问题只是其中一类原因,很多握手失败发生在检查证书之前,比如端口上没开 TLS、双方没有共同的加密套件。看具体的错误代码就知道属于哪一类。

现在不是都用 TLS 了吗,为什么还叫 SSL?

严格来说现在的连接用的都是 TLS,通常是 1.2 或 1.3,SSL 3.0 及更早的版本早已淘汰。只是报错信息和各种库沿用了老名字,所以“SSL handshake failed”还到处可见。

只有部分用户握手失败,从哪里查起?

比较这些用户有什么不同:老设备(缺根证书、不支持 TLS 1.2)、公司网络或杀毒软件在拦截 TLS、客户端不发送 SNI。临时把服务器日志级别调到 info,再按时间点对照。

最快的单项测试是什么?

在网络外部的机器上执行 openssl s_client -connect example.com:443 -servername example.com -brief,再用 SSL 检测 测一次。两者都成功,问题就在报错的那个客户端或它所在的网络。

资料来源

  1. RFC 9846: TLS 1.3 协议
  2. Chromium net_error_list.h
  3. NSS SSLerrs.h(Firefox TLS 错误)
  4. nginx 源码:ngx_event_openssl.c
  5. Cloudflare: Error 525
  6. 360 安全浏览器帮助:浏览器内核控制标签 meta 说明