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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
waf/
├── config.lua # 全局配置(开关、日志路径、规则目录等)
├── init.lua # init_by_lua_file 预加载模块
├── access.lua # access_by_lua_file 每请求检测入口
├── lib.lua # 核心库(IP获取、规则加载、域名配置、日志、输出)
├── nginx-config/
│ └── nginx.conf # nginx 配置示例
└── rule-config/
├── domain.json # 域名级规则配置
├── args.rule # URL 参数规则
├── blackip.rule # 黑名单 IP
├── cdnip.rule # CDN/可信代理 IP 列表
├── cookie.rule # Cookie 规则
├── post.rule # POST 规则
├── url.rule # URL 规则
├── useragent.rule # User-Agent 规则
├── whiteip.rule # 白名单 IP
├── whiteua.rule # 白名单 UA(搜索引擎爬虫)
├── whiteurl.rule # 白名单 URL
├── referer.rule # Referer 规则
├── fileext.rule # 文件上传扩展名规则
└── domains/ # 域名专属规则目录
└── www.example.com/
└── ...(同构的规则文件)

工作原理

NginxGuard 挂载在 Nginx 的 access_by_lua_file 阶段,每个请求进入时依次执行安全检测:

1
2
3
# nginx.conf 关键配置
init_by_lua_file "/apps/nginx/conf/waf/init.lua";
access_by_lua_file "/apps/nginx/conf/waf/access.lua";

init.lua 在 Nginx 启动时预加载模块;access.lua 在每个请求的 access 阶段执行检测逻辑;lib.lua 是核心库,提供 IP 匹配、规则加载、日志记录等能力。

检测流程

每个请求按以下顺序依次检测,命中任一项即拦截并返回 403,后续检测不再执行:

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
1. $waf_enable 检查(location 级开关,off 则跳过全部)
↓
2. waf_enable 检查(域名级/全局级开关)
↓
3. 白名单 IP 放行
↓
4. 动态黑名单(CC 自动拉黑期内)
↓
5. 静态黑名单 IP
↓
6. 白名单 URL 匹配(返回 per-URL 跳过表)
↓
7. User-Agent 攻击检测(白名单 UA 跳过此项)
↓
8. Referer 攻击检测
↓
9. CC 限速
↓
10. 文件上传扩展名检测
↓
11. URL 路径攻击检测
↓
12. URL 参数攻击检测
↓
13. Cookie 攻击检测
↓
14. POST 攻击检测(表单 + JSON body)
↓
放行

整个流程包裹在 pcall 中,任何意外错误都被捕获并记录到 ngx.log(ngx.ERR),不会导致 500 错误返回给客户端。

白名单 URL 新机制:white_url_check() 不再简单地全量放行,而是返回一个跳过表(如 {url_attack=true} 或 {user_agent=true, referer=true}),后续各检测项根据跳过表决定是否执行。纯路径格式默认只跳过 url_attack,扩展格式可指定跳过任意组合的检测项。

核心特性详解

1. 基于域名的差异化规则

这是 NginxGuard 最实用的功能之一。通过 domain.json 配置文件,可以为不同域名设置不同的防护策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"www.example.com": {
"url_check": "off",
"cc_rate": "100/60",
"cc_block_ttl": 300,
"rule_dir": "domains/www.example.com"
},
"api.example.com": {
"waf_enable": "off"
},
"limit.example.com": {
"multipart_streaming_check": "on",
"post_body_scan_limit": 1048576,
"upload_filename_scan_limit": 1024
},
"strict.example.com": {
"bodyless": "off"
},
"*.test.com": {
"post_check": "off",
"cookie_check": "off"
}
}

配置优先级:location 级 > 域名级 > 全局级

  • config.lua 是全局基线,所有未在 domain.json 中配置的域名都走全局配置
  • domain.json 只放需要覆盖的域名,未指定的字段自动回退到全局
  • 支持通配符域名(*.test.com),精确匹配优先于通配符

