前言:最近,我的环境遭遇了一场针对性的暴力枚举攻击。
起初,我尝试在云服务器安全组 和 Nginx 中直接封禁攻击 IP,却发现它们像穿了隐身衣------完全无效。
经过排查,我揪出了元凶:CDN 代理。它本为加速而生,却在此刻成了攻击者的"天然屏障"。
本文将复盘我从 IP 封禁失效 ,到利用 WAF 识破真实 IP,再到借助 Nginx + Lua + Redis 实现业务级动态封禁的完整防御进化之路。
希望能为遇到类似困境的你,提供一些实战参考。
一、 背景:诡异的日志
最近,我的网关服务器日志里出现了极其规律的恶意扫描流量。攻击者正在对 API 接口进行猛烈的暴力枚举,试图通过猜测商户号或管理员账号来寻找突破口。
以下是经过脱敏处理的真实日志片段:
bash
2026-05-20 17:29:36.362 DEBUG [io-8188-exec-11] [com.example.dao.MerchantMapper.selectOne] : ==> Preparing: SELECT id, merchant_no, merchant_name, [隐去敏感字段] FROM merchant WHERE (merchant_no = ?)
2026-05-20 17:29:36.362 DEBUG [io-8188-exec-11] [com.example.dao.MerchantMapper.selectOne] : ==> Parameters: adbcmaster(String)
2026-05-20 17:29:36.471 DEBUG [io-8188-exec-29] [com.example.dao.MerchantMapper.selectOne] : <== Total: 0
2026-05-20 17:29:36.471 INFO [io-8188-exec-29] [com.example.security.ExampleBasicAuth] : 用户不存在: admin ipAddr: 155.94.241.106
2026-05-20 17:29:36.472 INFO [io-8188-exec-30] [com.example.security.ExampleBasicAuth] : 用户不存在, 或未开启
2026-05-20 17:29:36.555 INFO [io-8188-exec-29] [com.example.security.ExampleBasicAuth] : 用户不存在: root ipAddr: 155.94.241.106
同一个 IP 155.94.241.106 在几毫秒内,疯狂尝试 adbcmaster、admin、root 等账号,并频繁查询 merchant_no。这明显是一次自动化枚举攻击。
那么问题来了:一个显而易见的恶意 IP,为什么我在云服务器安全组和 Nginx 里封禁都无效呢?
二、 复盘:为什么传统 IP 封禁失灵?
1. 部署现状
我的网站启用了 CDN(例如 Cloudflare 或 AgileCDN),所有流量先经过 CDN 节点,再回源到源站 IP。
2. 云防火墙 / 安全组为何无效?
当我在云服务器(Azure/阿里云/腾讯云)的安全组里添加规则 拒绝 IP 155.94.241.106 对所有端口的访问 时,我发现完全无效。
原因: 安全组拦截的是 IP 包。攻击者通过 CDN 访问时,安全组看到的源 IP 是 CDN 的边缘节点 IP (如 162.158.175.13),而不是 155.94.241.106。因此,规则无法匹配。
⚠️ 重要提醒:虽然安全组拦不住 CDN 后的 HTTP 请求,但它绝对不能省!
它依然是保护服务器的第一道物理防线。假设攻击者绕过域名,直接扫描源站的 22 端口(SSH)或 3389 端口(RDP)等非 HTTP 服务,安全组是你最后的堡垒。因此,务必在安全组中配置一条拒绝所有 IP 访问源站 非 HTTP/HTTPS 端口 的规则。
✅ 安全组推荐配置(以 Azure NSG 为例):
- 来源 IP 地址 :
155.94.241.106, 45.205.1.8 - 目标端口范围 :
*(拒绝所有端口) - 协议 :
Any - 操作 :
拒绝 - 优先级 :
100(优先级需小于放行规则,如 HTTP(300))
3. Nginx deny 为何也无效?
很多运维第一时间会想到在 Nginx 里写 deny 155.94.241.106;。
原因: 默认情况下,Nginx 的 $remote_addr 记录的是直接发起 TCP 连接的 IP ,即 CDN 节点 IP。因此,deny 同样匹配不到真实 IP。
三、 破局点:Web 应用防火墙 (WAF)
既然传统方法失效,必须引入能识别 CDN 背后真实 IP 的设备 ------Web 应用防火墙 (WAF)。
原理分析
以 Cloudflare WAF 为例,它部署在 CDN 边缘节点。当请求到达 Cloudflare 时,它能直接读取客户端真实 IP(例如 155.94.241.106),无需二次转发。
✅ 实操演示:配置 Cloudflare WAF 拦截真实 IP(操作+参数)
操作步骤:
- 登录 Cloudflare 控制台 -> 安全性 (Security) -> WAF -> 自定义规则 (Custom Rules)。
- 点击 创建规则。
- 填写规则名称(如
Block_Malicious_IPs)。 - 配置 表达式(核心参数) :
-
字段 :选择
IP 来源地址(IP Source Address) -
运算符 :选择
等于(eq) 或属于(in) -
值 :输入
155.94.241.106(如需多个,使用{"155.94.241.106" "45.205.1.8"}) -
表达式预览(可直接粘贴到编辑器) :
nginxip.src eq "155.94.241.106"或(拦截多个 IP):
nginxip.src in {"155.94.241.106" "45.205.1.8"} -
💡 注意: Cloudflare 要求 IP 地址必须用双引号包裹,否则规则无法保存。
-
- 然后采取措施 :选择 阻止 (Block)。
- 放置位置 :建议自定义并设置为
优先级1(最顶部),确保最先匹配。 - 点击 部署。
效果验证:
部署后,在 WAF -> 事件 日志里,你会立刻看到 155.94.241.106 被成功拦截的记录。
⚠️ 注意:WAF 只能保护开启了 CDN 代理(橙色云)的域名。如果你的域名仅做 DNS 解析(灰色云),流量不经过 Cloudflare,WAF 无效。
四、 终极方案:Nginx + Lua(针对业务字段的精准防御)
虽然 WAF 解决了 IP 识别问题,但攻击者只要换个 IP 就得重新去 WAF 后台添加规则,运维成本高。如果你有唯一的「商户号」或「渠道号」,Nginx+Lua+Redis 是杀伤力最强的动态防御方案。
为什么 Nginx+Lua 比纯 Nginx if 更好?
| 维度 | 纯 Nginx if |
Nginx + Lua |
|---|---|---|
| 配置生效 | 修改文件后必须 nginx -s reload |
实时生效(通过 Redis 动态控制) |
| 性能 | 在 location 里写复杂 if 会消耗 CPU |
使用 access_by_lua 阶段,性能优秀 |
| 防御粒度 | 只能匹配简单的字符串 | 可以执行复杂的业务逻辑(查数据库、限流等) |
| IP 依赖性 | 依赖 IP,变 IP 就失效 | 不依赖 IP,只要业务字段(商户号)不变,立刻拦截 |
✅ 实操:Nginx+Lua 配置参数(拦截特定商户号)
前提: 假设你的业务里存在统一的 merchant_id,并且您安装了 lua-resty-redis 库。
1. 在 Nginx 的 http 块中配置 Lua 路径:
nginx
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
lua_shared_dict limit_store 10m;
2. 编写高性能 Lua 脚本(例如 /etc/nginx/lua/block_merchant.lua):
lua
-- 引入 redis 库
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000)
-- 连接 redis
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
-- 👇 此处建议加上错误日志,以便运维人员能及时发现 Redis 故障
ngx.log(ngx.ERR, "Failed to connect to Redis: ", err)
return
end
-- 从请求参数或 body 中获取 merchant_id(根据实际业务调整)
local args = ngx.req.get_uri_args()
local merchant_id = args["merchant_id"]
if not merchant_id then
-- 尝试从 POST body 解析
ngx.req.read_body()
local body = ngx.req.get_body_data()
-- 注意:若 body 过大导致 get_body_data() 为 nil,此处暂不处理缓存文件
if body then
local pattern = "merchant_id[=: ]([0-9]+)"
local m, err = ngx.re.match(body, pattern, "jo")
if m then
merchant_id = m[1]
end
end
end
if merchant_id then
-- 去 Redis 查这个 merchant_id 是否在黑名单里
local key = "blocked:merchant:" .. merchant_id
local is_blocked = red:get(key)
if is_blocked == "1" then
ngx.log(ngx.INFO, "Blocked request for merchant_id: ", merchant_id)
-- ✅ 即使被拦截,也要放回连接池,防止连接泄漏
red:set_keepalive(10000, 100)
return ngx.exit(403) -- 直接返回 403
end
end
-- ✅ 核心优化:使用连接池替代 close,提升高并发性能
red:set_keepalive(10000, 100)
📝 总结段落(替换红框内容):
在连接 Redis 失败时,我们引入了 ngx.log(ngx.ERR)。这不仅是为了让运维能第一时间在 error.log 中捕捉到缓存故障,更重要的是,我们选择了 return(放行)而不是 ngx.exit(500)(拦截)。在安全防御中,"降级放行"往往比"全站玉碎"更符合业务连续性要求。
🚀 为什么使用 set_keepalive?
在高并发网关场景下,每个请求都 connect() 然后 close() 会频繁触发 TCP 三次握手和四次挥手,导致 Redis 产生大量 TIME_WAIT 连接,最终可能耗尽服务器端口。
使用 red:set_keepalive(10000, 100) 可以建立一个连接池 ,复用已经建立好的 Redis 连接,极大提升性能并降低资源消耗。这是生产环境 Nginx+Lua 脚本的标准写法。
3. 在 Nginx 的 server 或 location 中引入脚本:
nginx
server {
listen 443 ssl http2;
server_name securegtw.example.com;
# ... SSL配置 ...
location / {
access_by_lua_file /etc/nginx/lua/block_merchant.lua;
proxy_pass http://your_backend;
# ... 其他 proxy 配置 ...
}
}
4. 运维操作:
当发现商户 1001 异常时,运维只需在 Redis 中执行:
bash
redis-cli SET blocked:merchant:1001 1
此时,所有商户 1001 的请求会被 Nginx 直接拦截(返回 403),无需重启 Nginx,秒级生效。
五、 没有统一的商户号,怎么防御?
很多业务初期就是没有统一的商户号/渠道号,或者业务字段是随机的、无法枚举的。这种情况下,你依然可以借助 Nginx 实现很好的防御。
方案一:Nginx 速率限制(防暴力破解/CC 攻击的杀手锏)
这是用来解决"同一个 IP 疯狂试密码"最直接的工具。不需要任何业务 ID,只认 IP。
操作步骤:
- 在
nginx.conf的http块添加限流区域:
nginx
# 定义限流区域,基于真实 IP ($remote_addr),每分钟允许 5 次
limit_req_zone $remote_addr zone=login_limit:10m rate=5r/m;
- 在
server块里的登录接口位置应用:
nginx
location /login {
# 启用限流,超过 5 次/分钟,返回 429
limit_req zone=login_limit burst=3 nodelay;
proxy_pass http://your_backend;
# ... 其他配置 ...
}
⚠️ 关键前提: 必须先配置 set_real_ip_from(让 Nginx 识别 CDN 后的真实 IP),否则限流会限到 CDN 节点头上。
方案二:Nginx 的 realip 模块 + deny(手动封 IP)
如果没有业务 ID,那只能手动把抓到 IP 加入黑名单。虽然笨,但有效。
nginx
# 必须先配置这个,让 Nginx 识别真实 IP
set_real_ip_from 173.245.48.0/20; # (Cloudflare 的 IP 段)
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# 在 server 块里拒绝特定 IP
deny 155.94.241.106;
deny 45.205.1.8;
方案三:Nginx+Lua 来做「动态 IP 黑名单」(运维进阶)
如果你希望不需要重启 Nginx 就能动态封禁新出现的 IP,那可以用 Lua 脚本配合 Redis 来实现。
Lua 脚本逻辑(简化版):
lua
local redis = require "resty.redis"
local red = redis:new()
red:connect("127.0.0.1", 6379)
-- 获取真实 IP
local ip = ngx.var.remote_addr
local is_blocked = red:get("blocked_ip:" .. ip)
if is_blocked == "1" then
red:set_keepalive(10000, 100)
return ngx.exit(403) -- 直接拦截
end
red:set_keepalive(10000, 100)
运维操作:
在 Redis 执行以下命令:
bash
redis-cli SET blocked_ip:155.94.241.106 1
无需重启 Nginx,新 IP 瞬间被拦截。
六、 总结
这次攻防实战让我深刻领悟到防御的进化路径:
- 基础防御 :云服务器安全组。必须配置,它能挡住针对服务器 IP 的直接扫描,是最后一道防线。
- 感知防御 :WAF。解决了 CDN 隐藏真实 IP 的难题,推荐所有开启 CDN 的域名接入,但通过 IP 封禁仍不够完美。
- 逻辑防御 :Nginx + Lua + Redis 。如果你有商户号或渠道号,一定要在这个层面实现"业务级动态封禁"。它能让你从"追着 IP 跑"的疲于奔命中解脱出来,变成"守株待兔"。
记住:真正的安全,是无惧 IP 变换,只在业务逻辑层面做拦截。防御是一场猫鼠游戏,从「封 IP」到「封特征」是运维人员必须面对的进化之路。
(本文日志已进行脱敏处理,仅作技术交流使用。)
