HTTP/3 部署安全在 2026 年已经从「尝鲜选项」变成「默认要评估的项目」:主流浏览器默认走 QUIC,CDN 厂商把 h3 当卖点,而多数四层高防、WAF、限速策略还停留在只认 TCP 的时代。结果是网站开了 HTTP/3 之后,攻击流量跟着 UDP 443 绕过原有防护链路,监控里却看不出异常。本文给出 nginx 侧可落地的 HTTP/3 配置、0-RTT 的处理方式、UDP 层限速方案,以及如何确认自己的 WAF 到底能不能看见 QUIC 流量。
适用场景
- 已在 nginx、Caddy 或云 CDN 上开启 HTTP/3,但不确定安全策略是否同步覆盖。
- 站点接入四层高防或云清洗,需要评估 UDP 443 是否在防护范围内。
- 移动端用户占比高、弱网体验差,需要用 QUIC 降低握手开销,同时不想牺牲可观测性。
- 安全团队要求给出「HTTP/3 上线后的攻击面变化」说明与验证记录。
前置条件
- nginx 1.25.0 及以上(内置 http_v3_module),编译时带 –with-http_v3_module。
- OpenSSL 3.2 及以上或 quictls/BoringSSL,且需要支持 TLS 1.3 early data 才有 0-RTT 能力。
- 防火墙放行 443/UDP;云主机安全组同步放行,否则 QUIC 握手全部失败、浏览器静默回退。
- 验证工具:curl 8.0 及以上(带 HTTP/3 支持)、nmap、openssl 3.2。
原理说明
QUIC 把传输层握手和 TLS 1.3 合并在 UDP 上完成,一个 RTT 内既能建连又能拿到密钥, reconnect 时还能靠 Connection ID 做连接迁移。这些特性带来三个必须单独处理的安全问题:
- 0-RTT 重放:early data 不具备前向安全性,攻击者可以截获并重放。对 POST、支付、下单、改密这类非幂等请求,0-RTT 是实打实的风险。
- 握手前放大:UDP 无连接,攻击者可伪造源 IP。协议规定服务器在地址未验证前回包不得超过请求的 3 倍,实践中仍需要用 RETRY 令牌强制验证,否则小请求打大响应的放大攻击依然成立。
- 防护链路失明:四层高防、基于 TCP 会话表的限速、部分 WAF 的流量镜像都只处理 TCP。UDP 443 一旦在边缘被丢弃或直连回源,HTTP/3 请求就绕过了整套七层检测,日志里只剩源站自己的记录。
理解这三点之后,配置动作就很明确:默认关掉 0-RTT,打开 RETRY,UDP 层单独限速,并且确认 QUIC 流量要么过防护、要么在边缘被拒。
操作步骤
步骤一:确认 nginx 与 OpenSSL 具备 QUIC 能力
先看编译参数,没有 http_v3_module 就只能换包或自行编译:
nginx -V 2>&1 | tr ' ' '\n' | grep -E 'http_v3|with-openssl|version'
openssl version -a | head -3
nginx -v
# 期望看到:--with-http_v3_module,以及 OpenSSL 3.2 或更高版本
Debian/Ubuntu 官方源里的 nginx 从 1.25 起已带 http_v3_module;老系统建议直接用官方 nginx.org 的 stable 源,避免自编译带来的升级负担。
步骤二:开启 HTTP/3 并默认关闭 0-RTT
QUIC 需要独立的 host key 来签发地址验证令牌,多 worker、多实例之间必须共用同一个文件,否则用户会被反复要求验证:
# 生成 host key(只生成一次,纳入备份与权限管控)
openssl rand 32 > /etc/nginx/quic_host.key
chmod 600 /etc/nginx/quic_host.key
# /etc/nginx/conf.d/quic.conf
limit_req_zone $binary_remote_addr zone=perip:20m rate=30r/s;
log_format quic '$remote_addr [$time_local] "$request" $status '
'$server_protocol h3=$http3 $body_bytes_sent $request_time';
server {
listen 443 ssl;
listen 443 quic reuseport;
http2 on;
http3 on;
server_name www.example.com;
ssl_certificate /etc/nginx/ssl/example.fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.key;
ssl_protocols TLSv1.3; # QUIC 只跑 TLS 1.3
ssl_early_data off; # 0-RTT 默认关闭,见步骤四
quic_retry on; # 强制地址验证,抗伪造源放大
quic_gso on;
add_header Alt-Svc 'h3=":443"; ma=3600' always;
limit_req zone=perip burst=20 nodelay;
access_log /var/log/nginx/quic.access.log quic;
}
关键参数说明:ssl_early_data off 保证所有请求都完成完整握手,牺牲一次 RTT 换取重放免疫;quic_retry on 让服务器先回 RETRY 令牌验证客户端地址,未验证的连接拿不到大响应;Alt-Svc 是浏览器发现 h3 的唯一入口,下线 HTTP/3 时删掉这一行即可让客户端在 ma 秒后回退 TCP。
步骤三:UDP 443 单独限速,别指望 limit_conn
HTTP/3 没有 TCP 连接概念,limit_conn 对它基本失效,UDP 洪水必须在防火墙层拦。下面的 nftables 规则给单 IP 的 QUIC 包速率设上限,并对超限做计数丢弃:
# /etc/nftables.d/quic-guard.nft
table inet quic_guard {
set quic_allow { type ipv4_addr; flags timeout; }
chain input {
type filter hook input priority -10; policy accept;
ct state established,related accept
# 白名单:监控探针与内部调用
ip saddr @quic_allow udp dport 443 accept
# 单 IP 每秒最多 800 个 QUIC 包,突发 400
ip protocol udp udp dport 443 meter quic_perip { ip saddr limit rate over 800/second burst 400 packets } counter log prefix "quic-flood: " drop
ip protocol udp udp dport 443 accept
}
}
nft -f /etc/nftables.d/quic-guard.nft
nft list table inet quic_guard
阈值要按真实业务量调:一个正常浏览器页面的 QUIC 包量在几十到几百之间,800 pps 属于保守起点。规则里带 log prefix 是为了让 SIEM 能把 QUIC 洪水与普通 CC 攻击区分开统计。
步骤四:按业务类型决定 0-RTT 的开关
0-RTT 不是全站开关,正确做法是只给幂等请求用。下面用 map 按 URI 前缀判断,仅静态资源允许 early data:
# /etc/nginx/conf.d/early-data.conf
map $request_uri $allow_early {
default 0;
~^/static/ 1;
~^/assets/ 1;
~^/img/ 1;
}
server {
# 放在 http3 的 server 块内
ssl_early_data off;
location / {
if ($allow_early = 0) { set $early off; }
# 幂等 GET 且命中静态前缀时才允许 0-RTT
}
location /api/ {
ssl_early_data off; # 接口一律关闭,防重放
}
}
如果业务确实需要接口层 0-RTT,必须在应用侧补两道防线:为每个 early data 请求带一次性 nonce 并做服务端去重,同时对写入类操作加幂等键。只靠传输层开关不算方案,因为重放发生在服务端逻辑里。
步骤五:确认 WAF 与高防是否覆盖 QUIC
这是最容易出事的环节。验证方式很直接——看 QUIC 请求在防护侧有没有日志:
# 1) 从外部用 HTTP/3 打一次请求
curl -sI --http3-only https://www.example.com/ | head -5
# 2) 立刻在 WAF / 高防控制台查该 URL 与源 IP 的记录
# 3) 同时看源站 access log 是否出现同一条请求
tail -n 20 /var/log/nginx/quic.access.log
# 4) 若防护侧无记录而源站有记录,说明 UDP 443 绕过了防护
# 临时处置:边缘直接拒绝 UDP 443,只保留 TCP 回退
nft add rule inet filter input udp dport 443 counter drop
处置优先级是:能升级到支持 QUIC 清洗的防护产品就升级;做不到就在边缘丢弃 UDP 443,让浏览器自动回退到 TCP + TLS,宁可损失一点弱网体验,也不要把七层防护绕开。对以 CC 与爬虫为主要威胁的业务,这个取舍几乎总是划算的。
步骤六:补齐日志字段与慢请求观测
HTTP/3 的排障难点在于「连接没了」,传统按 TCP 连接维度的统计会失真。日志里至少要留下协议版本与请求耗时:
log_format quic_full '$remote_addr - $remote_user [$time_local] '
'"$request" $status $server_protocol h3=$http3 '
'bytes=$body_bytes_sent rt=$request_time '
'ua="$http_user_agent"';
# 统计 QUIC 与 TCP 各自占比
awk '{print $9}' /var/log/nginx/quic.access.log | sort | uniq -c | sort -rn
如果发现 h3 请求的 rt 明显高于 TCP,多半是 UDP 被中间设备限速或 GSO 未生效,先查 quic_gso 与网卡 offload 设置,而不是急着怀疑应用。
配置验证
# 1) 协议协商成功,响应头带 Alt-Svc
curl -sI --http3-only https://www.example.com/ | grep -i alt-svc
# 2) 未开启 HTTP/3 的客户端仍可访问(TCP 回退正常)
curl -sI https://www.example.com/ | head -1
# 3) UDP 443 处于监听状态
ss -ulnp | grep ':443'
# 4) 0-RTT 已关闭:握手中不应出现 early_data 扩展被接受
openssl s_client -quic -connect www.example.com:443 -servername www.example.com &1 \
| grep -iE 'early|protocol|TLSv1.3'
四条全部符合预期,才算 HTTP/3 上线完成:h3 可用、TCP 可回退、UDP 有限速、0-RTT 未放开。
常见问题
Q1:开了 HTTP/3 之后 WAF 拦截率明显下降,是规则失效了吗?
大概率不是规则问题,而是流量根本没经过 WAF。QUIC 走 UDP 443,基于 TCP 会话的四层设备与部分七层代理不解析这种流量,源站却仍然能收到请求。按步骤五验证一次:外部发 h3 请求,看 WAF 有无记录。若确认绕过,处置只有两条——换支持 QUIC 清洗的防护链路,或者在边缘封掉 UDP 443 让客户端回退 TCP,别指望在源站 nginx 上补规则来替代 WAF。
Q2:0-RTT 到底要不要开?开了会怎样?
默认不要开。0-RTT 的 early data 可以无限重放且不具备前向安全性,攻击者截获一个包就能反复提交。确实要开的话,只对静态资源放开,接口一律 ssl_early_data off,并在应用层加一次性 nonce 与幂等键。衡量标准是「重放这个请求会不会造成状态变更」,会变更就别开。
Q3:开启 quic_retry 会不会拖慢首次访问?
会让首次建连多一个 RTT,因为服务器要先回 RETRY 令牌验证地址。对已经缓存了令牌的老用户没有影响。如果站点正被 UDP 反射或伪造源攻击,这点代价完全值得;如果是纯内网或低风险业务,可以把 quic_retry 设为 off 换取首包速度,但要同时保证 UDP 层限速生效。
总结
HTTP/3 部署安全的关键不在能不能开,而在开完之后三件事是否到位:0-RTT 按业务类型收紧、UDP 443 有独立的限速与丢弃策略、防护链路确认能看见 QUIC 流量。把这三条落到配置与验证记录里,QUIC 才是性能优化,而不是给自己挖一个监控看不见的洞。