NginxGuard:一款基于 Lua 的高性能轻量级 Web 应用防火墙
NginxGuard:一款基于 Lua 的高性能轻量级 Web 应用防火墙
在 Nginx 请求处理管道中嵌入 Lua,用不到 1000 行核心代码实现企业级 WAF 防护——支持基于域名的差异化规则、IPv6/CIDR 黑白名单、CC 防护、云凭据探测拦截,GET 流量吞吐仅下降约 16%。
为什么需要又一个 WAF?
市面上的 WAF 方案很多:Cloudflare 等 SaaS WAF 简单但黑盒、ModSecurity 功能强大但过重、商业硬件 WAF 价格不菲。很多团队的实际诉求其实很简单:
- 轻量:不想引入额外进程,直接在现有 Nginx 上挂 Lua 脚本
- 可控:规则文件就是纯文本正则,随时改随时生效,不需要重启
- 多域名:不同域名需要不同防护策略,而不是一刀切
- 高性能:不能因为加了 WAF 就让吞吐掉一半
NginxGuard 就是围绕这几个目标设计的。
项目架构
整个项目只有 4 个 Lua 文件和一组规则文件:
1 | waf/ |
工作原理
NginxGuard 挂载在 Nginx 的 access_by_lua_file 阶段,每个请求进入时依次执行安全检测:
1 | # nginx.conf 关键配置 |
init.lua 在 Nginx 启动时预加载模块;access.lua 在每个请求的 access 阶段执行检测逻辑;lib.lua 是核心库,提供 IP 匹配、规则加载、日志记录等能力。
检测流程
每个请求按以下顺序依次检测,命中任一项即拦截并返回 403,后续检测不再执行:
1 | 1. $waf_enable 检查(location 级开关,off 则跳过全部) |
整个流程包裹在 pcall 中,任何意外错误都被捕获并记录到 ngx.log(ngx.ERR),不会导致 500 错误返回给客户端。
核心特性详解
1. 基于域名的差异化规则
这是 NginxGuard 最实用的功能之一。通过 domain.json 配置文件,可以为不同域名设置不同的防护策略:
1 | { |
配置优先级:location 级 > 域名级 > 全局级
config.lua是全局基线,所有未在domain.json中配置的域名都走全局配置domain.json只放需要覆盖的域名,未指定的字段自动回退到全局- 支持通配符域名(
*.test.com),精确匹配优先于通配符
规则文件回退机制也设计得很精巧:当域名配置了独立的 rule_dir 时,NginxGuard 会先在域名目录中查找规则文件。如果文件不存在则回退到全局目录;如果文件存在但为空,则不回退,等同于该域名无此项规则。这意味着空文件 = 明确指定”无规则”,而非”使用全局规则”。
2. IP 规则与 CIDR 支持
IP 规则文件(blackip.rule / whiteip.rule / cdnip.rule)全面支持 IPv4/IPv6、CIDR、通配符和精确 IP:
1 | # blackip.rule 示例 |
性能方面做了大量优化:
| 规模 | 匹配方式 | 查找复杂度 |
|---|---|---|
| 1195 条 IPv4 CIDR | 排序数组 + 二分查找 | O(log n) ≈ 11 次比较 |
| IPv6 CIDR | 4×32-bit chunk mask 匹配 | O(n),通常 <20 条 |
| 通配符 | 预编译 regex | O(n),通常 <10 条 |
| 精确 IP | 小写化字符串比较 | O(n),通常 <100 条 |
CIDR 规则在文件加载时预编译为数值区间,查找性能极高。所有 IP 规则缓存 10 秒 + mtime 检查,修改规则文件后无需 reload nginx。
3. CDN 场景下的 IP 信任链
当 WAF 部署在 CDN 后面时,需要从 X-Forwarded-For 获取真实客户端 IP,但直接信任 XFF 会让攻击者伪造该头绕过 IP 黑白名单。
NginxGuard 的解决方案是 cdnip.rule:
1 | -- config.lua |
1 | # rule-config/cdnip.rule |
| 条件 | XFF 处理 | 说明 |
|---|---|---|
remote_addr 在 cdnip.rule 中 |
信任 XFF | 提取真实客户端 IP |
remote_addr 不在 cdnip.rule 中 |
不信任 XFF | 使用 remote_addr(防直连伪造) |
cdnip.rule 不存在 |
信任所有 XFF | 向后兼容 |
IP 提取优先级:CF-Connecting-IP > X-Real-IP > X-Forwarded-For > remote_addr。
4. CC 攻击防护与自动拉黑
CC 防护基于 ngx.shared.dict 实现滑动窗口限速:
1 | -- 每个IP每60秒最多150次请求 |
超限后 IP 自动加入 badGuys 共享字典,600 秒内所有请求直接返回 403,到期后自动解封。这个封禁是跨 Worker 生效的(基于共享字典),不需要外部存储。
cc_rate 参数解析结果也做了 worker 级缓存,避免每次请求都解析字符串。如果配置格式错误(如 "abc"),CC 检测会静默 fail open,同时在 error.log 中记录错误日志。
5. 云凭据探测拦截
url.rule 中内置了针对云服务凭据和敏感配置文件的探测拦截规则,覆盖攻击者常用的扫描路径:
| 规则 | 拦截目标 |
|---|---|
/firebase.*\.json |
Firebase 配置文件 |
/gcp-key\.json |
GCP 服务账号密钥 |
/\.aws/credentials |
AWS 凭据文件 |
/\.gcloud/ |
GCP 凭据目录 |
/\.ssh/id_rsa |
SSH 私钥 |
/terraform\.tfstate |
Terraform 状态文件 |
/.env |
环境变量配置 |
/.git/config |
Git 仓库配置 |
/actuator/env |
Spring Boot Actuator |
/swagger-ui |
API 文档 |
/druid/ |
Druid 监控面板 |
/h2-console |
H2 数据库控制台 |
还覆盖了版本控制目录(.git/、.svn/)、包管理文件(package.json、composer.json、Pipfile)、管理后台路径(wp-admin/、phpmyadmin)、Webshell 探测(shell.php、eval.php)等数百条规则。
6. 多层解码与绕过防护
攻击者经常通过编码绕过 WAF,NginxGuard 的 full_decode 函数实现了递归解码:
1 | -- 第一步:递归 URL 解码(最多8层) |
关键优化是 快速特征检测(has_encode_markers):只有当输入中包含 %、\u、\x、&# 等编码标记时才触发解码,正常流量零额外开销。
所有检测项(URL、参数、Cookie、POST)都采用”先匹配原始输入 → 未命中且有编码标记 → 解码后再匹配”的双重检测策略。
性能优化
NginxGuard 在性能方面做了大量工程优化,以下是关键措施:
规则引擎优化
双引擎匹配是核心优化之一。规则文件加载时,自动将纯字符串规则和正则规则分离:
- 纯字符串规则:用
string.find(plain 模式)匹配,比正则快约 10 倍 - 正则规则:合并为
(?:rule1|rule2|rule3|...)单一正则,一次匹配代替 N 次循环
1 | -- 规则加载时自动分流 |
正常流量匹配从 O(N) 降至 O(1),对 300 条规则的 url.rule,原来需要 300 次正则匹配,现在只需 1 次合并正则 + 少量字符串查找。
缓存层次
| 层级 | 存储位置 | 缓存内容 | 失效策略 |
|---|---|---|---|
| Worker 级 | Lua 模块变量 | 规则文件 + 合并正则 + 预编译 glob | TTL 10s + mtime 检查 |
| Worker 级 | Lua 模块变量 | domain.json 解析结果 | TTL 10s + mtime 检查 |
| Worker 级 | Lua 模块变量 | cc_rate 解析结果 | 配置变更时重解析 |
| 请求级 | ngx.ctx |
client_ip / domain / config / headers | 每请求自动清除 |
FFI stat 获取文件修改时间
规则热加载的关键是检测文件 mtime 变化。NginxGuard 使用 LuaJIT FFI 直接调用 libc 的 stat() 函数,无需编译任何 C 模块:
1 | ffi.cdef[[ |
支持现代 glibc(stat())和旧版 glibc(__xstat()),兼容 x86_64 和 aarch64。当 FFI 不可用时回退为文件大小 + 60 秒 TTL。
请求级短路优化
- GET/HEAD/OPTIONS 跳过 body 检测:
bodyless=on时跳过 POST 和文件上传检测 - 无参数 URL 短路:
next(REQ_ARGS) == nil时直接返回 - 最小输入长度检查:
#input < 2直接返回,避免对短参数做正则 - UA bloom-filter 预检查:先检查 7 个 bot 标记词,99% 正常流量跳过 50 次
string.find - 内网 IP 快速短路:127.x/10.x/172.x/192.x 跳过完整 CIDR 匹配
- Cookie 检测后移:Cookie 检测移到 URL/Args 之后,攻击请求在 URL 阶段即短路返回
配置一次加载
所有 20 个配置项在请求入口一次性加载到 ngx.ctx._cfg,避免每检测项重复调用 get_effective_config:
1 | local CONFIG_KEYS = { |
规则匹配缓存
对小体积请求体/参数,按 规则文件 + mtime + flags + input 做 worker 级缓存,重复请求直接复用匹配结果:
1 | if entry.cache_key and #input <= MATCH_CACHE_INPUT_MAX then |
压测数据
测试环境
| 项目 | 配置 |
|---|---|
| CPU | 4 核 x86_64 |
| 内存 | 24 GB |
| OS | Linux 6.18 (RHEL 9) |
| Nginx | 1.31.3 + LuaJIT2 (OpenResty 分支) |
| 测试工具 | ApacheBench (ab),200 并发 50000 请求 |
GET 请求
| 场景 | req/s | CPU | RSS | P99 | 吞吐下降 |
|---|---|---|---|---|---|
| 基线(WAF 全关) | 27,516 | 273% | 174 MB | 43ms | — |
| + WAF(无 CC/POST) | 22,912 | 304% | 179 MB | 34ms | 16.7% |
| + WAF + CC | 23,287 | 304% | 179 MB | 30ms | 15.4% |
| + WAF 全开(生产) | 21,240 | 268% | 179 MB | 35ms | 22.8% |
POST 请求
| 场景 | req/s | CPU | RSS | P99 | 吞吐下降 |
|---|---|---|---|---|---|
| form POST 基线 | 31,243 | 187% | 174 MB | 27ms | — |
| form POST + WAF | 21,634 | 286% | 183 MB | 29ms | 30.7% |
| JSON POST 基线 | 32,188 | 192% | 174 MB | 39ms | — |
| JSON POST + WAF | 20,547 | 292% | 182 MB | 66ms | 36.2% |
WAF 关闭时内存 174MB,全开后 179-183MB,仅增加 5-9MB。所有场景 P99 延迟控制在 66ms 以内。
日志系统
同步写入
攻击日志采用同步写入策略,确保在 ngx.exit(403) 前已落盘:
1 | local file = io.open(LOG_NAME, "a") |
只有检测到攻击时才触发日志写入,正常流量零日志开销。
JSON 格式
每条日志为一行 JSON:
1 | { |
截断保护与日志轮转
单字段最大 4096 字节,超出追加 ...[truncated]。日志文件超过 100MB 自动轮转为 *.old,轮转检查每 60 秒执行一次,不影响写入性能。
测试覆盖
项目包含完整的测试体系,16 个测试脚本覆盖各种攻击场景:
| 测试脚本 | 覆盖范围 |
|---|---|
waf_full_test.sh |
全量规则测试:URL/参数/UA/Cookie/POST/文件上传/IP/XFF |
waf_regression_test.sh |
回归测试:bodyless/无效cc_rate/日志截断/PUT方法/大body |
waf_scenario_test.sh |
场景测试:多域名/白名单/动态拉黑 |
waf_coverage_test.sh |
覆盖率测试 |
test_all_rules.sh |
逐条规则验证 |
benchmark.sh |
性能基准测试 |
测试覆盖的攻击类型包括:SQL 注入、XSS、路径遍历、命令注入、NoSQL 注入、SSTI 模板注入、PHP 代码注入、Log4j、SSRF、XXE、Webshell 探测等。
快速开始
安装依赖
1 | # 安装 LuaJIT2 |
配置 Nginx
1 | http { |
修改配置
编辑 config.lua 中的日志路径和规则目录:
1 | config_log_dir = "/apps/nginx/log/" |
Location 级别开关
如果某些路径需要关闭 WAF(如 gRPC 长连接、WebSocket),只需一行:
1 | location /ws { |
规则文件示例
URL 参数规则(args.rule / post.rule / cookie.rule)
三份文件内容相同,覆盖 OWASP Top 10 攻击向量:
1 | # SQL 注入 |
User-Agent 规则(useragent.rule)
拦截常见安全扫描工具:
1 | (sqlmap|nikto|nmap|dirb|gobuster|ffuf|nuclei|httpx|feroxbuster|masscan|wpscan) |
白名单 UA(whiteua.rule)
搜索引擎爬虫放行(仅跳过 UA 黑名单检测,其他安全检测仍执行):
1 | Googlebot |
安全设计:白名单 UA 仅跳过
useragent.rule检测,不会跳过 URL/POST/CC/Cookie 等其他检测,防止攻击者伪造搜索引擎 UA 绕过全部防护。
设计思考
为什么不使用 OpenResty?
NginxGuard 只依赖 lua-nginx-module + LuaJIT + lua-cjson,不需要完整的 OpenResty 发行版。对于已经在用原版 Nginx 的团队,只需加两个动态模块即可获得 WAF 能力,迁移成本极低。
为什么用同步日志?
WAF 日志的核心需求是”不丢”。如果用异步批量写入,在 ngx.exit(403) 后请求立即终止,尚未刷盘的日志可能丢失。同步写入 + flush() 确保每条攻击日志在返回 403 前已落盘。由于只在攻击时触发,正常流量不受影响。
为什么规则文件用纯文本而非 JSON/YAML?
纯文本规则文件的优势在于:
- 每行一条正则,极致简洁
- 支持
#注释,方便分组和说明 vim/sed直接编辑,无需格式化工具- 文件修改后通过 mtime 检查自动热加载,无需重启
fail open 设计
整个检测流程包裹在 pcall 中。即使某条规则有语法错误、某个函数抛出异常,请求也不会返回 500,而是被捕获并记录到 error.log。在安全性和可用性之间,NginxGuard 选择了可用性优先(fail open),避免 WAF 自身故障导致全站不可用。
总结
NginxGuard 是一个”够用就好”的 WAF:
- 轻量:4 个 Lua 文件,核心代码不到 1000 行,不引入额外进程
- 高性能:GET 吞吐下降约 16%,内存仅增加 5-9MB
- 多域名:基于
domain.json的差异化配置,支持通配符和规则回退 - 热加载:修改规则文件后 10 秒内自动生效,无需 reload nginx
- 全面防护:覆盖 SQLi/XSS/SSTI/SSRF/Log4j/NoSQL/命令注入/Webshell 探测等
- IPv6 友好:全链路支持 IPv4/IPv6/CIDR/通配符
- CDN 适配:通过
cdnip.rule构建 XFF 信任链,防止 IP 伪造
如果你的团队已经在用 Nginx,需要一个可控、可审计、不拖慢业务的 WAF 方案,NginxGuard 值得一试。