完整配置参数参考:

config.lua 中的全局配置项(对应 domain.json 中的键名):

domain.json 键名 config.lua 变量 默认值 说明
waf_enable config_waf_enable "on" WAF 总开关,off 跳过全部检测
trust_proxy_headers config_trust_proxy_headers "on" 是否信任代理转发的 IP 头(CDN 场景)
white_ip_check config_white_ip_check "on" 白名单 IP 检测
black_ip_check config_black_ip_check "on" 黑名单 IP 检测
white_url_check config_white_url_check "on" 白名单 URL 检测
white_ua_check config_white_ua_check "on" 白名单 UA 检测(搜索引擎爬虫)
user_agent_check config_user_agent_check "on" User-Agent 黑名单检测
url_check config_url_check "on" URL 路径攻击检测
url_args_check config_url_args_check "on" URL 参数攻击检测
cookie_check config_cookie_check "on" Cookie 攻击检测
cc_check config_cc_check "on" CC 限速检测
cc_rate config_cc_rate "150/60" CC 限速参数(请求次数/时间窗口秒)
cc_block_ttl config_cc_block_ttl 600 CC 触发后自动拉黑秒数,0=禁用自动拉黑
post_check config_post_check "on" POST body 攻击检测
referer_check config_referer_check "off" Referer 检测
file_upload_check config_file_upload_check "on" 文件上传扩展名检测
bodyless config_bodyless "on" GET/HEAD/OPTIONS 跳过 body 检测,off=全方法扫描
multipart_streaming_check config_multipart_streaming_check "off" multipart 临时文件 body 流式扫描
upload_filename_scan_limit config_upload_filename_scan_limit 0 multipart 文件名扫描字节上限,0=全扫
post_body_scan_limit config_post_body_scan_limit 2097152 大 body 扫描字节上限,超出直接拒绝

domain.json 中的键名就是 config.lua 变量去掉 config_ 前缀。例如 config_url_check → url_check。此外 domain.json 还支持 rule_dir 键(指定域名专属规则目录),该键没有对应的 config.lua 变量。config_log_dir、config_rule_dir、config_waf_output、config_redirect_url、config_output_html 为全局路径/输出配置,不支持域名级覆盖。

规则文件回退机制也设计得很精巧:当域名配置了独立的 rule_dir 时,NginxGuard 会先在域名目录中查找规则文件。如果文件不存在则回退到全局目录;如果文件存在但为空,则不回退,等同于该域名无此项规则。这意味着空文件 = 明确指定”无规则”,而非”使用全局规则”。

2. IP 规则与 CIDR 支持

IP 规则文件(blackip.rule / whiteip.rule / cdnip.rule)全面支持 IPv4/IPv6、CIDR、通配符和精确 IP:

1
2
3
4
5
6
7
# blackip.rule 示例
8.8.8.8 # 精确 IP
10.0.0.0/8 # IPv4 CIDR
2001:db8::/32 # IPv6 CIDR
fe80::* # IPv6 通配符
::1 # IPv6 精确
# 这是注释行,自动跳过

性能方面做了大量优化:

规模 匹配方式 查找复杂度
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
2
-- config.lua
config_trust_proxy_headers = "on"
1
2
3
4
5
6
# rule-config/cdnip.rule
# Cloudflare IPv4
173.245.48.0/20
104.16.0.0/13
# 内部代理
10.0.0.0/8
条件 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
2
3
4
-- 每个IP每60秒最多150次请求
config_cc_rate = "150/60"
-- 超限后自动拉黑600秒
config_cc_block_ttl = 600

超限后 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. 白名单 URL 的精细化跳过

旧版白名单 URL 一旦匹配即跳过全部安全检测,这在实际场景中过于宽松——一个合法的旧系统路径可能只需跳过 URL 路径检测,但不应该同时跳过 SQL 注入、XSS 等参数检测。

