将 limit_req、limit_conn 和几条拦截规则拼到 Nginx 配置里并不难。真正的难点在于,你得清楚地知道它们各自能挡住什么、挡不住什么、误伤会发生在哪里、上线后如何验证,以及一旦出了问题该怎么快速回滚。
很多线上事故的根源,并非因为没做任何防护,而是因为把请求洪泛当成连接洪泛去处理,把 WAF 规则当作流量清洗的替代品,或者把白名单信任建立在了错误的真实源地址链路上。最终的结果往往是,正常用户和恶意请求被一并挡在了门外。
Nginx 的视角是有限的。它只能看到自己所在层面的连接、请求、URI、Header、Body 以及和上游交互的结果。超出这个边界的事情——比如大规模带宽型 DDoS、链路拥塞、源站之前的网络设备被打满,单靠调整 Nginx 配置是解决不了的。本文将连接层、请求速率层、规则层拆开讲透,给出默认边界、常见误区、副作用分析、推荐配置、验证步骤、监控告警和回滚方案。目标是让这份文档能直接作为上线前的审查清单和值班处置手册使用。
一、概述
1.1 背景介绍
当 Nginx 暴露在公网时,最常见的三类压力并不等价,你需要分清:
- 连接洪泛:短时间内出现大量并发连接,目的是占满 worker 连接数、socket、backlog 或上游连接池。
- 请求洪泛:连接数不一定很高,但单位时间内的请求数异常。典型场景是撞库、刷接口、爆破验证码、恶意调用开放 API。
- 规则命中型攻击:包括扫描器、恶意 UA、目录探测、SQL 注入/XSS 探测、异常方法调用等。这类攻击更适合由特征规则和行为分析来拦截。
现场最常见的误判往往源于混淆了上述层次:
- 看到 QPS 高,就直接把
limit_req 的阈值调得很低,结果最先受伤的却是正常流量。
- 看到连接数高,就只想着加
limit_conn,但真实问题可能是某个登录接口被脚本高速重放,关键在于请求速率而非连接数。
- 把 WAF 规则写得过宽,导致搜索、富文本上传、代码编辑等业务接口被大量误杀。
- 在 Nginx 之前部署了 CDN 或四层代理,却没有正确配对真实源地址链路,导致所有流量都被识别成代理 IP,使得限流和封禁完全失真。
1.2 基础背景 / 核心语义
| 攻击或异常类型 |
Nginx 可见层级 |
首选能力 |
不适合的误用方式 |
正确处理方向 |
| 大带宽 DDoS |
网络入口之前 |
上游清洗/CDN/高防 |
指望 Nginx 单机硬扛 |
先做接入层清洗,再做应用层限流 |
| 连接洪泛 |
TCP 连接层 |
limit_conn、连接超时、backlog 配合 |
只靠 URI 级限流 |
先限制并发连接和空闲连接耗尽 |
| 请求洪泛 |
HTTP 请求层 |
limit_req |
只看 active connections |
依据接口画像设定速率阈值 |
| 目录扫描 |
URI/方法/UA |
WAF 规则、黑白名单 |
只调连接数 |
基于路径、方法、特征串阻断 |
| SQL 注入/XSS 探测 |
参数/请求体 |
WAF/CRS/上游应用校验 |
仅靠 deny IP |
分阶段规则命中、审计与灰度 |
| 撞库/爆破 |
接口行为层 |
限速、验证码、人机校验、账号维度联动 |
只按 IP 限制 |
IP、账号、设备、多维度联合控制 |
| 指令或模块 |
作用 |
典型放置位置 |
常见副作用 |
核心边界 |
limit_conn_zone |
定义连接数统计键和共享内存区 |
http |
key 设计错误导致统计失真 |
只限制并发连接,不限制每秒请求数 |
limit_conn |
限制某键并发连接数 |
http/server/location |
HTTP/2 下一个连接多路复用需谨慎理解 |
更适合防连接占满 |
limit_req_zone |
定义请求速率统计键和内存区 |
http |
zone 太小导致淘汰和误判 |
核心是请求速率,不是总量 |
limit_req |
限制请求速率,可带 burst/nodelay |
server/location |
阈值过低会放大突刺误伤 |
适合登录、验证码、开放 API |
real_ip_header / set_real_ip_from |
恢复真实客户端地址 |
http/server |
信任链错误会被伪造源地址绕过 |
只信任已知代理来源 |
| WAF 规则 |
按 UA、URI、方法、参数特征拦截 |
server/location 或 OpenResty/Lua/ModSecurity |
规则过宽导致误杀 |
必须灰度、审计、可回滚 |
1.3 适用场景
- 公网 API 网关,需要对开放接口和鉴权接口进行分层限流。
- 登录、注册、验证码、短信发送等高风险接口,需要抵御撞库和刷量攻击。
- 反向代理入口,需要将异常路径扫描、恶意 UA、无意义方法调用挡在上游之前。
- 管理后台或内网开放页面,需要通过 IP 白名单和低阈值限流控制暴露面。
- 静态站点、下载站点、图片服务,需要识别路径扫描、盗链或低成本资源滥用。
- 前置有 CDN、SLB、四层代理的环境,必须保证真实源地址恢复正确,否则限流、审计、黑白名单将全部失效。
1.4 环境要求
| 组件 |
版本 / 要求 |
说明 |
| Nginx |
1.20+,建议 1.24 或更新稳定版 |
限流和真实 IP 模块能力成熟,便于统一维护 |
| OpenResty |
1.21+ 可选 |
需要 Lua 规则、动态封禁、共享字典时更方便 |
| ModSecurity v3 |
可选 |
需要接入 OWASP CRS 时使用 |
| ngx_http_realip_module |
必须确认已编译 |
前有代理时必须恢复真实地址 |
| ngx_http_stub_status_module 或 exporter |
建议启用 |
便于监控连接数和请求状态 |
| 日志 |
access log 至少保留状态码、请求时间、上游时间、真实 IP |
没有观测字段就无法调整阈值 |
| Prometheus / 日志平台 |
建议具备 |
上线后需要观察 429、403、连接数、规则命中率 |
在照抄任何配置之前,先确认你的 Nginx 编译了哪些模块,不要想当然。
nginx -V 2>&1 | tr ' ' '\n' | egrep 'nginx version|with-http_realip_module|with-http_stub_status_module|with-http_v2_module'
输出里至少要确认版本、realip 模块、状态页能力。如果你计划接入 ModSecurity,还要确认对应的动态模块是否存在。
另一个关键步骤是导出最终生效的完整配置,后续排查限流、WAF、白名单问题时,要以这个展开后的结果为准,而不是某个孤立的 include 片段。
nginx -T >/tmp/nginx.full.conf 2>/tmp/nginx.dump.err && wc -l /tmp/nginx.full.conf && tail -n 20 /tmp/nginx.dump.err
1.5 安全配置判断坐标系
先看证据,再决定动作。一个可执行的判断坐标系如下:
| 观察面 |
先看什么 |
如果异常说明什么 |
下一步去哪里 |
| 连接层 |
active connections、accepts、SSL 握手、SYN backlog |
连接占用型压力偏大 |
limit_conn、连接超时、系统 socket 参数 |
| 请求层 |
QPS、单 URI QPS、状态码分布、429 比例 |
请求洪泛、接口被刷 |
limit_req、接口级限流、账号维度风控 |
| 规则层 |
403/444、恶意 UA、目录扫描路径、异常方法 |
扫描和特征命中型攻击 |
WAF、黑白名单、特征规则 |
| 真实地址层 |
remote_addr 与 X-Forwarded-For |
限流对象失真 |
real_ip_header 与信任链检查 |
| 上游层 |
upstream 响应时间、5xx、连接池耗尽 |
上游已被放大 |
回头检查入口阈值和熔断 |
| 网络外部 |
带宽、丢包、RTT |
大流量型或链路型攻击 |
CDN/高防/运营商侧联动 |
下面这条链路可以作为全篇的执行顺序:
先确认真实 IP -> 再补齐日志字段 -> 再按连接层/请求层/规则层划分问题 ->
优先做低风险审计或软性限制 -> 再灰度硬拦截 -> 上线后盯 429/403/延迟/误杀比例 ->
保留独立回滚入口,只回滚新增规则或高风险阈值
二、安全配置方法、风险边界与落地步骤
2.1 先把观测面补齐
没有清晰的观测,调整阈值无异于盲人摸象。首先,统一日志格式,确保至少能看到真实 IP、请求时间、上游响应时间、限流结果以及基本的请求特征。
log_format security_main '$realip_remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time '
'uaddr="$upstream_addr" ustatus="$upstream_status" '
'host="$host" uri="$uri" args="$args" '
'xff="$http_x_forwarded_for" '
'ua="$http_user_agent" ref="$http_referer"';
access_log /var/log/nginx/access.security.log security_main;
error_log /var/log/nginx/error.log warn;
其中最有价值的几个字段是:
$realip_remote_addr:恢复后的真实客户端 IP。
$request_time:Nginx 整体处理时长。
$upstream_response_time:上游响应时长,用来区分入口拦截和上游自身变慢。
$status:判断请求是被限流、被规则拦截还是上游异常。
$uri、$args、$http_user_agent:后续做规则归类和特征分析的主要素材。
关键一步:恢复真实 IP
如果 Nginx 前面挂了 CDN、SLB 或四层代理,必须先把真实地址链路配对好,否则所有后续的限制都将建立在错误的地址之上。
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
set_real_ip_from 100.64.0.0/10;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
重要提醒:这段配置只有在你的前置代理地址范围明确且可信时才能使用。绝对不要把 0.0.0.0/0 加入信任列表,否则任何客户端都能伪造 X-Forwarded-For 头来绕过你的限流和白名单。
快速验证真实地址是否生效:
tail -n 20 /var/log/nginx/access.security.log | awk '{print $1, $9, $12, $13}'
如果输出第一列(IP地址)始终是代理网段或固定负载均衡地址,说明真实 IP 没有恢复好。在解决这个问题之前,任何限流和黑名单策略都不要急于上线。
补充连接层面观测
至少需要能看到活跃连接和读写状态。
server {
listen 127.0.0.1:8080;
location /nginx_status {
stub_status;
allow 127.0.0.1;
deny all;
}
}
访问状态页:
curl -s http://127.0.0.1:8080/nginx_status
输出通常类似:
Active connections: 233
server accepts handled requests
912345 912345 1482340
Reading: 5 Writing: 39 Waiting: 189
如果 Active connections 持续很高,但总请求量(requests)并不高,你应该优先怀疑慢连接、空闲连接占用、上游响应拖长或 HTTP/2 长连接占比异常,而不是立刻断定接口被刷。
2.2 第一轮判断:先分清是哪一层的问题
第一轮判断的目标不是立刻拦截,而是将问题收敛到连接层、请求层还是规则层。
先快速分析状态码、URI 和来源分布:
awk '{print $9}' /var/log/nginx/access.security.log | sort | uniq -c | sort -nr | head -20
awk -F'uri="|" args=' '{print $2}' /var/log/nginx/access.security.log | sort | uniq -c | sort -nr | head -30
awk '{print $1}' /var/log/nginx/access.security.log | sort | uniq -c | sort -nr | head -30
如果同一 IP 在短时间内持续请求同一接口,且 429 状态码开始升高,说明请求速率类限制已经介入或阈值过紧。如果 403/444 很高且 URI 分散、UA 异常,这更像扫描和规则命中。如果连接数明显攀升但单 URI QPS 不高,则需要检查连接耗尽和超时设置。
可以使用下表快速收敛问题:
| 现象 |
首看指标或日志 |
更可能的问题层 |
第一处理动作 |
| active connections 高,QPS 不高 |
stub_status、request_time |
连接层 |
先看连接数限制、keepalive、超时 |
| 某个 URI QPS 高,连接数一般 |
URI TopN、来源 IP TopN |
请求层 |
给接口加 limit_req,不要先改全局 |
| 403/444 上升,URI 离散且 UA 异常 |
access log、规则命中日志 |
规则层 |
先审计扫描特征和黑白名单 |
| 5xx 上升伴随 upstream_time 高 |
upstream 日志 |
上游已吃满 |
限流入口侧,保护上游 |
| 真实来源几乎都一样 |
$realip_remote_addr |
地址恢复错误 |
先修 real IP |
快速判断某个特定接口(如 /api/login)是否被刷:
grep 'uri="/api/login"' /var/log/nginx/access.security.log | awk '{print $1}' | sort | uniq -c | sort -nr | head -20
grep 'uri="/api/login"' /var/log/nginx/access.security.log | awk '{print substr($4,2,17)}' | sort | uniq -c | sort -nr | head -20
第一条命令看来源 IP 的集中度,第二条看秒级请求突刺。如果来源分散但秒级突刺高,可能是代理池在刷接口;如果来源集中且状态码高度一致,则更像固定来源或脚本重放。
2.3 第二轮下钻:连接限制、请求限速、黑白名单与 WAF
2.3.1 连接数限制的正确用法
连接限制首先需要定义统计键。最常见的是按真实客户端地址统计。
limit_conn_zone $binary_remote_addr zone=conn_per_ip:20m;
然后在 server 或特定的 location 上应用:
server {
listen 80;
server_name example.com;
limit_conn conn_per_ip 30;
}
这里的数值 30 并非通用最佳值。HTTP/2、移动网络波动、浏览器多资源并发下载都会影响连接占用。管理后台、登录页、接口网关和静态资源站点的阈值应该分开设置。
对于高风险后台可以更严格:
location /admin/ {
limit_conn conn_per_ip 10;
proxy_pass http://admin_backend;
}
而对于下载接口,则不能简单套用后台的严格阈值,否则断点续传或大文件下载很容易被误伤。
常见误改:
| 误改动作 |
后果 |
更稳的做法 |
全站统一设置很低的 limit_conn |
浏览器并发资源加载、HTTP/2 用户被误伤 |
按业务路径拆分阈值 |
| 前有代理但没恢复真实 IP |
全部用户被视为一个代理地址,限流失效 |
先修 real_ip,再设置连接限制 |
看到连接高就只调 limit_conn |
请求洪泛仍未被控制 |
区分连接数与请求速率,对症下药 |
2.3.2 请求速率限制的正确用法
limit_req 比 limit_conn 更适合处理撞库、登录爆破、验证码接口刷量、开放 API 被打满等问题。
先定义统计键和共享内存区:
limit_req_zone $binary_remote_addr zone=req_per_ip:50m rate=10r/s;
再在具体接口上应用:
location = /api/login {
limit_req zone=req_per_ip burst=20 nodelay;
proxy_pass http://auth_backend;
}
这里有三个关键点:
rate=10r/s 是稳定速率,不是峰值总量。
burst=20 允许短时间的请求突刺进入排队或快速处理。
nodelay 表示不让突刺请求排队等待,而是超过速率后直接按 burst 许可的数量快速通过,后续超出部分再拒绝。
如果你希望平滑排队而不是立即拒绝,可以不加 nodelay:
location = /api/sms/send {
limit_req zone=req_per_ip burst=5;
proxy_pass http://sms_backend;
}
不加 nodelay 时,短时突刺会被延迟处理而不是立刻拒绝。这对用户可感知的接口可能会增加尾延迟,但比直接返回大量 429 更平滑。
为开放 API 做更稳妥的分级:
limit_req_zone $binary_remote_addr zone=api_default:50m rate=50r/s;
limit_req_zone $binary_remote_addr zone=api_sensitive:20m rate=5r/s;
location /api/public/ {
limit_req zone=api_default burst=100 nodelay;
proxy_pass http://public_backend;
}
location ~ ^/api/(login|register|password|otp) {
limit_req zone=api_sensitive burst=10 nodelay;
proxy_pass http://auth_backend;
}
为管理后台设置更严格的请求限制:
location /manage/ {
limit_req zone=api_sensitive burst=3 nodelay;
limit_conn conn_per_ip 5;
proxy_pass http://manage_backend;
}
如果网关层面已经有用户身份信息,可以考虑将限流键改为更稳定的标识(如 API Key),但要避免直接使用客户端可伪造的 Header。
map $http_x_api_client $rate_limit_key {
default $binary_remote_addr;
"" $binary_remote_addr;
}
limit_req_zone $rate_limit_key zone=client_api:30m rate=20r/s;
只有在 X-API-Client 这样的头由可信网关(而非客户端)注入且无法伪造时,才建议这样做。
2.3.3 黑白名单与可信来源边界
对于管理后台、健康检查或办公网出口场景,白名单比复杂规则更稳定,但前提是来源链路可信。
geo $office_allow {
default 0;
203.0.113.0/24 1;
198.51.100.10/32 1;
}
map $office_allow $admin_guard {
0 1;
1 0;
}
location /admin/ {
if ($admin_guard) {
return 403;
}
proxy_pass http://admin_backend;
}
如果办公网出口 IP 经常变动,不建议长期靠手工维护 CIDR 列表。更稳妥的方式是由 VPN、零信任网关或统一入口注入可信标识,再由 Nginx 做二次校验。
黑名单适合快速止血,但不适合替代长期规则体系:
deny 198.51.100.23;
deny 203.0.113.44;
allow all;
黑名单最大的问题是扩展性差、维护成本高、在代理池面前效果有限,而且容易误封共享出口。它更适合用作临时措施,而非长期策略。
2.3.4 WAF 基础规则编写
如果暂时不使用 ModSecurity 或 CRS,也可以先用 Nginx 原生的 map 和 if 做基础规则拦截,把低价值的扫描和异常方法挡在外面。
拦截不必要的危险 HTTP 方法:
if ($request_method !~ ^(GET|HEAD|POST)$ ) {
return 405;
}
拦截常见扫描路径:
location ~* /(\.git|\.svn|\.env|wp-admin|wp-login\.php|phpmyadmin|actuator|jenkins|manager/html) {
return 403;
}
拦截恶意或低价值 User-Agent:
map $http_user_agent $bad_ua {
default 0;
~*(sqlmap|nmap|nikto|masscan|acunetix|dirbuster|gobuster) 1;
}
server {
if ($bad_ua) {
return 403;
}
}
按参数特征做轻量规则时要非常克制,优先只拦截明显的扫描器模式,不要一开始就覆盖宽泛的业务关键字。
map $query_string $bad_args {
default 0;
~*(union[[:space:]]+select|sleep\(|benchmark\(|information_schema|/etc/passwd|<script) 1;
}
server {
if ($bad_args) {
return 403;
}
}
这类规则只能作为低阶保护,不要把它当作 SQL 注入和 XSS 的完整防线。业务接口中可能本来就有 <script>、select、union 等合法内容,尤其在搜索、富文本编辑、代码托管、模板编辑等场景更容易误杀。
更稳妥的方式是先审计,再拦截:
map $query_string $bad_args_audit {
default 0;
~*(union[[:space:]]+select|sleep\(|benchmark\(|information_schema|<script) 1;
}
log_format waf_audit '$time_local ip=$realip_remote_addr status=$status uri="$uri" args="$args" hit_bad_args=$bad_args_audit ua="$http_user_agent"';
access_log /var/log/nginx/waf_audit.log waf_audit if=$bad_args_audit;
先让命中规则的请求进入审计日志,确认误伤面可控后,再决定是否升级为直接 return 403。
2.3.5 接入 ModSecurity 与 CRS 的基础模式
如果需要系统化的 WAF 能力,建议使用 ModSecurity v3 配合 OWASP CRS,并采用“先检测、后拦截”的灰度策略。
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/main.conf;
main.conf 中最关键的是先把引擎模式设为检测,而非一上来就阻断:
SecRuleEngine DetectionOnly
Include /etc/nginx/modsec/crs/crs-setup.conf
Include /etc/nginx/modsec/crs/rules/*.conf
当审计周期证明误伤面可控后,再改为:
SecRuleEngine On
如果只希望对高风险路径开启 WAF,最好按 location 精准启用,不要全站一刀切:
location /api/ {
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/api-only.conf;
proxy_pass http://api_backend;
}
2.3.6 WAF 误杀控制
误杀通常出现在三类业务:搜索接口、富文本接口、上传或模板编辑接口。控制方式不是简单地关闭规则,而是缩小规则作用范围、排除特定业务路径或参数。
示例:对搜索接口跳过过于宽泛的 SQL 注入规则。
SecRule REQUEST_URI "@beginsWith /api/search" \
"id:100100,phase:1,pass,nolog,ctl:ruleRemoveByTag=attack-sqli"
示例:只对某个特定参数(如 content)排除某条规则(ID: 942100)。
SecRule REQUEST_URI "@streq /api/editor/save" \
"id:100101,phase:1,pass,nolog,ctl:ruleRemoveTargetById=942100;ARGS:content"
误杀处理原则是:
- 先保留完整的命中日志。
- 只排除受影响的具体路径或参数。
- 不要直接关闭整类规则集。
- 排除后继续观察误杀比例和真正的攻击命中率。
2.4 根因与风险矩阵
| 现象 |
首看证据 |
二次验证 |
高概率原因 |
处理动作 |
| 连接数高但 QPS 一般 |
stub_status 中 active/waiting 高 |
看 request_time、keepalive、HTTP/2 连接占比 |
慢连接、空闲连接占坑、上游慢拖长 |
调整超时、连接限制、上游保护 |
| 登录接口被刷 |
/api/login URI QPS、来源 IP TopN |
看 401/429 分布、秒级请求量 |
撞库、代理池爆破 |
登录接口专属 limit_req,联动验证码 |
| 403 暴涨 |
WAF 审计日志 |
样本回放、看受影响 URI |
新规则误伤或扫描被拦截 |
分辨误伤与真实命中,必要时局部排除 |
| 429 暴涨且投诉增加 |
429 比例、用户投诉接口 |
关联正常用户行为日志 |
阈值过低或 burst 太小 |
提高阈值、扩大 burst、按路径拆分 |
| 所有请求来自同一 IP |
access log 中真实 IP 单一 |
查看 XFF、代理地址 |
real IP 配置错误 |
先修地址恢复,再评估安全规则 |
| 5xx 上升同时 upstream_time 高 |
access log、upstream 状态 |
上游实例 CPU/连接池/线程池 |
限流不够、异常流量已打穿上游 |
入口限流 + 上游扩容/熔断 |
2.5 推荐调整、临时止血、正式加固、回滚边界与验证动作
先给出一套可执行的分层策略:
| 阶段 |
目标 |
推荐动作 |
不要做什么 |
| 观测补齐 |
看清真实问题 |
修 real IP、补日志、补状态页 |
在黑盒状态下直接大幅降低阈值 |
| 临时止血 |
先保业务可用 |
针对热点 URI 加临时限速、封禁显著恶意 UA/IP |
全站大幅度收紧所有阈值 |
| 正式加固 |
形成长期策略 |
分路径限流、WAF 灰度、监控告警、准备回滚脚本 |
把临时 deny 列表当作长期方案 |
| 持续运营 |
降低误伤和漏拦 |
周期复盘 429/403/规则命中率 |
长期不校准阈值,配置僵化 |
一个稳妥的临时止血示例如下:
location = /api/login {
limit_req zone=api_sensitive burst=5 nodelay;
proxy_pass http://auth_backend;
}
if ($bad_ua) {
return 403;
}
正式加固则要把配置拆层,并且每一层都能独立回滚:
include conf.d/security/realip.conf;
include conf.d/security/rate-limit-zones.conf;
include conf.d/security/waf-maps.conf;
include conf.d/security/site-policy.conf;
这样做的好处是,回滚时可以只下掉某一类策略,而不需要整站回滚。比如新 WAF 规则误伤时,只回滚 waf-maps.conf 或特定 location 规则,不影响真实 IP 恢复和限流策略。
验证动作至少包含:
- 语法检查:
nginx -t
- 恶意 UA 验证:
curl -I -A 'sqlmap/1.7' http://example.com/
- 限流验证(模拟撞库):
for i in $(seq 1 20); do curl -s -o /dev/null -w '%{http_code}\n' http://example.com/api/login; done | sort | uniq -c
nginx -t 只证明语法没问题,不证明阈值正确、真实 IP 无误、误伤可控。必须还要做 UA 命中验证、限流验证和正常业务回归验证。
三、示例代码和配置
3.1 一套可落地的安全配置目录结构
/etc/nginx/
├── nginx.conf
├── conf.d/
│ ├── site.conf
│ └── security/
│ ├── realip.conf
│ ├── rate-limit-zones.conf
│ ├── waf-maps.conf
│ ├── allow-deny.conf
│ └── site-policy.conf
└── modsec/
├── main.conf
└── crs/
目录拆分的目的是将真实地址恢复、限流区定义、WAF 规则、黑白名单和站点应用层策略解耦,便于灰度发布和回滚。
3.2 主配置样例:全局安全基础能力
# 场景:全局启用真实 IP、日志格式和限流区定义
# 文件位置:/etc/nginx/nginx.conf
worker_processes auto;
worker_rlimit_nofile 200000;
events {
worker_connections 8192;
multi_accept on;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 15;
keepalive_requests 1000;
reset_timedout_connection on;
client_header_timeout 10s;
client_body_timeout 15s;
send_timeout 15s;
include conf.d/security/realip.conf;
log_format security_main '$realip_remote_addr [$time_local] '
'"$request" $status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time '
'uaddr="$upstream_addr" uri="$uri" '
'args="$args" ua="$http_user_agent" '
'xff="$http_x_forwarded_for"';
access_log /var/log/nginx/access.security.log security_main;
include conf.d/security/rate-limit-zones.conf;
include conf.d/security/waf-maps.conf;
include conf.d/*.conf;
}
关键点:
- 超时设置不宜过长,否则慢连接和恶意拖延请求更容易占用资源。
worker_connections 和 worker_rlimit_nofile 需与系统级文件描述符上限匹配。
- 全局只定义能力(日志格式、限流区),不直接在全局一刀切应用所有业务阈值。
3.3 真实 IP 配置样例
# 场景:前有四层负载均衡和 CDN,需要恢复客户端真实 IP
# 文件位置:/etc/nginx/conf.d/security/realip.conf
set_real_ip_from 10.10.0.0/16;
set_real_ip_from 172.20.0.0/16;
set_real_ip_from 192.168.10.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
如果前置代理使用的是 Proxy Protocol 而非标准 X-Forwarded-For,需要改用对应的配置,不要混用。
3.4 限流区定义样例
# 场景:为不同业务类型准备不同限流区
# 文件位置:/etc/nginx/conf.d/security/rate-limit-zones.conf
limit_conn_zone $binary_remote_addr zone=conn_per_ip:20m;
limit_req_zone $binary_remote_addr zone=req_default:50m rate=30r/s;
limit_req_zone $binary_remote_addr zone=req_login:20m rate=5r/s;
limit_req_zone $binary_remote_addr zone=req_sms:20m rate=2r/s;
limit_req_zone $binary_remote_addr zone=req_search:30m rate=15r/s;
不要一开始就为每个接口建立一个 zone,先按业务类别分组即可。zone 太多、太碎会提升维护成本,也让问题定位变复杂。
3.5 站点级策略样例
server {
listen 80;
server_name example.com;
limit_conn conn_per_ip 50;
location / {
limit_req zone=req_default burst=60 nodelay;
proxy_pass http://web_backend;
}
location = /api/login {
limit_conn conn_per_ip 10;
limit_req zone=req_login burst=10 nodelay;
proxy_pass http://auth_backend;
}
location = /api/sms/send {
limit_req zone=req_sms burst=3;
proxy_pass http://sms_backend;
}
location = /api/search {
limit_req zone=req_search burst=20;
proxy_pass http://search_backend;
}
}
这套配置体现了三个原则:
- 默认接口和高风险接口阈值分离。
- 登录和短信接口不仅限速,还可叠加连接限制。
- 搜索接口保留一定突刺和排队空间,避免误伤热点检索。
3.6 WAF Map 规则样例
# 场景:先用原生 map 拦截低价值扫描与恶意特征
# 文件位置:/etc/nginx/conf.d/security/waf-maps.conf
map $http_user_agent $bad_ua {
default 0;
~*(sqlmap|masscan|acunetix|nikto|nmap|dirbuster|gobuster) 1;
}
map $uri $bad_uri {
default 0;
~*/(\.git|\.env|phpmyadmin|wp-admin|wp-login\.php|actuator|jenkins|manager/html) 1;
}
map $query_string $bad_args {
default 0;
~*(union[[:space:]]+select|sleep\(|benchmark\(|/etc/passwd|<script) 1;
}
3.7 规则应用样例
server {
listen 80;
server_name example.com;
if ($bad_ua) {
return 403;
}
if ($bad_uri) {
return 403;
}
location / {
if ($bad_args) {
return 403;
}
proxy_pass http://web_backend;
}
}
如果业务中存在富文本、代码搜索、模板编辑等场景,不建议把 $bad_args 直接在全站硬拦截。更稳妥的是先只在高风险接口或非业务关键路径应用,或先走审计模式。
3.8 管理后台白名单样例
geo $office_allow {
default 0;
203.0.113.10/32 1;
203.0.113.11/32 1;
198.51.100.0/24 1;
}
server {
listen 443 ssl http2;
server_name admin.example.com;
location / {
if ($office_allow = 0) {
return 403;
}
limit_conn conn_per_ip 5;
limit_req zone=req_login burst=3 nodelay;
proxy_pass http://admin_backend;
}
}
3.9 日志采集脚本
#!/usr/bin/env bash
set -euo pipefail
# 文件名:collect_nginx_security_snapshot.sh
# 作用:快速采集 Nginx 安全相关运行快照,便于排查连接洪泛、请求洪泛和规则命中
# 使用方法:bash collect_nginx_security_snapshot.sh /var/log/nginx/access.security.log
LOG_FILE="${1:-/var/log/nginx/access.security.log}"
printf '== time ==\n'
date '+%F %T'
printf '\n== status top ==\n'
awk '{print $4,$9}' "$LOG_FILE" | tail -n 50000 | awk '{print $2}' | sort | uniq -c | sort -nr | head -10
printf '\n== uri top ==\n'
awk -F'uri="|"' '{print $2}' "$LOG_FILE" | tail -n 50000 | sort | uniq -c | sort -nr | head -10
printf '\n== ip top ==\n'
awk '{print $1}' "$LOG_FILE" | tail -n 50000 | sort | uniq -c | sort -nr | head -10
printf '\n== nginx status ==\n'
curl -s http://127.0.0.1:8080/nginx_status || true
结果解读:
status top:看 403、429、5xx 是否显著抬升。
uri top:看热点 URI 是否集中在登录、短信、搜索等接口。
ip top:看来源是否高度集中(单点攻击)还是分散(代理池)。
nginx status:看 active/waiting 连接数是否与请求量匹配。
3.10 验证与回滚辅助脚本
#!/usr/bin/env bash
set -euo pipefail
# 文件名:verify_or_rollback_nginx_policy.sh
# 作用:对新规则执行语法检查、灰度验证,并支持按文件级快速回滚
# 使用方法:
# bash verify_or_rollback_nginx_policy.sh verify /etc/nginx/conf.d/security/waf-maps.conf
# bash verify_or_rollback_nginx_policy.sh rollback /etc/nginx/conf.d/security/waf-maps.conf
ACTION="${1:-verify}"
TARGET_FILE="${2:-/etc/nginx/conf.d/security/waf-maps.conf}"
BACKUP_FILE="${TARGET_FILE}.bak"
case "$ACTION" in
verify)
nginx -t
systemctl reload nginx
;;
rollback)
cp "$BACKUP_FILE" "$TARGET_FILE"
nginx -t
systemctl reload nginx
;;
*)
echo "usage: $0 verify|rollback /path/to/file" >&2
exit 1
;;
esac
3.11 关键验证命令速查
- 登录接口限流验证:
seq 1 30 | xargs -I{} -P10 curl -s -o /dev/null -w '%{http_code}\n' http://example.com/api/login | sort | uniq -c
- 恶意 UA 命中验证:
curl -I -A 'sqlmap/1.7#stable' http://example.com/
- 异常路径验证:
curl -I http://example.com/.git/config
curl -I http://example.com/wp-login.php
- 查看近期限流命中:
grep ' 429 ' /var/log/nginx/access.security.log | tail -n 50
四、监控、验证与回滚
4.1 关键监控指标
| 指标名称 |
正常范围 |
告警阈值 |
说明 |
| 总 QPS |
按业务基线 |
高于基线 2-3 倍 |
判断是否有异常流量进入 |
| active connections |
与并发规模匹配 |
持续高于基线 2 倍 |
判断连接层压力 |
| 429 比例 |
通常接近 0 或小幅可控 |
高于 1%-3% 且持续 5 分钟 |
判断限流是否开始影响正常流量 |
| 403 比例 |
取决于扫描面 |
突升超过基线 3 倍 |
判断规则命中或误杀 |
| 5xx 比例 |
接近 0 |
高于 0.5%-1% |
判断上游是否已被打穿 |
| P95 请求时延 |
按业务基线 |
高于基线 50% |
判断排队、上游抖动或误伤引发重试 |
4.2 Prometheus 告警规则示例
如果已接入 Nginx exporter 或通过日志转指标,可从以下基础规则开始。它们能帮你快速定位 运维 层面的核心问题。
- alert: Nginx429RateHigh
expr: sum(rate(nginx_http_requests_total{status="429"}[5m])) / sum(rate(nginx_http_requests_total[5m])) > 0.03
for: 10m
labels:
severity: warning
annotations:
summary: "Nginx 429 ratio too high"
description: "5分钟窗口内 429 占比超过 3%,需判断是防护成功还是误伤扩大。"
- alert: Nginx403RateSpike
expr: sum(rate(nginx_http_requests_total{status="403"}[5m])) > 20
for: 10m
labels:
severity: warning
annotations:
summary: "Nginx 403 spike"
description: "403 在 10 分钟持续升高,需检查新 WAF 规则或扫描行为。"
4.3 回滚方案
回滚必须分层,不要一出问题就整站回退。推荐三层回滚策略:
| 回滚层级 |
适用场景 |
回滚动作 |
观察窗口 |
| 只回滚新 WAF 规则 |
403 突升、误杀、搜索/编辑器异常 |
恢复 waf-maps.conf 或特定 CRS 排除规则 |
10-30 分钟 |
| 只回滚高风险限流阈值 |
429 高、业务投诉、正常用户失败 |
恢复 site-policy.conf 中相关 location 阈值 |
10-15 分钟 |
| 回滚整个安全策略分组 |
发布组织错误、真实 IP 配错、整体策略异常 |
恢复 security/ 目录到上一个已知良好版本 |
30-60 分钟 |
示例:回滚 WAF 新规则
cp /etc/nginx/conf.d/security/waf-maps.conf.bak /etc/nginx/conf.d/security/waf-maps.conf && nginx -t && systemctl reload nginx
回滚后要盯两件事:
- 原来的业务异常是否立刻消失。
- 原先被挡住的恶意流量是否重新抬头。
如果回滚后业务恢复,但恶意流量随即压高,说明问题不是“规则不该有”,而是“规则太宽或阈值太紧”。下一步应该做更细粒度的规则收敛,而不是彻底放弃入口防护。
五、总结与技术要点回顾
- 分层治理:Nginx 入口防护必须先分清连接层、请求层和规则层。
limit_conn 防占坑,limit_req 防刷量,WAF 防特征,三者不能互相替代。
- 真实 IP 是基石:前面有代理时,正确恢复真实源地址是所有限流、封禁、白名单和审计的前提。配置错误会导致所有防护策略失真。
- 灰度与审计:WAF 规则最容易出问题的不是“挡不住”,而是“误伤正常业务”。必须遵循先审计、再灰度、最后阻断的流程,尤其对搜索、富文本等敏感接口。
- 指标联动分析:429 和 403 本身不是坏指标。关键要结合 5xx 错误率、上游响应时延、用户投诉和命中分布一起判断防护效果。
- 配置可回滚:安全配置必须设计成可独立回滚的。按 real IP、限流区、规则映射、业务策略分层拆分配置文件,是线上稳定的保障。
- 验证高于语法:
nginx -t 只证明语法正确,不证明策略合理。必须补充速率验证、恶意特征验证和核心业务回归验证。
- 认清能力边界:Nginx 是出色的应用层防护工具,但不是高防。切勿把应用层限流和规则拦截当成大带宽 DDoS 清洗的替代品。
通过系统化的配置、严谨的观测和分层的回滚机制,你可以让 Nginx 成为业务入口一道可靠的安全防线。云栈社区 提供了更多关于 网络 架构与运维实践的深入讨论,欢迎持续探索,共同构建更稳固的基础设施。