Haproxy + Nginx 前端架构 如何防止 slowhttp 攻击, https 与 http 的差异问题解决

1)背景:前端入口链路

我们的站点前端是典型的两层入口:

  • HAProxy:公网入口(80/443),负责分流与转发
  • Nginx:承载前端静态资源与后端反代

部署架构图

部署架构


2)问题现象:同样是 SlowHTTP,HTTPS “十几秒断光”,HTTP 却能一直挂着

我们用 slowhttptest 测试 Slow Headers / Slowloris(慢慢发送请求头,让服务器长时间保持连接,最终耗尽连接资源)时:(HAProxy Technologies)

  • HTTPS(443):连接在 10 多秒左右大量断开,攻击很难长期占用资源
  • HTTP(80):连接能长期保持;攻击持续后才出现 429,但 429 会影响所有正常用户访问

原因 : 一开始我们把防护都写在 Nginx(例如 header 超时等),结果出现非常反直觉的情况:HTTPS 有效,HTTP 无效


3)根因分析:HTTP/HTTPS 的“第一层解析 HTTP 的位置”不同

这次的关键点不是 Nginx 配得对不对,而是:

Slowloris 卡住的是“第一层负责接收/解析 HTTP 请求头”的组件。

✅ HTTPS(443)为什么能防住

我的 443 链路是:

  • HAProxy:mode tcp + SNI 分流(不解析 HTTP
  • 后端:send-proxy-v2 透传到 Nginx
  • Nginx:负责 TLS 终止与 HTTP 解析(第一层真正“读请求头”的地方)

因此 Slowloris 的“慢请求头”最终会到 Nginx,Nginx 的相关超时策略会把它掐掉,所以你看到 HTTPS 连接会在十几秒内“断光”。

另外,Nginx 要接收 PROXY 协议,必须在 listen 上启用 proxy_protocol。(loadbalancer.org)

❌ HTTP(80)为什么最初防不住

我的 80 链路是:

  • HAProxy:mode http会解析 HTTP

这意味着 HAProxy 才是第一层接收并解析 HTTP 请求头的地方。而 HAProxy 在收到完整请求之前不会把请求转发到后端(官方说明:只有收到完整请求才会发给后端,因此后端往往看不到 slowloris 流量)。(HAProxy Technologies)

所以你在 Nginx 上做的“读请求头超时”对 HTTP 链路会出现一个现实问题:

慢头连接根本没到 Nginx,就卡在 HAProxy 这一层了。

这就是“HTTPS 能防、HTTP 防不住”的根本原因:两条链路的防护点不在同一层


4)最终解决方案:把 SlowHTTP 防护前移到 HAProxy 的 80(HTTP)入口

要在 HAProxy mode http 下解决 slowloris,核心就一条:

  • timeout http-request:限制“收完整请求头”的最大时间(slowloris 依赖慢慢发头来续命,这条能直接掐断)(HAProxy Technologies)

在此基础上我们叠加 stick-table,用“按源 IP 的并发连接数 / 请求速率”做更精细的保护与兜底。(HAProxy Technologies)


5)最终版配置

下面给出最终版(精简但完整可用)的 HAProxy + Nginx 关键配置结构,你可以把业务路由部分按自己的域名/SNI/后端继续扩展。


A. HAProxy(最终版)

版本:haproxy:2.3(Docker)
说明:443 维持 TCP/SNI 分流,80 负责 HTTP 分流 + SlowHTTP 防护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
global
log 127.0.0.1 local0
daemon
maxconn 2000

defaults
log global
option redispatch
retries 3
timeout connect 5s
timeout client 50s
timeout server 50s

# 管理页面(可选)
frontend admin_stats
bind :8070
mode http
stats enable
stats uri /admin
stats refresh 30s
stats auth admin:xxxxxxxx
stats hide-version

#####################################################################
# HTTP :80 入口(关键:在这里防 slowloris / slowhttp)
#####################################################################
frontend header_front
bind *:80
mode http
option httplog
option forwardfor

# --- 核心 1:专杀 Slow Headers / Slowloris ---
# 限制接收完整请求头时间:慢头无法“续命”
timeout http-request 10s