NginxGuard 对 whiteurl.rule 进行了重构,支持两种格式:

1
2
3
4
5
6
7
8
9
10
11
12
# whiteurl.rule

# 纯路径格式(默认只跳过 url_attack 路径检测)
/static/
/api/public/

# 扩展格式(指定跳过的检测项,逗号分隔)
/legacy/ user_agent,referer,url_attack,url_args
/api/old/ post,cookie

# 扩展格式 + 正则路径($ 锚定末尾,精确匹配)
/ipinfo$ user_agent

纯路径格式只跳过 URL 路径检测(url.rule),参数检测、Cookie 检测、POST 检测等照常执行。扩展格式则可以精确指定跳过哪些检测项:

可跳过的检测项 说明
user_agent User-Agent 黑名单检测
referer Referer 检测
url_attack URL 路径攻击检测
url_args URL 参数攻击检测
cookie Cookie 攻击检测
post POST body 攻击检测
file_upload 文件上传扩展名检测
cc CC 限速检测

安全设计要点:

  • 双模式匹配:纯路径采用前缀匹配(string.sub),含正则元字符的路径用正则匹配(ngx.re.find),支持 /ipinfo$ 精确匹配、/api/v[0-9]+/ 版本号匹配等场景
  • 防绕过:前缀匹配而非子串匹配(string.find),防止 /123/ 匹配到 /x/123/etc/passwd 这类绕过攻击
  • 最长匹配优先:多条规则同时匹配时,选择路径最长的规则,实现精确控制
  • 默认最小权限:纯路径格式默认只跳过 url_attack,不跳过其他安全检测
1
2
3
4
5
6
7
8
-- waf_main() 中的跳过逻辑
if not (url_skips and url_skips.url_attack) then
if url_attack_check() then return end
end
if not (url_skips and url_skips.url_args) then
if url_args_attack_check() then return end
end
-- ...其他检测项同理

7. 多层解码与绕过防护

攻击者经常通过编码绕过 WAF,NginxGuard 的 full_decode 函数实现了递归解码:

1
2
3
-- 第一步:递归 URL 解码(最多8层)
-- 第二步:JS unicode/hex/entity 解码
-- \u003c → < \x3c → < &#60; → < &#x3c; → <

关键优化是 快速特征检测(has_encode_markers):只有当输入中包含 %、\u、\x、&# 等编码标记时才触发解码,正常流量零额外开销。

所有检测项(URL、参数、Cookie、POST)都采用”先匹配原始输入 → 未命中且有编码标记 → 解码后再匹配”的双重检测策略。

性能优化

NginxGuard 在性能方面做了大量工程优化,以下是关键措施:

规则引擎优化

双引擎匹配是核心优化之一。规则文件加载时,自动将纯字符串规则和正则规则分离:

  • 纯字符串规则:用 string.find(plain 模式)匹配,比正则快约 10 倍
  • 正则规则:合并为 (?:rule1|rule2|rule3|...) 单一正则,一次匹配代替 N 次循环
1
2
3
4
5
6
7
-- 规则加载时自动分流
if r:find("[()%.%[%]%*%+%?%^%$|\\]") then
table.insert(regex_rules, r) -- 正则规则
else
table.insert(fast_rules, r) -- 纯字符串规则
fast_hash[r] = true -- hash set for O(1) lookup
end

正常流量匹配从 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
2
3
4
5
6
7
8
ffi.cdef[[
struct waf_stat_t {
long long _pad_to_mtime[11];
long long st_mtime;
long long _rest[7];
};
int stat(const char *path, struct waf_stat_t *buf);
]]

支持现代 glibc(stat())和旧版 glibc(__xstat()),兼容 x86_64 和 aarch64。当 FFI 不可用时回退为文件大小 + 60 秒 TTL。

