本文主要记录在 Nginx 上同时开启 HTTP/2 与 TLSv1.3 后,部分浏览器出现 ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY 的排查过程,以及如何调整 ssl_ciphers 优先级来兼顾兼容性与安全性。
相关背景可先阅读站内 nginx 启用 HTTP/2 和 TLSv1.3。需要注意的是:HTTP/2 对底层 TLS 密码套件有额外限制,只打开 listen ... http2 和 ssl_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增加http2ssl_protocols增加TLSv1.3ssl_ciphers在旧列表之后追加 TLS 1.3 与若干 ECDHE 套件
环境背景
| 项目 | 说明 |
|---|---|
| 角色 | 面向公网的公共 Nginx(HTTPS 终结) |
| 目标 | 启用 HTTP/2,并加入 TLSv1.3 |
| OpenSSL | 1.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 / §7 将 INADEQUATE_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。
握手匹配过程示意(历史记录):

参考配置来源
可对照:
- myssl HTTPS 安全兼容实践
- Mozilla Server Side TLS(Intermediate 档)
- Cloudflare sslconfig
推荐配置示例
结合上述原则与当时 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)或历史弱套件,需要继续调整字符串顺序或排除项。
验证结果
- 问题浏览器复测:此前失败的客户端应能正常打开页面,不再出现
ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY。 - TLS 1.3 客户端:日志或抓包中可见
TLSv1.3与对应 AEAD 套件(例如TLS_AES_256_GCM_SHA384)。 - 外部扫描:可用 myssl 等工具检查协议与套件评分;历史检测截图:



- 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);以抓包与服务端日志为准。