# 可选:缩短 keep-alive 空等时间,减少连接长期占用
timeout http-keep-alive 10s

# 可选:收紧 client(视业务而定)
timeout client 30s

# --- 核心 2:stick-table(按IP并发/速率兜底)---
stick-table type ip size 200k expire 30s store conn_cur,conn_rate(10s),http_req_rate(10s)

# 连接一建立就跟踪:对“慢头阶段”的连接非常关键
tcp-request connection track-sc0 src
acl abuse_conn sc0_conn_cur gt 80
tcp-request connection reject if abuse_conn

# 请求层限速(按业务调整阈值)
http-request track-sc0 src
acl abuse_rps sc0_http_req_rate gt 100
http-request deny deny_status 429 if abuse_rps

# --- 你的业务分流规则(示例保留你的结构)---
acl finereport_h5 hdr_dom(host) -i finereport.xxxxx.com.cn
http-request redirect scheme https if finereport_h5

acl iphone_browser hdr_reg(User-Agent) -i iPhone
acl ipad_browser hdr_reg(User-Agent) -i iPad
acl android_browser hdr_reg(User-Agent) -i Android
use_backend app_backend if iphone_browser or ipad_browser or android_browser
default_backend pc_backend

backend app_backend
mode http
option httpchk
server mom 192.168.139.118:30001 check
# 注意:不要再手动 set-header X-Forwarded-For,否则会和 option forwardfor 叠加导致 "ip, ip"
http-request set-header X-Real-IP %[src]

backend pc_backend
mode http
server mom-app 192.168.139.118:30002
http-request set-header X-Real-IP %[src]

#####################################################################
# HTTPS :443 入口(维持 TCP + SNI 分流,交给 Nginx 终止 TLS)
#####################################################################
frontend https_frontend
bind *:443
mode tcp

tcp-request inspect-delay 5s
tcp-request content accept if { req.ssl_hello_type 1 }

acl app_host req.ssl_sni -i app.xxxxx.com.cn
acl finereport_host req.ssl_sni -i finereport.xxxx.com.cn

use_backend app_https_backend if app_host
use_backend finereport_https_backend if finereport_host
default_backend pc_https_backend

backend pc_https_backend
mode tcp
server mom-https 192.168.139.118:18443 send-proxy-v2

backend app_https_backend
mode tcp
server app-https 192.168.139.118:18443 send-proxy-v2

backend finereport_https_backend
mode tcp
server fr-https 192.168.139.118:18443 send-proxy-v2

关键点回顾:

  • timeout http-request 是解决 HTTP slowloris 的“必杀开关” (HAProxy Technologies)
  • stick-table 用于按 IP 做并发/速率兜底;http-request deny deny_status 429 是官方/手册常用写法 (HAProxy Technologies)

B. Nginx(最终版:只保留与链路相关的两句)

你不需要在这篇文章里展开 Nginx 的各类参数(因为这次问题本质在 80 的 HAProxy)。但为了说明链路,保留两句就够:

1
2
3
4
5
# HTTP 入口
listen 80;

# HTTPS 入口:接收 HAProxy send-proxy-v2
listen 443 ssl proxy_protocol;

proxy_protocol 的意义:让 Nginx 能从 HAProxy 透传的 PROXY 协议里获取真实源地址。(loadbalancer.org)


6)验证方式:怎么确认 HTTP 真的“像 HTTPS 一样”防住了

  1. 再跑一次 slowhttptest(SLOW HEADERS 模式)
  • 观察 80 端口:连接会在 timeout http-request 10s 附近开始大量断开,不会长期“挂住”
  • 443 端口:保持原有效果(十几秒内大量断开)
  1. HAProxy 日志/统计
  • 你会看到更多连接在请求未完成阶段被拒绝/超时
  • 若开了 stats(:8070),也能看到前端并发回落更快

7)总结:这次踩坑的真正经验

  • 同样是 slowloris,HTTP/HTTPS 的防护点可能完全不同。
  • 80 端口 HAProxy 是 mode http 时,慢头卡在 HAProxy,Nginx 的“读头超时”自然不生效。
  • 解决它的正确方式是:在 HAProxy 80 入口加 timeout http-request + stick-table,把 slowhttp “掐死在入口层”。