请求级短路优化

  • GET/HEAD/OPTIONS 跳过 body 检测:config_bodyless 默认为 on,GET/HEAD/OPTIONS 请求跳过 POST body 和文件上传检测以降低延迟;设为 off 时对所有方法扫描 body
  • 无参数 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
2
3
4
5
6
7
local CONFIG_KEYS = {
"waf_enable", "white_ip_check", "black_ip_check", ...
}
-- 请求入口一次性加载
for _, k in ipairs(CONFIG_KEYS) do
ctx._cfg[k] = get_effective_config(k)
end

规则匹配缓存

对小体积请求体/参数,按 规则文件 + mtime + flags + input 做 worker 级缓存,重复请求直接复用匹配结果:

1
2
3
4
5
if entry.cache_key and #input <= MATCH_CACHE_INPUT_MAX then
cache_key = entry.cache_key .. "|" .. (flags or "") .. "|" .. input
local cached = worker_match_cache[cache_key]
if cached ~= nil then return cached or nil end
end

压测数据

测试环境

项目 配置
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
2
3
4
local file = io.open(LOG_NAME, "a")
file:write(LOG_LINE .. "\n")
file:flush() -- 强制刷盘
file:close()

只有检测到攻击时才触发日志写入,正常流量零日志开销。

JSON 格式

每条日志为一行 JSON:

1
2
3
4
5
6
7
8
9
10
11
{
"@timestamp": "2026-08-21T08:30:00Z",
"client_ip": "1.2.3.4",
"local_time": "2026-08-21 16:30:00",
"server_name": "www.example.com",
"user_agent": "Mozilla/5.0...",
"attack_method": "Deny_URL_Args",
"req_url": "/?id=1+union+select+1",
"req_data": "-",
"rule_tag": "(?i:union).*select.*from"
}

截断保护与日志轮转

单字段最大 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
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
# 安装 LuaJIT2
git clone https://github.com/openresty/luajit2.git
cd luajit2 && make -j$(nproc) && make install
ln -sf /usr/local/lib/libluajit-5.1.so.2 /lib64/libluajit-5.1.so.2

# 安装 lua-cjson
git clone https://github.com/openresty/lua-cjson.git
cd lua-cjson
export LUAJIT_LIB=/usr/local/lib
export LUAJIT_INC=/usr/local/include/luajit-2.1
make -j$(nproc) && make install

# 编译 Nginx + lua-nginx-module
git clone --branch v0.10.31 https://github.com/openresty/lua-nginx-module.git
git clone https://github.com/simplresty/ngx_devel_kit.git
wget https://nginx.org/download/nginx-1.31.3.tar.gz
tar -xvf nginx-1.31.3.tar.gz && cd nginx-1.31.3

export LUAJIT_LIB=/usr/local/lib
export LUAJIT_INC=/usr/local/include/luajit-2.1

./configure \
--add-module=../ngx_devel_kit \
--add-module=../lua-nginx-module \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_realip_module \
--with-threads \
--with-file-aio
make -j$(nproc) && make install

配置 Nginx

1
2
3
4
5
6
7
8
9
10
http {
lua_package_path "/apps/nginx/conf/waf/?.lua";
lua_shared_dict limit 10m;
lua_shared_dict badGuys 10m;
lua_code_cache on;
lua_need_request_body off;

init_by_lua_file "/apps/nginx/conf/waf/init.lua";
access_by_lua_file "/apps/nginx/conf/waf/access.lua";
}

修改配置

编辑 config.lua 中的日志路径和规则目录:

1
2
config_log_dir = "/apps/nginx/log/"
config_rule_dir = "/apps/nginx/conf/waf/rule-config"

Location 级别开关

如果某些路径需要关闭 WAF(如 gRPC 长连接、WebSocket),只需一行:

1
2
3
4
location /ws {
set $waf_enable off;
proxy_pass http://backend;
}

规则文件示例

URL 参数规则(args.rule / post.rule / cookie.rule)

