本文主要记录在 Nginx 上同时开启 HTTP/2 与 TLSv1.3 后,部分浏览器出现 ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY 的排查过程,以及如何调整 ssl_ciphers 优先级来兼顾兼容性与安全性。

相关背景可先阅读站内 nginx 启用 HTTP/2 和 TLSv1.3。需要注意的是:HTTP/2 对底层 TLS 密码套件有额外限制,只打开 listen ... http2ssl_protocols ... TLSv1.3 并不够ssl_ciphers 的顺序在 ssl_prefer_server_ciphers on 时尤为关键。

问题现象

某次对公共 Nginx 做 HTTP/2 + TLSv1.3 配置升级后,服务进程与 access 日志整体正常,但部分用户反馈首页无法打开,浏览器报错:

ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY

回滚配置后恢复。随后在测试环境复现:同一份配置下,部分 Chromium 系浏览器(如当时的 QQ 浏览器)失败,另一些浏览器可正常访问。

当时升级涉及的核心变更示意如下(敏感业务域名已泛化):

# 升级前
listen 443 ssl;
ssl_ciphers                 RC4+SHA:AES128+SHA:ALL:!ADH:!EXP:!LOW:!MD5:!SSLV2:!aNULL:!NULL;
ssl_protocols               TLSv1 TLSv1.1 TLSv1.2;

# 升级后(有问题的版本)
listen 443 ssl http2;
ssl_ciphers                 RC4+SHA:AES128+SHA:ALL:!ADH:!EXP:!LOW:!MD5:!SSLV2:!aNULL:!NULL:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-256-GCM-SHA384:TLS13-AES-128-GCM-SHA256:EECDH+CHACHA20:EECDH+AESGCM:EECDH+AES;
ssl_protocols               TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;

主要变动:

  • listen 增加 http2
  • ssl_protocols 增加 TLSv1.3
  • ssl_ciphers旧列表之后追加 TLS 1.3 与若干 ECDHE 套件

环境背景

项目说明
角色面向公网的公共 Nginx(HTTPS 终结)
目标启用 HTTP/2,并加入 TLSv1.3
OpenSSL1.1.1 系列(示例中用过 1.1.1g)
服务端偏好ssl_prefer_server_ciphers on(问题复现时开启)
客户端差异部分浏览器支持 TLS 1.3 + HTTP/2;部分仅 HTTP/2 + TLS 1.2

环境、证书与业务路径因环境而异;下文以抓包与 RFC 结论为主,参数请按现网调整。

排查过程

复现与浏览器差异

在测试机上修改 hosts 指向问题 Nginx,使用同一 ssl_ciphers 配置:

  • 部分浏览器:稳定复现 ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY
  • 另一些浏览器:页面可打开

相关截图(历史记录):

抓包:TLS 握手成功,HTTP/2 立刻 GOAWAY

对失败客户端做 Wireshark 分析,可看到组合为 HTTP/2 + TLSv1.2(该客户端未协商到 TLS 1.3)。握手本身能完成,Server Hello 选定的套件为:

TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013)

随后 HTTP/2 连接建立阶段即结束,GOAWAY 中错误为:

Error: INADEQUATE_SECURITY (12)

这与浏览器侧的 ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY 对应。

抓包相关截图:

需要注意的是:此时往往尚未形成可记入 access_log 的 HTTP 请求;error_log 也只有在 debug 级别时才容易看到细节。因此“日志看起来正常”并不能排除 TLS/HTTP2 协商层故障。

原因分析

RFC 7540 §6.8 / §7INADEQUATE_SECURITY (0xc) 定义为:底层传输不满足最低安全要求,并指向 §9.2。HTTP/2 要求 TLS 至少为 1.2,并且禁用大量在 TLS 1.2 中仍可能协商到的弱/不合规套件,其中包括 CBC + SHA1 一类,例如上面的 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA。禁用清单见 RFC 7540 Appendix A

ssl_prefer_server_ciphers on 时,服务端按 自身 ssl_ciphers 列表顺序 在客户端提供的交集中选套件。问题配置把历史遗留、安全性偏弱的套件(如 RC4+SHA:AES128+SHA:...)放在列表最前,对“支持 HTTP/2 但不支持 TLS 1.3”的客户端,服务端会优先选出已被 HTTP/2 禁用的套件,于是客户端在 HTTP/2 层直接 GOAWAY。

几种客户端能力组合的粗略结论:

客户端能力典型结果
HTTP/2 + TLS 1.3通常协商到 TLS 1.3 套件,满足 HTTP/2 安全要求
仅 TLS 1.3、无 HTTP/2无 HTTP/2 对套件的额外限制(实际较少见)
HTTP/2 + 仅 TLS 1.2,且服务端优先弱套件易触发 INADEQUATE_SECURITY

关闭 ssl_prefer_server_ciphers 可让部分失败客户端改选更强套件并恢复访问,但等于放弃服务端密码策略,更容易被降级攻击利用,不建议作为正式方案

解决方法

排序原则

