简介:这份PPT面向Web安全工程师、运维人员及安全架构学习者,系统梳理WAF从产品形态到架构落地的完整知识脉络,帮助读者理解如何为Web应用构建有效的攻击防护体系。内容围绕硬件盒子、软件WAF与云WAF三类产品展开,深入讲解云WAF的流量接入方式(DNS调度、CNAME、Anycast)、HTTP(S)反向代理选型(Nginx与OpenResty对比)以及安全规则引擎的取舍,并对ModSecurity CRS与Naxsi的规则机制、动作配置与参数组做了细致拆解,兼顾原理认知与方案选型参考。资源包共1个pptx文件,约3.93MB,页面结构按产品形态、实施要点、规则引擎、配置实践逐层推进,便于按主题检索与课堂讲解。目前已有314人学习,适合需要快速建立WAF架构全局认知或准备技术分享的读者使用。
1. 从一份 PPT 说起:WAF 到底在防什么,谁该看
上周有个做后端的朋友问我,他们业务刚上线就被扫出一堆 SQL 注入告警,老板让他“加个 WAF”,他第一反应是去搜“WAF 怎么装”。这其实是很多人的真实处境:知道 WAF 这个词,但说不清它拦的是哪一层、部署在哪、规则怎么落地。这份《WAF 的实现与架构演进》PPT 正好卡在这个缺口上——它不讲空泛的安全理念,而是把 WAF 的产品形态、流量接入方式、反向代理选型、规则引擎配置一条线串下来,最后落到 OpenResty 上跑一套可配置的规则。适合两类人:一是要给自己业务加一层七层防护的后端或运维,二是想搞明白云 WAF 背后那套反向代理加规则引擎怎么搭起来的安全初学者。下面我按“先立架构、再动手配规则、最后说坑”的顺序拆一遍,能直接抄的部分我都给到参数和代码。
2. 三种产品形态与流量接入:先想清楚请求怎么走到你面前
WAF 的第一道分水岭不是规则写得多好,而是流量怎么接进来。PPT 把产品形态分成硬件、软件、云 WAF 三类,这个分类背后其实是三种完全不同的流量路径,选型时先对号入座,后面反向代理和规则引擎才有意义。
2.1 硬件、软件、云 WAF 的部署位置差异
硬件 WAF 是一台独立设备,串行或旁路挂在本地网络出口,典型场景是机房里有几十台 Web 服务器、需要统一出口防护。串行部署意味着流量必须穿过它,好处是阻断彻底,坏处是它挂了业务就断,所以实际项目里常见旁路加引流,或者串行但做双机热备。软件 WAF 直接装在 Web 服务器上,以模块或 agent 形式存在,部署成本低,但和业务进程抢资源,规则误杀时排查也更麻烦。云 WAF 本质是反向代理,用户浏览器先连到 WAF 节点,节点再回源到真实服务器,它天然是“中间人”,所以流量接入方式成了它的核心设计点。
选型时我一般看三个数:QPS 峰值、是否有多机房、能不能接受改 DNS。QPS 上万且多机房,云 WAF 的反向代理模型更省事;单机房、延迟敏感、不想把证书交给第三方,软件 WAF 或自建 OpenResty 更可控。硬件盒子现在更多出现在等保合规场景,纯技术选型时它的性价比要打问号。
2.2 DNS 调度与 Anycast:两种把流量引到 WAF 的路子
云 WAF 接入最常用的是 DNS 方式,用户把域名 CNAME 到 WAF 厂商给的地址,解析结果指向 WAF 边缘节点。PPT 里提到的 DNS 调度,指的是按来源 IP、线路、机房负载做解析分流,比如北京用户解析到北京节点、联通线路走联通机房。这种做法实现简单,但生效依赖 DNS 缓存,切换节点时 TTL 没调好会出现一段时间的流量错位,这是血泪经验里最常见的一条。
Anycast 则是网络层寻址,同一个 IP 在多个节点宣告,路由协议把用户带到“最近”的节点。它的优点是调度在路由层完成,单点故障后流量自动切走,不依赖 DNS 解析性能;缺点是成本高,需要整段 IP,BGP 路由优化也复杂。PPT 里点到的代表厂商这里不展开,记住结论就行:预算充足、全球多节点、要求故障自愈快,Anycast 更合适;中小规模、想快速上线,DNS 加 CNAME 足够。
提示:CNAME 接入后一定要确认回源地址和证书链,很多“WAF 上了但 HTTPS 报错”的问题出在证书没同步到边缘节点,而不是规则。
3. 反向代理选型:Nginx 还是 OpenResty,差别在扩展点
流量接进来之后,WAF 节点本身要做一个七层反向代理。PPT 对比了 Nginx 和另一类代理方案,结论偏向 Nginx 系,理由是网络模型效率高、模块化、支持 Lua 扩展。这一节把“为什么选 OpenResty”和“扩展点在哪”讲透,后面配规则才有落脚处。
3.1 为什么是 OpenResty:Nginx 加 Lua 的执行阶段
原生 Nginx 做 WAF 的短板是规则逻辑要写进 C 模块,改一条规则就得重新编译。OpenResty 把 Lua 嵌进 Nginx 的各个执行阶段,规则可以用脚本写、热加载,这才是它能承载 WAF 规则引擎的前提。PPT 列出的阶段对应关系很关键,我按请求生命周期排一下:
| 执行阶段 | 典型用途 |
|---|---|
| ssl_certificate_by_lua | SSL 证书处理,动态选证书 |
| set_by_lua | 变量初始化 |
| rewrite_by_lua | 转发、重定向、缓存 |
| access_by_lua | 请求头与参数检查,WAF 主战场 |
| content_by_lua | 内容生成 |
| header_filter_by_lua | 响应头过滤 |
| body_filter_by_lua | 响应体过滤,防敏感信息泄露 |
| log_by_lua | 异步日志记录 |
WAF 的请求检测基本都放在 access_by_lua,因为此时请求头和参数已经解析完、还没转发到后端,拦在这里成本最低。响应侧的马脚检测放 body_filter_by_lua,但要注意这个阶段数据是分块的,跨块匹配需要自己做缓冲,这是后面避坑章节要展开的点。
3.2 用 OpenResty 搭一个最小可用的检测入口
先给一个能跑起来的最小配置,把 access 阶段的检测入口留出来,规则逻辑后面再填。这段配置的作用是:所有请求进 access 阶段先过一遍 Lua 检测函数,命中就返回 403,不命中放行到后端。
# nginx.conf 片段 http { lua_shared_dict waf_cache 10m; # 规则缓存与计数共享内存 init_by_lua_block { -- 启动时加载规则文件,避免每请求读盘 local waf = require "waf.engine" waf.load_rules("/etc/openresty/waf/rules.json") } server { listen 443 ssl; server_name example.internal; ssl_certificate /etc/openresty/cert/fullchain.pem; ssl_certificate_key /etc/openresty/cert/privkey.pem; location / { access_by_lua_block { local waf = require "waf.engine" local ok, err = waf.check() if not ok then ngx.log(ngx.WARN, "waf block: ", err) return ngx.exit(403) -- 命中规则直接阻断 end } proxy_pass http://backend_pool; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } }逻辑说明:init_by_lua_block在 master 进程启动时执行一次,把规则从 JSON 读进内存,lua_shared_dict给多 worker 共享计数和缓存用,CC 防护的频次统计就靠它。access_by_lua_block里waf.check()返回 false 时直接ngx.exit(403),这是阻断动作最直接的写法。参数上,waf_cache的大小按规则条数和并发量估,规则几千条、QPS 几千,10m 起步够用;proxy_pass指向后端池,回源地址别写死单机,否则 WAF 节点本身成了单点。
3.3 规则引擎的两种思路:正则匹配与语义分析
PPT 把规则引擎分成两派:ModSecurity 的 CRS 核心规则集走正则验证,覆盖 OWASP Top10 常见威胁,规则全但误报要靠调;Naxsi 不依赖规则库,靠学习正常流量生成白名单,对未知威胁有优势但初期学习成本高。实际落地我一般用前者打底、后者补位:CRS 负责已知攻击特征,语义分析库负责 SQL 注入和 XSS 的变形识别,PPT 里提到的 libinjection 和 libhtmlentities 就是干这个的,前者做 SQL 词法分析,后者做 HTML 实体解码,比纯正则抗混淆。
选型判断很简单:业务参数结构稳定、误报容忍度低,先上 CRS 再逐条关规则;业务形态多变、攻击面大,加一层语义检测兜底。两者都不是“开了就安全”,规则要跟着业务参数调,这是后面避坑章节反复要说的。
4. 规则怎么写:动作、变量、操作符与转换函数
规则引擎选好之后,真正决定拦截效果的是规则本身。PPT 给了一套配置结构,包含动作、匹配条件、变量组、解析方式、转换函数和操作函数。这一节把它拆成能直接抄的写法,重点讲清楚每个字段改哪里、为什么这么改。
4.1 一条规则的完整结构:从动作到匹配条件
一条规则由动作和若干匹配条件组成,多个条件之间是“与”的关系,单个条件里的多个变量是“或”的关系。动作决定命中后干什么:PASS 跳过当前 phase 的规则,DENY 阻断请求,ALLOW 跳过所有检查,REDIRECT 重定向,RESET_CONNECTION 重置连接。生产环境里 DENY 用得最多,ALLOW 要慎用,一条写错的 ALLOW 可能把整个检测链短路。
匹配条件的结构如下,这是 PPT 里给出的字段,我补上注释说明每个字段的实际含义:
{ match = { { vars = { -- 数组,多个变量之间是“或” { var = "ARGS", -- 变量名,如 ARGS、REQUEST_URI parse = { specific = "id" -- 只取参数 id,支持数组 } } }, transform = "uri_decode", -- 转换函数,可多个,按顺序执行 operator = "detect_sqli", -- 操作函数 pattern = "", -- 操作参数,detect_sqli 不需要 op_negated = false -- 是否取反,默认 false } } }逻辑说明:vars里var指定从哪个参数组取值,parse决定取这个组的哪部分,specific取指定 key、ignore排除 key、keys取所有 key、values取所有 value、all全取,这五种只能选一种。transform在匹配前对值做预处理,比如先uri_decode再检测,能防住 URL 编码绕过。operator是真正的判断函数,pattern是它的入参。op_negated为 true 时结果取反,适合写“不匹配某特征就放行”的白名单逻辑。
4.2 变量组与解析方式:取错变量等于没防
变量组决定规则看哪里。PPT 列出的常用变量包括 REQUEST_METHOD、URL、REQUEST_URI、ARGS、HTTP_USER_AGENT、HTTP_REFERER、REMOTE_ADDR、REQUEST_HEADERS、RESPONSE_HEADERS、TIME_WDAY 等。这里有个容易翻车的点:ARGS 只覆盖查询串和表单体,JSON body 里的字段不一定在里面,如果业务是 JSON 接口,规则要额外解析 body,否则攻击者把 payload 塞进 JSON 就绕过了。
解析方式的选择直接影响命中率。举个例子,检测参数id的 SQL 注入,specific = "id"最精准;检测所有参数里的 XSS,用values = true遍历所有值;检测请求头里的恶意 UA,变量换成 HTTP_USER_AGENT 且不需要 parse。我一般按“先精准后放宽”的顺序调:先用 specific 定位高频攻击参数,误报可控后再扩到 values,一上来就 all 会把正常业务参数也扫一遍,误报量很难压。
4.3 操作符与转换函数的组合套路
操作符是规则的判断核心,PPT 列了一长串,常用的几类我按场景归一下:字符串类有 begins_with、contains、contains_word、ends_with、equal、str_match;数值类有 greater、less、greater_eq、less_eq、num_range;安全语义类有 detect_sqli、detect_xss、validate_url_encoding;网络类有 ip_utils,支持单 IP、子网掩码和 IP 段。正则 regex 最灵活但也最慢,高频路径上能不用就不用。
转换函数负责在判断前“洗数据”,PPT 里的 base64_decode、hex_decode、html_decode、sql_hex_decode、remove_comments、remove_whitespace、lowercase、normalise_path 都是抗绕过的关键。典型组合是:先remove_comments去掉 SQL 注释符,再lowercase统一大小写,最后detect_sqli检测。少一步就可能被/*!50000select*/这类写法绕过。下面给一条检测参数注入的规则示例:
{ action = "DENY", match = { { vars = { { var = "ARGS", parse = { values = true } } }, transform = { "remove_comments", "lowercase", "uri_decode" }, operator = "detect_sqli", pattern = "" }, { vars = { { var = "REQUEST_URI" } }, transform = { "uri_decode" }, operator = "contains", pattern = "../" -- 路径穿越特征 } } }逻辑说明:这条规则有两个条件,第一个扫所有参数值的 SQL 注入,第二个扫 URI 里的路径穿越,任一条件命中即 DENY。transform写成数组表示按顺序执行,先去掉注释再转小写再解码,顺序不能乱——先解码再去注释,攻击者用编码藏起来的注释符就漏了。pattern对 detect_sqli 为空,因为语义检测不需要特征串;对 contains 则必须给具体字符串。参数上,values = true会遍历所有参数,QPS 高的接口要评估性能,必要时收窄到 specific。
5. 避坑与排查:规则上线后最容易翻车的五件事
规则配完不等于能用,上线后真正花时间的是误报和绕过。这一节按“现象 → 原因 → 解决”记五条我踩过的坑,都是能直接对号入座的。
5.1 业务接口大面积 403,日志里全是同一条规则
现象:规则上线后某个下单接口 403 率飙升,日志指向同一条 detect_sqli 规则。原因:下单备注字段允许用户输入自由文本,里面出现select、order by这类词被语义检测命中。解决:对备注类字段单独放行,用parse = { ignore = "remark" }排除,或者给该字段加白名单规则放在 DENY 之前。更稳的做法是先把新规则设成只记录不阻断,观察一周误报再切 DENY。
5.2 JSON body 里的攻击没被拦住
现象:安全扫描报告 JSON 接口存在注入,但 WAF 日志无记录。原因:规则只看了 ARGS,JSON body 没解析,攻击 payload 在 body 里完全没进检测范围。解决:在 access 阶段先读 body 并解析成表,把字段并入检测变量;注意ngx.req.read_body()要在get_body_data()之前调用,且大 body 要设上限,否则内存会被打爆。
5.3 响应体检测漏报,敏感信息照样泄露
现象:配置了 body_filter_by_lua 检测响应里的手机号,但部分响应没拦住。原因:响应体是分块到达的,单块内匹配不到跨块的敏感串。解决:在 body_filter 里做缓冲,按 chunk 拼接后再匹配,同时设最大缓冲长度防止大文件撑爆内存;或者把敏感信息检测前移到后端出口,WAF 只做请求侧。
5.4 规则热加载后旧规则还在生效
现象:改了规则文件,reload 后行为没变。原因:规则在 init_by_lua 里加载进共享内存,reload 只重启 worker,master 的 init 不一定重跑,旧缓存还在。解决:规则加载做成带版本号的接口,reload 时比对版本号强制刷新共享字典;或者用单独的 admin 接口触发waf.load_rules,别依赖 nginx -s reload。
5.5 CC 防护把正常用户也限了
现象:促销活动期间大量正常用户被 503,日志显示触发频次限制。原因:CC 规则按 IP 计数,办公网和运营商 NAT 出口下大量用户共享 IP,阈值按单 IP 设得太低。解决:计数维度加上 UA、Cookie 或路径做组合,阈值按业务峰值调;对静态资源路径单独放行,别让图片请求也计入频次。
注意:任何新规则先跑观察模式,日志里记命中详情但不阻断,确认误报率可接受再切 DENY,这一步能省掉大半线上事故。
6. 进阶:把规则验证做成可回归的流程
规则写到一定数量后,靠人工点页面验证已经不现实,我的习惯是给规则建一套回归用例,每次改规则先跑一遍。做法不复杂:准备一批正常请求和攻击请求的样本,用 OpenResty 的测试框架或者直接起一个本地实例,把样本灌进去,断言正常请求放行、攻击请求阻断。下面是一个用 resty 命令行跑单条规则验证的写法:
# 用 resty 直接加载规则引擎,喂样本断言结果 resty -I /etc/openresty/lua \ -e 'local waf = require "waf.engine" waf.load_rules("/etc/openresty/waf/rules.json") local cases = { { uri = "/api/item?id=1", expect = true }, { uri = "/api/item?id=1%27%20or%20%271%27%3D%271", expect = false }, { uri = "/api/item?name=%3Cscript%3Ealert(1)%3C/script%3E", expect = false } } for _, c in ipairs(cases) do local ok = waf.check_uri(c.uri) assert(ok == c.expect, "case failed: " .. c.uri) end print("all cases passed")'逻辑说明:resty是 OpenResty 自带的命令行工具,能脱离 Nginx 直接跑 Lua 代码,适合做规则单测。cases里每条样本带期望结果,expect = true表示应放行,false 表示应阻断,assert不通过就报出具体 URI。参数上,-I指定 Lua 模块搜索路径,规则文件路径和线上保持一致,避免测试和线上加载的不是同一份。这套用例积累到几十条后,改规则前跑一遍,能挡住大部分“改 A 坏 B”的回归问题。
再补一个性能观察的习惯:规则数量上去后,access 阶段的耗时要用ngx.now()打点,把每条规则的匹配耗时记进日志,定期看哪条规则最慢。正则类规则和 values 遍历是性能大户,慢规则要么收窄变量范围,要么前移到更便宜的 contains 预筛。我一般要求单请求 WAF 检测耗时控制在 1ms 以内,超了就说明规则该优化了。
从那以后我每次改规则都强制走一遍“观察模式 → 回归用例 → 灰度切 DENY”这三步,再急也不跳。规则是活的,业务在变,攻击手法也在变,没有一劳永逸的配置,只有持续验证的习惯。希望帮到你。
本文还有配套的精品资源,点击获取