三份文件内容相同,覆盖 OWASP Top 10 攻击向量:

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
# SQL 注入
(?i:union)[\s/\*!0-9-]*(?:(?:all|distinct)[\s/\*!0-9-]*)?(?i:select)
(?i:select).*from.*information_schema
sleep\((\s*)(\d*)(\s*)\)
benchmark\([^,]*,\s*[^)]*\)
(?i:load_file\()

# XSS
\<(iframe|script|body|img|layer|div|meta|style|base|object|input|svg)
(onmouseover|onerror|onload)\=
javascript\:
document\.cookie

# 命令注入
(?:define|eval|shell_exec|system|passthru|exec)\(
(gopher|doc|php|glob|file|phar)\:\/

# SSTI 模板注入
\{\{[^}]{0,100}(__class__|__subclasses__|__init__|__globals__)\}\}

# Log4j
log4j|jndi:|\$\{.*jndi
\$\{.*lower
\$\{.*env

# NoSQL 注入
\$where[\(:"']
\$regex[\(:"']
\$gt[\(:"']

# SSRF
169\.254\.169\.254
metadata\.google\.internal

User-Agent 规则(useragent.rule)

拦截常见安全扫描工具:

1
2
3
(sqlmap|nikto|nmap|dirb|gobuster|ffuf|nuclei|httpx|feroxbuster|masscan|wpscan)
(Acunetix|Nessus|OpenVAS|BurpSuite|AppScan|Metasploit|CobaltStrike)
(Python-urllib|Python-requests|Go-http-client|scrapy)

白名单 UA(whiteua.rule)

搜索引擎爬虫放行(仅跳过 UA 黑名单检测,其他安全检测仍执行):

1
2
3
4
5
6
Googlebot
Baiduspider
bingbot
YandexBot
Applebot
...

安全设计:白名单 UA 仅跳过 useragent.rule 检测,不会跳过 URL/POST/CC/Cookie 等其他检测,防止攻击者伪造搜索引擎 UA 绕过全部防护。

白名单 URL 规则(whiteurl.rule)

支持纯路径和扩展两种格式,按需跳过特定检测项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 纯路径格式(默认只跳过 url_attack 路径检测)
/static/
/api/public/

# 扩展格式(指定跳过的检测项,逗号分隔)
/legacy/ user_agent,referer,url_attack,url_args
/api/old/ post,cookie

# 扩展格式 + 正则路径($ 锚定末尾,精确匹配)
/ipinfo$ user_agent

# 可用检测项: user_agent,referer,url_attack,url_args,cookie,post,file_upload,cc
# 路径匹配: 纯路径用前缀匹配(最长匹配优先),含正则元字符的路径用正则匹配
# 修改后自动热加载(10秒内生效,无需重启 nginx)

安全设计:纯路径格式默认只跳过 url_attack,不再像旧版那样跳过全部检测。扩展格式允许按需组合跳过项,实现最小权限原则。纯路径采用前缀匹配而非子串匹配,防止 /path/ 匹配到 /malicious/path/etc/passwd 这类绕过攻击。含正则元字符的路径(如 /ipinfo$)自动切换为正则匹配,支持精确匹配和模式匹配。

设计思考

为什么不使用 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
  • 精细化白名单:whiteurl.rule 支持纯路径(默认只跳过 URL 路径检测)和扩展格式(指定跳过 user_agent,referer,url_attack,url_args,cookie,post,file_upload,cc 任意组合),前缀匹配防绕过
  • 全面防护:覆盖 SQLi/XSS/SSTI/SSRF/Log4j/NoSQL/命令注入/Webshell 探测等
  • IPv6 友好:全链路支持 IPv4/IPv6/CIDR/通配符
  • CDN 适配:通过 cdnip.rule 构建 XFF 信任链,防止 IP 伪造

如果你的团队已经在用 Nginx,需要一个可控、可审计、不拖慢业务的 WAF 方案,NginxGuard 值得一试。


项目地址:GitHub - qist/nginxguard