ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Lucky网关集成Coraza WAF与OWASP CRS:规则实战与性能调优指南

Lucky网关集成Coraza WAF与OWASP CRS:规则实战与性能调优指南 1. 为什么要在自己的网关里养一套WAF规则集1.1 从“告警一堆”到“裸奔”的真实处境先说个真实经历。之前我们把服务挂在公网每天安全扫描的告警堆成山有扫路径的、有试登录的、有往上怼乱七八糟参数的。当时我们用的还只是网关自带的基础访问日志能把来源IP记下来就不错了至于请求里带的是什么payload根本无从判断。后来咬牙上了云上的商业WAF按域名和QPS计费一个月账单下来小团队确实肉疼。而且商业WAF对“请求里到底有没有攻击特征”这件事给不了太多透明度它拦了就拦了放行就放行了你想看具体的规则命中原因得去控制台翻半天。所以我们在评估Lucky这个自建网关项目时最看重的一点就是它把CorazaWAF作为基础能力内置了。Lucky本身在架构里承担反向代理和站点统一入口的角色而在入口这一层嵌入WAF正好是拦截恶意请求成本最低的位置——请求还没进上游业务就可以被审计、拦截、记录。这一点对我们这种做内部系统聚合、对外暴露多个管理入口的团队来说意义非常大。需要先解释一下这里的角色分工。CorazaWAF是一个开源的Web应用防火墙引擎它兼容了ModSecurity的规则语法而OWASP核心规则集Core Rule Set简称CRS是OWASP组织维护的一套规则文件相当于WAF引擎里的“杀毒库”。在Lucky网关里Coraza负责执行规则CRS负责定义“什么样的请求是可疑的”两者是引擎与策略的关系。1.2 开源规则集与规则自维护的边界说实话别指望任何WAF规则集能百分百拦住所有攻击。CRS能做的是挡住绝大多数无差别扫描、脚本小子的常见payload以及在出现真实攻击时给出足够清晰的日志线索。它的优势在于三点稳定、透明、可审计。稳定CRS 4.x版本对规则的分类、编号、行为都有严格约定升级路径比较平滑。透明所有规则都是纯文本命中哪条、为什么命中、命中后的动作是什么全部可查。可审计规则ID是全球通用的。你在日志里看到942100全世界用CRS的人都知道那是SQL注入探测规则这一点在安全合规审计时特别有用。再讲一个容易被忽略的点国内很多团队被“WAF 买了就完事”这个思维坑过。商业WAF虽然省心但它的规则更新跑在厂商的黑盒里出了问题你只能反馈、等。自建Coraza CRS的组合规则文件在自己手里误报可以自己改绕过手法可以自己写规则补整个安全能力是沉淀在自己团队内部的。长期看这是真正能积累出安全运营能力的方式。当然代价也很现实你要花时间理解规则集的结构和运行机制否则就会出现我后面要说的那种“开箱即用、上线误报”的惨状。2. Coraza与ModSecurity的兼容底座CRS规则集是怎么跑起来的2.1 规则语言一脉相承不是两个物种很多第一次接触Coraza的人会问一个问题CRS不是给ModSecurity用的吗怎么Coraza也能跑这得从规则引擎的语法层面看。CRS 4.x本质上是用ModSecurity规则语言写成的文本规则集而Coraza的设计目标就是兼容这套规则语言的核心子集。也就是说规则文件里写的SecRule、SecAction、SecRuleRemoveById这些指令Coraza引擎认识并且能按照同样的语义执行。两者不是两个物种更像是同一套方言在不同宿主环境下的实现。为了让你更直观理解我列一下平时最常用的几个指令在两边的情况指令作用Coraza支持情况SecRuleEngine设置引擎模式On/DetectionOnly/Off支持SecRequestBodyAccess是否对请求体做检查支持SecRule定义一条检测规则支持SecAction执行一个动作不发变量匹配支持ctl:ruleRemoveById在规则执行链中动态移除某条规则支持SecAuditEngine审计日志开关支持这里有一个很重要的理解门槛Coraza不是简单把规则文本读一遍而是要解析规则里的变量、运算符、转换函数、动作、链式规则这些结构。所以CRS里那些带链式规则chain的复杂检测项Coraza能不能正确执行取决于它对这个语言子集的实现程度。建议在使用前看一下版本对应的兼容性说明尤其是你用的CRS版本比较新的时候。2.2 CRS的规则分类与请求生命周期CRS里的规则ID是有规律的这个规律在排错时价值巨大。我第一次排查误报时靠的就是日志里那个rule id一查就知道是哪个攻击类型。规则ID编号段大致是这样分配的规则ID区间攻击类型900000起规则配置与调整910000起请求头如扫描器特征920000起协议与编码异常930000起文件与路径攻击931000起路径穿越932000起命令注入933000起PHP注入941000起XSS跨站脚本942000起SQL注入943000起会话固定944000起反序列化攻击949000起异常分数汇总950000起附加规则因为CRS的检测机制不是“一条规则命中就拦”而是采用异常分数anomaly score累积的方式。每类规则命中后会给请求加一定分数所有规则跑完之后总分超过阈值默认是5分或10分具体看crs-setup.conf里的配置才执行拦截动作。这个设计的好处是能对抗多特征组合的攻击坏处是误报排查时不能只盯一条规则要看多个命中点的叠加。举个例子一个请求可能因为UA头比较可疑加了2分参数里又带了一个SQL关键字加了4分单独看都不是实锤但总分超过阈值就被拦了。这种情况在真实业务里非常常见。2.3 Coraza在Lucky中如何执行CRS我之前在Lucky里跑通CRS的时候特意确认过整个请求链路客户端请求先到Lucky的统一入口Lucky解析出目标站点和路由后在把请求转发给上游业务之前先进入Coraza的检测阶段。这个阶段的处理顺序是加载规则文件按顺序对请求执行各条规则然后根据规则命中的异常分数决定放行、拒绝还是仅记录。整个过程是同步的也就是说请求停留在这个阶段的时间会直接变成业务接口的响应延迟。这也是为什么我后面会专门花一章讲性能调优——WAF不是免费的它在网关层干的活越多请求的延迟就越明显关键是怎么把开销控制在合理范围内。规则文件的加载顺序也值得注意。Coraza启动时会按你配置文件里声明的顺序加载规则文件CRS的加载顺序有明确约定先加载coraza.conf-recommended或你自己写的coraza.conf再加载crs-setup.conf最后加载rules目录下的规则。顺序错了后面引用的变量或配置可能还没定义就会出现规则行为异常但引擎不报错的情况。3. Lucky中挂载CRS的配置链路与生效验证3.1 准备CRS文件目录先把CRS规则集从GitHub拉下来建议直接切到release tag不要用master分支方便后续升级和排查问题。git clone --branch v4.0.0 https://github.com/coreruleset/coreruleset.git拉下来的目录结构大致是这样的coreruleset/ ├── crs-setup.conf.example ├── rules/ │ ├── REQUEST-910-IP-REPUTATION.conf │ ├── REQUEST-913-SCANNER-DETECTION.conf │ ├── REQUEST-920-PROTOCOL-ENFORCEMENT.conf │ ├── REQUEST-930-APPLICATION-ATTACK-LFI.conf │ ├── REQUEST-931-APPLICATION-ATTACK-RFI.conf │ ├── REQUEST-932-APPLICATION-ATTACK-RCE.conf │ ├── REQUEST-933-APPLICATION-ATTACK-PHP.conf │ ├── REQUEST-941-APPLICATION-ATTACK-XSS.conf │ ├── REQUEST-942-APPLICATION-ATTACK-SQLI.conf │ ├── REQUEST-943-APPLICATION-ATTACK-SESSION-FIXATION.conf │ ├── REQUEST-949-BLOCKING-EVALUATION.conf │ ├── RESPONSE-950-DATA-LEAKAGES.conf │ └── ...先把crs-setup.conf.example复制成crs-setup.conf。这个文件里大部分配置默认是注释掉的但有几项我建议按需打开比如异常分数阈值默认是5和10如果你在DetectionOnly模式观察期可以先把阈值调低多暴露一些可疑请求出来。请求体检查开关如果业务有表单提交、JSON提交必须把SecRequestBodyAccess设为On否则请求体里带的攻击特征完全不会被检查。我自己的习惯是准备一个独立的custom.conf文件用来放所有不属于CRS原始文件的配置和自定义规则尽量不动CRS自己的文件。原因很简单以后CRS升级直接替换整个rules目录不会被我们改过的东西拖累。3.2 在Lucky配置里声明WAF规则Lucky的配置接口不同版本可能略有差异但大体上是一个基于yaml或json的站点配置块。下面是我当时用的一个配置写法注释已经写得很完整了你可以对照着看server: listen: :8080 engine: coraza waf: enabled: true mode: detection # detection 只记录, enforcement 拦截 files: - /etc/coraza/coraza.conf - /etc/coraza/crs/crs-setup.conf - /etc/coraza/crs/rules/*.conf - /etc/coraza/custom.conf routes: - path: /api upstream: http://127.0.0.1:9000这里最关键的两个设计是mode和files。mode我强烈建议第一次上线时先用detection。所谓detection就是引擎只记录命中不真正拦截引擎返回的结果和真实拦截时的判断逻辑完全一致但不会影响业务。你跑个三五天把误报全部清完再切到enforcement这样既能摸清规则集的脾气又不会因为误杀把业务搞挂。files的顺序我刚才提过不能乱。coraza.conf里定义了引擎的全局参数crs-setup.conf里定义了异常分数阈值等配置rules目录里才是真正的检测规则custom.conf放你的覆盖和放行规则。这个顺序如果乱了后面的规则可能引用不到前面定义的配置行为会变得很诡异。3.3 验证规则生效的三种方式配置写完了怎么确认CRS真的在拦东西、而不是躺在那吃灰我试过三种方法按操作成本从低到高排列。第一种用curl带一个明显的测试payload看返回码变化。比如访问一个接口带上SQL注入常见特征curl -i http://your-gateway/api/demo?id1%20OR%2011如果是enforcement模式网关应该直接返回403如果是detection模式业务正常返回200但审计日志里会多出命中记录。这个办法最快但只适合验证最基本的链路。第二种开审计日志看真实流量里有没有东西命中。Coraza的审计日志会在请求命中规则时把原始的请求头、请求体、命中规则ID都记录下来。这个方法特别适合上线初期因为你会发现生产环境里的攻击请求远比想象的普遍很多扫描器一天到晚在试各种路径和payload。第三种也是最靠谱的一种把WAF切到detection模式让它和业务并行跑一段时间然后定期导出审计日志做分析。这一步能让你在误报真正变成事故之前就把问题定位清楚。我后面误报那个案例就是在detection模式下发现的如果当时直接开enforcement搜索结果接口就当场挂了。4. 误报收敛实战一次搜索接口被SQL规则误杀的处理过程4.1 事故现场一个“select”引发的拦截当时我们的系统里有一个搜索接口前端会调用/search?q...直接把用户输入的原样传给服务端做分词检索。因为用户搜的内容什么都可能有参数里出现过“select”、“update”、“and”这类词本身没有任何攻击意图但在CRS的SQL注入规则看来这些词和数据特征组合在一起就是疑似注入的特征。我印象很深刻的一次误报是同事反馈搜索“select top 10 stars”这种词组时接口直接返回了403。我马上到审计日志里查命中的规则ID是942100。顺着规则文件一查这条规则是“SQL注入语法探测”它匹配的参数里出现了SQL关键字组合包括select、union、from这些。业务参数叫q内容又是用户自由输入的文本命中的概率自然高。这里有一个很多新手会犯的错误一看到误报第一反应是“把这条规则关了”。但942100只是SQL注入规则里的冰山一角如果你只是把这一个ID关掉攻击者换个注入手法规则照样拦不住。正确做法是找到误报的边界条件让规则绕过“业务参数本来就允许包含这些关键字”的场景同时保留对其他真正注入特征的检测能力。4.2 用规则定位技术做定向放行而不是关掉检测Coraza支持在运行时通过ctl:ruleRemoveById动作动态移除某条规则。你可以基于请求路径、参数名、来源IP等维度在一个独立的规则文件里做定向放行。我当时在custom.conf里加了这样一段规则SecRule REQUEST_URI beginsWith /search id:990001,phase:1,pass,nolog,t:lowercase,ctl:ruleRemoveById942100这条规则的含义是当请求URI以/search开头时在阶段1就移除942100的检测逻辑。这样既能保证搜索接口返回正常结果又不会影响其他接口对SQL注入的检测。需要注意这里用的是phase:1。因为请求在进入阶段2请求体检查之前就要把942100移除否则请求体里一带上SQL关键字还是会触发。在实际生产环境里你可能需要同时处理多个规则ID可以直接写成SecRule REQUEST_URI beginsWith /search id:990001,phase:1,pass,nolog,t:lowercase,ctl:ruleRemoveById942100,ctl:ruleRemoveById941100这里提一个排查时特别有用的点审计日志里通常能看到当前请求命中的所有规则ID以及每个规则加了多少分。你把它们列出来如果发现某些规则只在一个特定路径上误报那就说明这个路径的业务参数和规则特征冲突了适合用定向放行如果误报分散在很多路径上那可能是规则里有编码解析逻辑和业务数据格式冲突需要从更底层去调整。4.3 同类误报的普适排查方法这次误报处理完之后我把整个排查过程沉淀成了一张查检表后面遇到类似问题都是按这个流程走的分享给你直接抄作业步骤操作意图1从审计日志拿到命中的规则ID明确是哪条规则导致的误报2到CRS规则文件里检索该ID看规则匹配的变量和条件搞清楚规则为什么判定可疑3对比业务参数的正常取值范围判断是规则过严还是请求确实有异常4找业务方确认参数是否有机会被外部恶意控制确认风险等级5在custom.conf里写定向放行规则最小化影响面保留检测能力只绕过冲突场景6切回detection模式跑一段时间验证确保没有新误报这套流程下来绝大多数误报都能在不知不觉中被处理干净而且不会破坏WAF整体的防护能力。还有一个建议在上线新接口之前把接口的路径、参数、上传文件类型列个清单提前对照CRS的规则分类过一遍比上线后出问题再排查省事得多。5. 性能开销从哪来阈值调整与规则裁剪的平衡5.1 开启WAF后接口延迟翻倍的实测开WAF之前我们有个上传接口的平均响应时延大概在40ms左右开了CRS之后直接涨到150ms。第一反应是“是不是规则有问题导致死循环”后来用火焰图分析才知道大部分时间消耗在规则引擎对请求体内容的扫描上。Coraza在检测请求体时需要先把body读出来然后按规则里的运算符和正则表达式跑匹配。CRS默认上百条规则每条规则都要过一遍尤其涉及正则的执行叠加起来开销就很明显。请求体越大耗时越长这是物理规律。这里有几个关键参数直接决定WAF对性能的影响有多大参数作用默认值建议SecRequestBodyAccess是否检查请求体Off有表单/JSON就开启SecRequestBodyLimit请求体最大检查字节数13107200按业务最大包体调整SecRequestBodyNoFilesLimit排除文件上传后的请求体大小限制131072上传多的场景调大SecPcreMatchLimit正则匹配执行上限1000防止正则灾难SecPcreMatchLimitRecursion正则递归上限1000防止正则灾难我当时的做法是文件上传接口因为文件内容本身不需要做文本攻击检测把请求体限制调低并且把这个路径的请求体访问关掉普通JSON接口保持请求体检查但把SecPcreMatchLimit从默认的1000改成300减少单个请求上正则执行的总预算。5.2 阈值调整与规则裁剪的平衡异常分数阈值也是一个可以在性能和安全之间做取舍的点。默认情况下CRS的阈值是5和10。阈值越低单个请求越容易被判定为可疑拦截越激进但误报和引擎内部需要做的工作也越多。你可以这样理解异常分数阈值就是“多少个可疑特征叠加才会触发拦截”。阈值等于5意味着一条高分规则命中或者两条低分规则叠加就会拦截阈值等于10需要更多特征叠加才会拦截。如果业务接口对实时性要求高、特征又复杂适当调高阈值能显著降低误拦率和性能消耗。我给的参考配置是这样的SecAction id:900100,phase:1,nolog,pass,t:none,setvar:tx.blocking_threshold5如果业务接口比较多我建议按路径设置不同阈值而不是全局统一。比如登录接口这类攻击面大的路径阈值维持5搜索接口这类容易误伤的阈值调到10或者更高。5.3 裁剪不是禁用安全最后讲裁剪。很多团队在性能优化时喜欢大范围关闭规则比如直接把整个941000XSS或942000SQL注入注释掉。这种做法其实是把安全能力废掉了非常危险。合理裁剪的思路是根据你业务实际用到的技术栈把不会涉及的攻击类别关掉。举个例子如果服务端完全不用PHP933000PHP注入的规则就是纯冗余的关了不影响安全如果反序列化在架构里根本没出现944000的规则也可以不加载。我做过一个相对干净的方案在一个纯Go写的API服务前面挂了WAF服务端不解析PHP也没有文件上传需求我最后保留的是910扫描器、920协议、931路径穿越、941XSS、942SQL注入这几类其他全部注释。整体规则文件小了三分之一性能好了不少实际攻击检测能力没有打折。6. 用OWASP ZAP自测规则效果与白名单维护经验6.1 ZAP和CRS是同一生态的两把家伙OWASP ZAP是OWASP组织下的开源Web应用安全扫描器它和CRS虽然分工不同但搭配起来非常默契ZAP负责“主动发起攻击测试”CRS负责“在请求到达业务之前把攻击拦下来”。你可以把ZAP当作一个自动化攻击模拟器用它来验证规则集是否真的在正常工作。我当时在Lucky网关后面搭了一个测试环境业务流程和线上保持一致然后在本地启动ZAP配置代理指向测试环境的网关地址让它对整个站点跑了一轮主动扫描。ZAP的主动扫描会生成很多攻击payload包括SQL注入、XSS、路径穿越、命令注入等。这些请求会打在你的WAF上如果CRS真的生效扫描结果里大部分高风险告警应该是“连接被重置”或者“403响应”如果ZAP报告里的漏洞是200响应说明规则引擎可能漏了或者请求根本没被WAF检查到。这个验证的价值在于它能直接告诉你当前这套规则和配置在你的业务场景里真实防护效果到底如何而不是停留在“看起来拦了”的错觉里。6.2 从测试结果反推规则事件用ZAP跑完一轮扫描后我从两个地方对照数据ZAP告警列表和Coraza审计日志。如果ZAP报告里某个注入点返回了200同时Coraza日志里完全没有该请求的命中记录那就是配置链路的问题常见原因包括该路径被规则排除了、请求体检查没开、WAF只挂在某个域名上而测试请求走了另一个入口。如果Coraza日志里有命中但ZAP依然拿到非403响应那就是引擎和规则之间的执行顺序或模式问题需要单条规则单条规则地debug。这个流程走下来你对自家WAF的“脾气”会摸得特别清楚。另外ZAP扫描时一定要在隔离环境进行不要对着生产环境扫。哪怕你觉得自己配置很稳主动扫描的并发和payload组合也可能触发意想不到的异常行为。6.3 文件上传场景的典型拦截与放行配置相关热搜里一直有“owasp zap文件上传”这里专门展开一下这个最常见的坑。文件上传接口在CRS里非常容易被拦原因五花八门请求体的Content-Type是multipart/form-data某些协议规则会做额外检查文件名带特殊字符或很长的内容会命中文件名长度规则上传内容本身是图片或压缩包二进制数据里可能碰巧含有和文本攻击特征相似的字节序列。我当时上传接口的误报集中在920420请求体类型检查和932160命令注入特征探测这两条上。前者是因为业务允许上传任意类型的附件后者是因为某些文件名里带了“;”分号被规则判定为疑似命令拼接。针对这种情况我给当时的配置加了一个定向放行SecRule REQUEST_URI beginsWith /api/upload id:990002,phase:1,pass,nolog,t:lowercase,ctl:ruleRemoveById920420,ctl:ruleRemoveById932160但要强调文件上传是安全重灾区放行规则必须非常克制。我见过有些团队为了让功能跑通把整个上传路径的WAF全关了结果被传了个webshell上去那才是真正的灾难。正确做法是能不放行就不放行确需放行的要在上游做好文件类型校验、内存级内容检查、文件重命名、存储目录禁止执行脚本这些配套措施。6.4 白名单维护的实践心得白名单规则也就是你写的那些定向放行规则在一开始往往只有一两条但随着业务迭代会越积越多。积累到一定量就会变成一个新的维护负担。我的习惯是把所有自定义规则按用途分类管理。custom.conf里用大段注释分隔出“路径放行”“参数放行”“用户代理放行”几个区块每条规则后面注明添加人、日期和原因。比如# 2024-06-18 张三搜索接口允许包含SQL关键字定向移除942100 SecRule REQUEST_URI beginsWith /search id:990001,phase:1,pass,nolog,t:lowercase,ctl:ruleRemoveById942100这样做的好处是规则集升级或者安全审计时你能快速判断每条白名单规则是否还有存在的必要。很多团队的WAF配置最后变成一团乱麻问题不在于规则本身写错而在于没有人追踪“这条规则为什么在这里”。我个人在实际操作中最深的体会是WAF规则集不是装完就一劳永逸的基础设施它更像一套需要持续保养的安全策略。上线前花一周做detection模式观察每个季度定期用ZAP复测一轮规则有效性业务接口改动时同步回查WAF配置——这套节奏养成了CorazaWAF和OWASP CRS带来的价值会远远大于那点维护成本。
返回列表