这篇文章想回答的问题
如果你今天在企业里维护一套自建 WAF,三个事实很难绕开:
- Trustwave 已经在 2024 年 7 月 1 日终止对 ModSecurity 的商业支持,把代码托管权移交给 OWASP(LevelBlue 公告、OWASP 公告)。
- F5 在 2024 年也宣布 NGINX 的 ModSecurity WAF 模块进入 EOL,并把目光转向 Coraza(F5 NGINX 公告)。
- CRS(Core Rule Set)从 v3 进入 v4 之后还在持续高频迭代,最新 release 是 v4.27.0(2026‑06‑01)。
“ModSecurity 还活着”和“ModSecurity 还是默认选择”是两件事。这篇文章不写 EOL 焦虑,目标是回答一个非常具体的问题:
如果今天我要给一个新部署的 API 服务加 WAF,应该走哪条路径,付出多少 CPU 代价,CRS v4 在 PL1 / PL2 上分别会怎么影响业务?
为了避免泛泛而谈,下文所有性能数字都来自一次本地复现实测:Linux x86_64 主机、Go 1.23.4、Coraza v3.7.0、CRS v4.27.0 完整 phase‑1 + phase‑2 规则集,单进程单线程跑请求循环。具体跑法见后文“可复现实验”一节。
一、栈到底换了什么
ModSecurity 一直是“引擎 + 规则集”分离的架构:
- 引擎:ModSecurity v3(libmodsecurity,C++);
- 规则:OWASP CRS(SecLang 文本规则);
- 接入:Apache 模块、libmodsecurity + ModSecurity‑nginx connector,或 IIS 模块。
2024 年发生了两件实质性变化:
- Trustwave 退出商业维护,OWASP 接管 ModSecurity 代码库,最新版本
v3.0.15(2026‑04‑28) 已经在 OWASP 仓库下发布。 - 一个完全用 Go 写的新引擎 Coraza 进入 OWASP Production Project,并被 CRS 团队、F5 NGINX 团队都纳入推荐迁移目标。
CRS 团队对此的判断很直接:CRS 同时在维护 ModSecurity 和 Coraza 两条规则后端,“100% 兼容 CRS v4” 是 Coraza 当前阶段的硬约束(Coraza README)。
“换引擎不换规则”是这次迁移最舒服的地方:CRS 一份规则文件,在 ModSecurity 和 Coraza 上都能直接 Include,不需要改 SecLang。但接入方式会重新洗牌。
二、Coraza 不是 ModSecurity 的进程内替代
很多人对 Coraza 的第一印象是“Go 重写的 ModSecurity”,于是默认它能像 libmodsecurity 那样作为 nginx 模块加载进同一个进程。这条路目前不是主路。
按照 Coraza 官方 README 列出的接入方式,状态是这样的:
| 接入路径 | 形态 | 项目状态(README 表述) |
|---|---|---|
| Coraza Core | Go library,嵌入 Go 程序 | 稳定 |
coraza-caddy | Caddy 反代/服务器插件 | 稳定,缺维护 |
coraza-proxy-wasm | Envoy 等支持 proxy‑wasm 的代理 | 稳定,仍在开发 |
coraza-spoa | HAProxy SPOE/SPOA daemon | experimental |
libcoraza | 给 nginx 等用的 C 库 | experimental |
工程含义有三层:
- 要把 Coraza 接到 nginx 上,目前没有 OWASP 官方 nginx 模块的“稳定”项。要么改用 Caddy / Envoy / HAProxy 做前置层,要么自己接
libcoraza或者 proxy‑wasm。 - HAProxy 通过 SPOA 把请求外送给 Coraza daemon 检测:HAProxy 进程不内嵌引擎,规则评估走 TCP/SPOP 协议给
coraza-spoa,HAProxy 根据txn.coraza.action决定 deny。这是非常干净的“数据面/控制面分离”,但额外引入一个进程边界和一次本地 RPC(coraza‑spoa README)。 - 如果应用本身是 Go 服务,最干净的路径是直接 import
github.com/corazawaf/coraza/v3,把 WAF 当作 HTTP 中间件嵌入业务进程,避免任何额外的代理和 SPOA。
“哪条路径最适合”不是技术上的偏好,而是看你已经有什么入口层。已有 nginx 大集群的团队不要为了上 Coraza 临时切 Caddy:把 nginx 留着,用 SPOA 或者 proxy‑wasm 做外置 WAF 是更现实的演进。
三、用 CRS v4.27.0 跑一次完整规则的代价
光看“Go 比 C++ 快多少”的口号没意义。CRS v4 默认 phase‑1 + phase‑2 总规则数已经远超 200 条,规则间还共享变量、计分、互相 chain。我们要回答的问题应当是:
加载完整 CRS v4.27.0 规则集后,Coraza 处理一次 GET 请求要多少微秒?业务侧 RPS 能给到多少?
实测条件
- 主机:Linux x86_64,Coraza 在用户态进程里跑;
- Go:
go1.23.4; - Coraza:
github.com/corazawaf/coraza/[email protected]; - CRS:
coreruleset-4.27.0; - 加载文件:
REQUEST-901-INITIALIZATION.conf加上905/911/913/920/921/922/930/931/932/933/934/941/942/943/944/949共 16 个 phase‑1/2 文件; - 关闭
SecAuditEngine/SecAuditLog写入; - 测试请求:
GET /api/items?<query>,循环 5000 次构造 transaction,仅评估 phase‑1(headers)+ phase‑2(body/args); - PL(paranoia level)分别测 1 和 2;
- 结果是单进程、未做并发、不计 TLS/网络的“纯规则评估开销”,重点在于规则集本身的计算成本,而不是入口层吞吐。
WAF 加载耗时(PL=1):
CRS v4 phase-1+phase-2 load+compile: 93.128112ms
PL=2:
CRS v4 phase-1+phase-2 load+compile: 120.915703ms
SecRule 的正则、@detectSQLi/@detectXSS(libinjection)、ahocorasick 全在启动时编译完成。整个过程不到 100ms 量级,对蓝绿发布、Pod 启动几乎可忽略。
单请求处理代价
PL=1 全规则评估:
| 用例 | n | blocked | 平均每请求 | 单进程 RPS |
|---|---|---|---|---|
正常短查询 id=42 | 5000 | 0 | 595 µs | 1682 |
| 正常长查询(一段中文/英文混合) | 5000 | 0 | 1209 µs | 827 |
SQLi 经典 id=1' OR 1=1-- | 5000 | 5000 | 812 µs | 1232 |
| SQLi UNION SELECT | 5000 | 5000 | 1061 µs | 942 |
XSS <script>alert(1)</script> | 5000 | 5000 | 948 µs | 1054 |
XSS 事件属性 onload=alert(1) | 5000 | 5000 | 893 µs | 1120 |
路径穿越 ../../etc/passwd | 5000 | 5000 | 919 µs | 1088 |
RCE cmd=cat /etc/passwd; | 5000 | 5000 | 838 µs | 1193 |
PL=2 全规则评估:
| 用例 | n | 平均每请求 | 单进程 RPS |
|---|---|---|---|
| 正常短查询 | 5000 | 606 µs | 1650 |
| 正常长查询 | 5000 | 1240 µs | 807 |
| SQLi 经典 | 5000 | 795 µs | 1257 |
| SQLi UNION | 5000 | 1110 µs | 901 |
XSS <script> | 5000 | 916 µs | 1092 |
| XSS 事件属性 | 5000 | 966 µs | 1035 |
| 路径穿越 | 5000 | 873 µs | 1146 |
| RCE | 5000 | 907 µs | 1102 |
几个工程上比较关键的判断:
- 完整 CRS v4 + Coraza 在单线程下大约处于 800–1200 RPS 量级,对应单请求约 0.6–1.2 ms。多核线性扩展不严谨地估算,每核 ~1k RPS 是一个稳健的 capacity baseline,做容量规划应当按这个数字往里塞 30–50% 余量。
- 请求越长(query string、body)越贵:长查询比短查询贵了一倍,这与 CRS 大量基于
ARGS、REQUEST_BODY、REQUEST_URI的正则评估直接相关。对外暴露超长 GET、超大 JSON body 的接口应当先通过 limit 控制(SecRequestBodyLimit、SecRequestBodyNoFilesLimit),而不是只靠 WAF 兜底。 - PL1 → PL2 的单请求开销在我们这次实验里增加非常有限(个位数百分比,部分用例反而更快,落在测量噪声内)。PL 的真正代价不是 CPU,而是误报:PL 越高,规则越激进,越容易在合法业务里命中。这一点见下一节。
注意边界:以上数字对“规则评估”有意义;它不代表生产 WAF 的吞吐。生产路径还包括 TLS 终止、HTTP 解码、上下游 keepalive、SPOA/RPC(如果走 HAProxy SPOA)等。把这些数字当“规则集合理性的下限”,而不是“WAF 上线后实际能扛多少 QPS”。
四、Paranoia Level 的真正代价是误报,不是 CPU
CRS 的 paranoia level(官方文档)把规则按“激进程度”分成 PL1–PL4。PL1 是默认,几乎不应该误报;PL2 开始进入“偶尔误报、需要例外列表”的领域;PL3/4 是“给金融、政府、银行这种愿意为安全放弃可用性的场景”保留的。
工程上能直接落到决策上的判断:
| Paranoia Level | 业务场景 | 误报治理预算 | 推荐部署模式 |
|---|---|---|---|
| PL1(默认) | 普通 SaaS、内部业务、API 网关 | 低,可几乎零治理 | 直接 DetectionOnly → On |
| PL2 | 重要业务面、登录/支付/账户接口 | 中,需要 SecRuleRemoveTargetById、白名单 | 先在 staging 跑 2–4 周 detection only,再切阻断 |
| PL3 | 高风险面(管理后台、客服后台、内部账户系统) | 高,需要按页面级例外 | 限定路径开启,不要全站 PL3 |
| PL4 | 极高敏感(密钥服务、合规网关、政府/金融对外接口) | 非常高 | 与开发节奏强耦合,非通用方案 |
实测里我们造了几个“看起来像攻击的合法查询”做对照(来源于真实业务里常见的搜索/排序/过滤),在 PL1 和 PL2 都没有触发阻断(blocked=false):
[PL1] q="select your country" blocked=false
[PL2] q="select your country" blocked=false
[PL1] q="how to use ORDER BY ..." blocked=false
[PL2] q="how to use ORDER BY ..." blocked=false
[PL1] q="user comment: I love ..." blocked=false
[PL2] q="user comment: I love ..." blocked=false
[PL1] q="filter: created_at > 2024" blocked=false
[PL2] q="filter: created_at > 2024" blocked=false
这并不能证明 PL2 不会误报——它只能证明 CRS v4.27.0 在这几条非常温和的 query 上没有抓飞。真正的误报治理要在自家流量上跑 detection‑only 至少几天甚至几周,沿着 audit log 找出 PL 升级带来的新增触发,而不是看几条造出来的样例。教训写在前面:任何 “直接上 PL3 / PL4” 的提案,本质上是在把误报成本转嫁给客服和运维。
五、可复现实验
如果你想在自己机器上跑同样的测量,最小步骤如下(环境:Linux + Go 1.22 以上):
mkdir coraza-poc && cd coraza-poc
go mod init corazapoc
# 拉取 CRS v4.27.0
curl -sL -o crs.tar.gz \
https://github.com/coreruleset/coreruleset/archive/refs/tags/v4.27.0.tar.gz
tar -xzf crs.tar.gzmain.go 的核心结构(节选,便于理解 phase 流程):
package main
import (
"fmt"
"os"
"time"
"github.com/corazawaf/coraza/v3"
"github.com/corazawaf/coraza/v3/types"
)
func main() {
rules := os.Args[1] // .../coreruleset-4.27.0/rules
setup := `
SecRuleEngine On
SecRequestBodyAccess On
SecAction "id:900400,phase:1,nolog,pass,t:none,setvar:tx.paranoia_level=1,setvar:tx.executing_paranoia_level=1"
SecAction "id:900110,phase:1,nolog,pass,t:none,setvar:tx.inbound_anomaly_score_threshold=5"
// 省略其他 crs-setup 默认 setvar
`
dirs := setup + "\nInclude " + rules + "/REQUEST-901-INITIALIZATION.conf\n"
for _, f := range []string{
"REQUEST-905-COMMON-EXCEPTIONS.conf",
"REQUEST-911-METHOD-ENFORCEMENT.conf",
// ... 直到 949-BLOCKING-EVALUATION.conf
} {
dirs += "Include " + rules + "/" + f + "\n"
}
waf, err := coraza.NewWAF(coraza.NewWAFConfig().WithDirectives(dirs))
if err != nil {
panic(err)
}
t0 := time.Now()
const N = 5000
for i := 0; i < N; i++ {
tx := waf.NewTransaction()
tx.ProcessConnection("203.0.113.1", 8080, "127.0.0.1", 12345)
tx.ProcessURI("/api/items?id=1%27%20OR%201%3D1--", "GET", "HTTP/1.1")
tx.AddRequestHeader("Host", "example.com")
tx.AddRequestHeader("User-Agent", "Mozilla/5.0")
var it *types.Interruption = tx.ProcessRequestHeaders()
if it == nil {
it, _ = tx.ProcessRequestBody()
}
_ = it
tx.ProcessLogging()
tx.Close()
}
fmt.Println("dur:", time.Since(t0))
}跑法:
go run . ./coreruleset-4.27.0/rules要切 PL,把 setvar:tx.paranoia_level=1 改成 2/3/4 即可。要看具体哪些规则被触发,用 tx.MatchedRules() 拿回 ID()、Severity()、Message(),这是 ModSecurity 里 audit log 的等价物。
为什么我们没有用 coraza-spoa、Caddy、Envoy 做完整端到端测?因为本次问题边界是“规则评估代价”,加上代理层只会让数据更难比较;先把引擎本身的代价定下来,再决定接入层的余量,这是 WAF 容量规划更稳的顺序。
结论
从 ModSecurity 迁移到 Coraza + CRS v4,最大的风险不是性能,而是对规则行为、body 处理边界和灾备路径的理解缺失。接入路径要匹配你的网关类型,CRS 版本要固定到具体 release,初始阶段必须 DetectionOnly 并接审计日志到 SIEM。阻断切换要以路径或租户为粒度逐步推进,而不是整站一刀切。
总结
这次迁移的关键不是“ModSecurity 死了,赶紧搬”。Trustwave 退出之后,ModSecurity v3 的代码还在 OWASP 仓库里活跃;CRS 也仍然支持它。但新部署的 WAF 默认线已经从 “nginx + libmodsecurity + CRS v3” 实质性切换到 “Caddy/HAProxy/Envoy/Go App + Coraza + CRS v4”:
- Coraza 是 Go 原生引擎,规则语言 100% 兼容 SecLang,CRS v4 直接
Include即可;启动 + 全规则编译在 100 ms 量级。 - 单线程下 Coraza + CRS v4 大约 800–1200 RPS / core,单请求 0.6–1.2 ms,长查询/大 body 是真正的吞吐瓶颈。
- PL1 → PL2 的 CPU 代价不大,业务决策成本几乎全部来自误报治理;PL3/4 不应该作为默认值。
- 接入层选择决定一切:nginx 集群优先走 SPOA/proxy‑wasm 外置;新业务和 Go 服务可以直接进程内嵌入;不要为了上 Coraza 把入口层换掉。
下一步问题留给后续文章:
- 在 HAProxy SPOA 路径上,SPOP 协议对单请求增加多少延迟?fail‑open 是否会因为 SPOA 进程 OOM 引入安全空窗?
- CRS v4 的 anomaly scoring 如何与 SIEM/SOC 联动,把 false positive 闭环到规则例外仓库管理?
- Coraza 在 GraphQL、gRPC、WebSocket 这些非 RESTful 入口前的检测能力边界在哪里?