在继续使用 ssl_prefer_server_ciphers on 的前提下,建议按下面优先级组织 ssl_ciphers

HTTP/2 兼容的现代套件  >  TLS 1.3 专属套件(可任意位置,辨识度高)  >  仅兼容旧客户端、HTTP/2 禁用或偏弱的套件

原则要点:

  • TLS 1.3 套件只能用于 TLS 1.3,列表中可放任意位置;OpenSSL 1.1.1+ 往往通过 EECDH+... 等别名即可覆盖,不必手写 TLS13-... 旧名。
  • 绝不能把 HTTP/2 禁用的套件排在现代 AEAD 套件前面
  • 列表靠前的套件应同时满足:主流浏览器可协商、且符合 HTTP/2 §9.2。

握手匹配过程示意(历史记录):

参考配置来源

可对照:

推荐配置示例

结合上述原则与当时 OpenSSL 1.1.1g 实测,可用如下基线(请按现网证书算法、是否仍保留 TLS 1.0/1.1 再裁剪):

ssl_protocols               TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ecdh_curve              X25519:P-256:P-384:P-521;
ssl_ciphers                 EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:ALL:!ADH:!EXP:!LOW:!MD5:!SSLV2:!aNULL:!NULL;
ssl_prefer_server_ciphers   on;

说明:

  • TLS 1.3 的三个常用套件一般已包含在前面的别名展开中,无需单独写 TLS13-...
  • 多数现代浏览器会落在 EECDH+CHACHA20 / EECDH+AES128 展开出的 AEAD 套件上;更老的协议才落到后面。
  • ssl_ecdh_curve 默认多为 auto;显式列出曲线是为兼容更多 ECDH 路径,可参考 Cloudflare / Mozilla。
  • 若策略上已禁用 TLS 1.0/1.1,应同步收紧 ssl_protocols,并考虑进一步去掉弱套件别名。

更偏“现代优先”的另一种写法(GCM 显式置顶):

ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:EECDH+CHACHA20:ECDHE+AES128:RSA+AES128:ECDHE+AES256:RSA+AES256:ECDHE+3DES:RSA+3DES;

RSA 证书场景下也可把 ECDHE-RSA-AES128-GCM-SHA256 放在最前。是否保留 3DES、是否 !AES256-SHA 等排除项,按兼容性审计结果决定。

用 OpenSSL 校验展开顺序

在部署前用与 Nginx 链接的同一 OpenSSL 展开列表,确认前几项已是 HTTP/2 可接受的 AEAD 套件

openssl ciphers -V 'EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:ALL:!ADH:!EXP:!LOW:!MD5:!SSLV2:!aNULL:!NULL' | column -t

期望在列表前部看到类似(示例输出,以本机 OpenSSL 为准):

0x13,0x02  -  TLS_AES_256_GCM_SHA384         TLSv1.3  ...
0x13,0x03  -  TLS_CHACHA20_POLY1305_SHA256   TLSv1.3  ...
0x13,0x01  -  TLS_AES_128_GCM_SHA256         TLSv1.3  ...
0xCC,0xA8  -  ECDHE-RSA-CHACHA20-POLY1305    TLSv1.2  ...
0xC0,0x2F  -  ECDHE-RSA-AES128-GCM-SHA256    TLSv1.2  ...

若前部仍是 ...-SHA(非 GCM/CHACHA AEAD)或历史弱套件,需要继续调整字符串顺序或排除项。

验证结果

  1. 问题浏览器复测:此前失败的客户端应能正常打开页面,不再出现 ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY
  2. TLS 1.3 客户端:日志或抓包中可见 TLSv1.3 与对应 AEAD 套件(例如 TLS_AES_256_GCM_SHA384)。
  3. 外部扫描:可用 myssl 等工具检查协议与套件评分;历史检测截图:

  1. access_log 字段(若已配置 $ssl_protocol / $ssl_cipher):确认主流流量落在预期协议与套件上。示意(IP 已脱敏):
<client-ip> | ... | 200 | <domain> | GET / HTTP/2.0 | ... | TLSv1.3 | TLS_AES_256_GCM_SHA384

注意事项

  • 不要为了兼容个别旧浏览器关闭 ssl_prefer_server_ciphers 而不收紧套件列表;正确做法是把现代、HTTP/2 合规套件排在前面。
  • 在 access_log 看不到请求时,优先怀疑 TLS/HTTP2 协商失败,而不是业务 4xx/5xx。
  • OpenSSL / Nginx 版本不同,同一 ssl_ciphers 字符串展开结果可能不同;以实际链接的 OpenSSL 的 ciphers -V 为准
  • 现网若仍大量 TLSv1 / RC4 等历史流量,需要单独做兼容性与安全策略评审,而不是简单复用 2020 年的“全兼容”尾部列表。
  • 第三方 TLS 能力检测页可能与真实浏览器行为不一致(例如界面显示不支持 TLS 1.3,但实际协商已是 1.3);以抓包与服务端日志为准。

参考资料