如何在香港服务器的Debian系统中部署WAF防火墙,抵御跨境支付系统的SQL注入攻击?

那天凌晨 1:40,部署在香港荃湾机房的支付风控的告警:国内用户通过我们香港集群下单时,/api/pay/charge 的 5xx 报表开始抖;同时,日志里冒出一串典型的 1' OR '1'='1。熟悉的味道——SQL 注入在撞门。
我没有选择把流量切回 Cloud 侧的“保守”模式(会影响转化),而是决定直接在业务入口的 Debian 12(bookworm) Nginx 反向代理层落地 ModSecurity + OWASP CRS 的 WAF,配合几条和支付域模型贴身的规则,先拦住再说。
下面是我那晚的完整部署与优化流程,细到新手能照抄,老手也不浪费时间。
一、现场环境与目标
1)硬件与网络(香港机柜)
| 项 | 参数 |
|---|---|
| 机型 | 1U 单路,Intel® Xeon Silver(24 核),超线程开启 |
| 内存 | 64 GB DDR4-3200(vm.swappiness=1) |
| 磁盘 | 2 × 1.92 TB NVMe(RAID1,XFS,noatime) |
| 网卡 | 2 × 10 GbE,Bond0 (802.3ad),上联双路 |
| 系统 | Debian 12(bookworm),内核 6.x,systemd |
| 角色 | L7 反代 + WAF(Nginx)→ 上游应用(支付网关集群) |
2)流量与性能基线(上线前)
| 指标 | 数值(p95) |
|---|---|
| 峰值 QPS | 6.8k(主要为 /api/pay/* JSON POST) |
| 入站带宽 | 1.6 Gbps |
| 业务延迟 | 48 ms(HK→广州/深圳),95ms(SEA) |
| 错误率 | 0.12%(偶发上游超时) |
目标:在不开启全站挑战/JS 检测的前提下,拦截 99% 以上 SQLi,业务 p95 延迟增加 ≤ 6ms,峰值 QPS 损耗 ≤ 15%。
二、安装方案选择与原理确认(1 分钟决策)
Debian 12 官方仓库直接提供 libmodsecurity3(WAF 引擎库)与 Nginx 连接器模块 libnginx-mod-http-modsecurity,无需源码编译,可靠省时。
规则集采用 OWASP CRS 3.x(核心规则集,覆盖 SQLi/XSS/LFI 等 Top 10 类形)。
如果你的仓库缺少连接器,才考虑源码编译(当晚我没用这个兜底,但这里给出参考)。
三、快速部署:在 Debian 12 上启用 Nginx + ModSecurity + CRS
以下命令都在 root 或 sudo 下执行。域名以 pay.example.com 为例。
1)安装组件
apt update
apt install -y nginx libmodsecurity3 libmodsecurity-dev \
libnginx-mod-http-modsecurity modsecurity-crs
验证模块已装:
nginx -V 2>&1 | grep -i modsecurity || echo "动态模块将通过 load_module 加载"
dpkg -l | egrep "libmodsecurity3|libnginx-mod-http-modsecurity|modsecurity-crs"
上面两个包分别对应 WAF 引擎库 与 Nginx 连接器,CRS 为规则集。
2)启用 Nginx 动态模块
文件:/etc/nginx/modules-enabled/90-modsecurity.conf(若不存在则创建)
load_module modules/ngx_http_modsecurity_module.so;
3)初始化 ModSecurity
# 生成主配置
mkdir -p /etc/nginx/modsec
cp /etc/modsecurity/modsecurity.conf-recommended /etc/nginx/modsec/modsecurity.conf
编辑 /etc/nginx/modsec/modsecurity.conf 的关键项(精简示例):
SecRuleEngine On
SecAuditEngine RelevantOnly
SecAuditLog /var/log/modsec_audit.log
SecAuditLogFormat JSON
SecRequestBodyAccess On
SecRequestBodyLimit 13107200 # 12.5MB:足够支付票据/收据
SecRequestBodyNoFilesLimit 5242880
SecPcreMatchLimit 100000 # PCRE2 + JIT 环境下适中
SecPcreMatchLimitRecursion 100000
Debian 的 libmodsecurity3 已采用 PCRE2;可用 pcre2test -Cjit 检查 JIT。
4)接入 OWASP CRS
# Debian 的 CRS 安装路径
ls -l /usr/share/modsecurity-crs
mkdir -p /etc/nginx/modsec/crs
cp -r /usr/share/modsecurity-crs/* /etc/nginx/modsec/crs/
# 启用 crs-setup.conf:如有样例文件则复制
[ -f /etc/nginx/modsec/crs/crs-setup.conf.example ] && \
cp /etc/nginx/modsec/crs/crs-setup.conf.example /etc/nginx/modsec/crs/crs-setup.conf
创建 主规则汇入 文件 /etc/nginx/modsec/main.conf:
# 先加载引擎主配置
Include /etc/nginx/modsec/modsecurity.conf
# 告诉引擎:JSON 请求体使用 JSON 处理器(支付接口大多 application/json)
SecRule REQUEST_HEADERS:Content-Type "application/json" \
"id:100001,phase:1,pass,t:none,ctl:requestBodyProcessor=JSON"
# 加载 CRS
Include /etc/nginx/modsec/crs/crs-setup.conf
Include /etc/nginx/modsec/crs/rules/*.conf
CRS 是官方社区主力规则集,默认就覆盖 SQLi。
5)在 Nginx 站点中开启 WAF
编辑你的 server 配置(如 /etc/nginx/sites-available/pay.conf):
server {
listen 80;
listen 443 ssl http2;
server_name pay.example.com;
# ... SSL 略(生产务必启用 TLS1.2/1.3,OCSP,HSTS)
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/main.conf;
location /api/pay/ {
proxy_pass http://pay_upstream;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_read_timeout 30s;
}
}
upstream pay_upstream {
least_conn;
server 10.20.0.11:8080 max_fails=2 fail_timeout=10s;
server 10.20.0.12:8080 max_fails=2 fail_timeout=10s;
keepalive 256;
}
检测并重载:
nginx -t && systemctl reload nginx
四、支付域模型的“贴身”强化规则(拦 SQLi,少误杀)
通用规则挡大头,业务规则压住长尾。以下是我在 main.conf 里追加的“轻量但致命”的几条。
注意:下面示例使用 ModSecurity 的 @detectSQLi(libinjection)与参数白名单思路,仅对我们支付域模型,你可以按需改键名/正则。
# 1)对关键信息强约束:订单号/金额应为限定格式
SecRule ARGS_GET:order_id "!^\d{6,18}$" \
"id:110010,phase:2,deny,status:403,log,msg:'order_id must be 6-18 digits'"
SecRule ARGS|ARGS_NAMES|REQUEST_BODY "@rx \"\\b(amount|price)\\b\"" \
"id:110011,phase:2,pass,log,ctl:ruleRemoveById=942100"
SecRule REQUEST_BODY_JSON:amount "!^\d{1,6}(?:\.\d{1,2})?$" \
"id:110012,phase:2,deny,status:403,log,msg:'invalid amount format'"
# 2)对高风险字段直接做 SQLi 检测(GET/POST/JSON 都扫)
SecRule ARGS|ARGS_NAMES|REQUEST_COOKIES|REQUEST_HEADERS "@detectSQLi" \
"id:110020,phase:2,deny,status:403,log,msg:'SQLi detected in args/headers'"
SecRule REQUEST_BODY_JSON "@detectSQLi" \
"id:110021,phase:2,deny,status:403,log,msg:'SQLi detected in JSON body'"
# 3)典型情景白名单(减少误报)
# 产品名里可能出现 'select plan' 等字样,放宽 product_name
SecRuleUpdateTargetByTag "OWASP_CRS/WEB_ATTACK/SQL_INJECTION" "!ARGS:product_name"
SecRuleUpdateTargetByTag "OWASP_CRS/WEB_ATTACK/SQL_INJECTION" "!REQUEST_BODY_JSON:product_name"
规则顺序:先强约束(便宜、快),再行为检测(detectSQLi),最后白名单做减法。这样既快又稳。
五、联动:速拦 → 取证 → 复盘
1)日志与审计(不触碰敏感持卡信息)
Nginx access log(摘掉敏感字段,保留关键上下文):
log_format waf_json escape=json
'{ "time":"$time_iso8601", "client":"$remote_addr", "host":"$host", '
'"uri":"$request_uri", "status":$status, "rt":$request_time, '
'"up_rt":"$upstream_response_time", "req_len":$request_length, '
'"body_size":$body_bytes_sent, "ua":"$http_user_agent", '
'"x_req_id":"$request_id" }';
access_log /var/log/nginx/pay_access.log waf_json;
ModSecurity 审计已设置为 JSON,结合 jq/ELK 立刻分流分析:
tail -f /var/log/modsec_audit.log | jq '.msg, .transaction.request.uri'
合规提醒(PCI DSS):不要把 PAN/CVV 原样落日志;上游应用应在进入反代前就进行脱敏,或使用字段级加密后在后端解密。WAF 侧一律不记录敏感载荷。
2)快速验证(安全自测,不冲生产)
正常:
curl -sS -o /dev/null -w '%{http_code}\n' \
-H "Content-Type: application/json" \
-d '{"order_id":"20250913000123","amount":"199.99"}' \
https://pay.example.com/api/pay/charge
恶意(演示用):
curl -i -H "Content-Type: application/json" \
-d '{"order_id":"1 OR 1=1","amount":"199.99"}' \
https://pay.example.com/api/pay/charge
# 预期:403 & ModSecurity 记录
六、性能与延迟:怎么把代价压到业务能接受
1)Nginx 基础调优
/etc/nginx/nginx.conf 关键项:
worker_processes auto;
worker_rlimit_nofile 262144;
events { worker_connections 65535; use epoll; }
http {
sendfile on; tcp_nopush on; tcp_nodelay on;
keepalive_timeout 65s;
client_body_buffer_size 64k;
client_max_body_size 10m; # 结合 SecRequestBodyLimit
# gzip/br 视流量类型决定
}
2)ModSecurity 的性价比配置
PCRE2 JIT:确认启用(上文),可显著降低复杂正则开销。
- 请求体限制:按支付载荷设定 SecRequestBody{,NoFiles}Limit,避免过度扫描大文件。
- 有的放矢:只在 /api/pay/ 开启 modsecurity on;,静态与图片域名不启。
- 白名单减法:对明确“常误杀”的键做 SecRuleUpdateTargetByTag 排除,减少无用匹配。
3)上线后的真实数据(开启 WAF 当晚)
| 指标 | 上线前 | 上线后(15 分钟窗口) |
|---|---|---|
| 峰值 QPS | 6.8k | 6.1k(-10.3%) |
| p95 网关延迟 | 48 ms | 53 ms(+5 ms) |
| 403 拦截率(SQLi 类) | — | 99.2%(审计样本) |
| 误报率(人工抽样) | — | 0.18%(白名单后 <0.05%) |
这组数据达成了当晚的目标:延迟 +5ms,吞吐损耗 ~10%,换来稳定的 SQLi 拦截。
七、那晚踩过的坑与我当场怎么填
JSON 没被解析:最初没显式声明 JSON 处理器,导致规则命中率低。
修复:在 main.conf 里加了 ctl:requestBodyProcessor=JSON(上文示例)。
产品名误杀:"select plan" 被当成 SQLi。
修复:对 product_name 用 SecRuleUpdateTargetByTag 排除 SQLi 目标(上文示例)。
上游 502 抖动:CRS 默认扫描 + 限流叠加,个别慢接口被误判掉链。
修复:支付接口分层:对 /api/pay/charge 开较严,对 /api/pay/query 放宽;并把 proxy_read_timeout 调到 30s。
审计日志爆量:RelevantOnly 仍多。
修复:接入 ELK 的索引生命周期策略(ILM),并分 index:waf-audit-YYYY.MM.DD。
动态模块未加载:忘了 load_module。
修复:统一以 modules-enabled/90-modsecurity.conf 管理,增 nginx -t precheck 的 GitHook。
八、进一步加固(第二天白天做的)
- 地理/ASN 细粒度:结合 geoip2 或自建 IP 信誉表,对“新 ASN + 高失败率”的来源提高敏感度(仅调 WAF 灵敏度,不做地域歧视)。
- 速率限制:在 /api/pay/ 增加 limit_req(按 X-Request-Id/账户维度更合理)。
- 蓝绿规则:把自定义规则拆为 main-business.conf,灰度发布(按 map $cookie_sticky 选择是否加载)。
- 上游参数强类型:后端网关仍要做参数化查询(WAF 不是银弹)。
- 备选方案兜底:若某时段攻击过猛,可以短时切换到云侧/前置高防的托管 WAF策略,再回切(本次没有动用)。
九、完整目录结构(上线时的落地)
/etc/nginx/
├── nginx.conf
├── modules-enabled/
│ └── 90-modsecurity.conf # load_module ...
├── sites-available/
│ └── pay.conf # server + upstream
├── sites-enabled/
│ └── pay.conf -> ../sites-available/pay.conf
└── modsec/
├── modsecurity.conf
├── main.conf # Include 引擎 + CRS + JSON ctl + 业务规则
├── main-business.conf # (次日拆分出的业务规则)
└── crs/
├── crs-setup.conf
└── rules/*.conf
十、应急与回滚剧本(我机房里贴在架子上的那张纸)
观察窗口:规则改动后 5 分钟内,Grafana 四套看板(QPS/RT/4xx/上游 5xx)不得离眼。
流量分流:Nginx 层标签灰度(或 LVS 层),异常即回滚到“仅日志、不开惩罚”的 SecRuleEngine DetectionOnly。
审计抽样:每 1000 条 403 抽 30 条人工复核,误报 >0.5% 即回减规则。
工单告知:风控、客服同步模板:出现人机验证/被拦提示如何申诉。
十一、我为什么选这条路(以及你可以怎么变体)
- 用官方仓库的包:libmodsecurity3 + libnginx-mod-http-modsecurity 稳、快、可追溯;出问题能“apt pin + 快速回退”。
- CRS + 业务小规则:CRS 吃下 80%,剩余 20% 用你自己的业务知识补齐,误报率好控。
- 变体:如果你环境特殊(比如 Nginx 版本自行编译、或仓库缺模块),再用 源码路径装连接器。
系统稳定下来时,机房外开始泛白。那晚我们拦住了异常的注入流量,交易成功率没怎么掉,支付页面也不再卡顿。我抓起那杯被空调风吹成常温的冰美式,补上变更记录,给白班同事留了两页纸的“坑位图”。
这就是我在香港服务器的 Debian 系统里,把 WAF 从零到一搭起来、并在跨境支付场景下压住 SQL 注入的全过程。
如果你也在做类似的事,建议从“先跑起来,再贴身调优”开始:CRS 护住大面,业务规则补齐长尾,日志和灰度把握节奏。等到哪天深夜警报又响起,你会发现,那杯咖啡还能喝得上。
附:快速命令清单(方便你复制)
# 安装
apt install -y nginx libmodsecurity3 libmodsecurity-dev \
libnginx-mod-http-modsecurity modsecurity-crs
# 启用模块
echo 'load_module modules/ngx_http_modsecurity_module.so;' \
> /etc/nginx/modules-enabled/90-modsecurity.conf
# 初始化配置
mkdir -p /etc/nginx/modsec
cp /etc/modsecurity/modsecurity.conf-recommended /etc/nginx/modsec/modsecurity.conf
cp -r /usr/share/modsecurity-crs /etc/nginx/modsec/crs
[ -f /etc/nginx/modsec/crs/crs-setup.conf.example ] && \
cp /etc/nginx/modsec/crs/crs-setup.conf.example /etc/nginx/modsec/crs/crs-setup.conf
# 主规则文件(按上文 main.conf 内容)
$EDITOR /etc/nginx/modsec/main.conf
# Nginx 站点开启 WAF(按上文 server/upstream)
$EDITOR /etc/nginx/sites-available/pay.conf
ln -sf /etc/nginx/sites-available/pay.conf /etc/nginx/sites-enabled/pay.conf
# 生效
nginx -t && systemctl reload nginx
参考:Debian 的 ModSecurity 库与 Nginx 连接器包说明、OWASP CRS 项目页(确认名称与作用)。