ModSecurity 3 is dead. Time to talk about its successor.
If you’ve been running ModSecurity in front of your self-hosted apps, here’s the thing: ModSecurity 3.x (the Apache module) is officially end-of-life as of January 2024. Trustwave’s SpiderLabs handed off maintenance, and the C codebase is essentially in maintenance mode. But before you panic about upgrading or ripping things out, let me introduce you to Coraza — the Go-native WAF that’s quietly replacing ModSecurity across the open-source ecosystem.
Coraza isn’t a brand-new project. It’s actually a spiritual successor built by some of the same people who maintain the OWASP Core Rule Set (CRS) — the industry-standard ruleset that powers ModSecurity. The result? A WAF that speaks ModSecurity’s language fluently, runs faster due to being written in Go, and integrates seamlessly into modern reverse proxies like Caddy, Traefik, and Envoy.
If you’re running a reverse proxy for your home lab or small business infrastructure, this matters. A lot.
What’s a WAF, anyway? (Quick refresh)
Before we get into the drama between these two, let’s ground ourselves. A Web Application Firewall (WAF) sits in front of your web apps and inspects incoming HTTP requests before they hit your actual application. It’s looking for suspicious patterns — SQL injection attempts, cross-site scripting (XSS), path traversal, that kind of thing.
The WAF doesn’t block individual IP addresses (that’s your basic firewall). Instead, it understands HTTP semantics. It can see into request headers, query strings, POST bodies, and file uploads. It compares what it sees against a ruleset. If a request matches a rule marked as “block,” the WAF stops it dead.
The ruleset in question is usually the OWASP Core Rule Set (CRS) — a free, open-source collection of detection rules maintained by OWASP volunteers. It covers the OWASP Top 10 vulnerabilities and has been battle-tested across millions of production deployments. When someone says “our WAF runs CRS,” they mean it’s inspecting traffic using these community-maintained rules.
ModSecurity, for years, was the way you ran CRS. It was the reference implementation. That’s changing.
The ModSecurity story (and why it’s ending)
ModSecurity was created by Ivan Ristic at Breach Security in the early 2000s. Trustwave acquired it, handed it off to SpiderLabs, and it became the industry standard. If you wanted to add WAF protection to Apache, Nginx (via ngx_http_modsecurity_module), or even standalone, ModSecurity was your answer.
Here’s the problem: it’s written in C. Back in the 2000s, that made sense — maximum performance, proven technology. Fast forward to today, and a C codebase built on Apache’s ecosystem is… let’s say “not where anyone wants to be.” Building it is a pain. Debugging segfaults is a pain. Adding new features requires C expertise. Integrating it into modern infrastructure (Kubernetes sidecars, serverless, modern proxies) is awkward.
In 2021, ModSecurity 3 was released, supposedly a rewrite. But it’s still a C library (libmodsecurity) with language bindings. The Apache connector still exists, and it’s still finicky. Build issues with different OS versions are common. Performance is decent but not spectacular.
By 2024, Trustwave basically said: we’re not actively developing this anymore. Maintenance mode only. If you’re building new WAF deployments, you should look elsewhere.
Coraza: The Go rewrite everyone needed
Around 2020, some OWASP CRS maintainers started asking: “What if we just rewrote this in Go?”
The answer, turns out, was Coraza. It’s not a fork of ModSecurity. It’s a clean rewrite from scratch, written in Go, that speaks ModSecurity’s rule language (SecRule) fluently. It understands the exact same directive syntax, the exact same ruleset, the exact same paranoia levels.
The genius move: Coraza is embeddable. Because it’s a Go library, not a monolithic Apache module, it can be compiled into any Go-based proxy. Caddy has a native plugin. Traefik has a middleware. Envoy uses it via WASM. This is how modern infrastructure works.
Is it faster than ModSecurity? Generally, yes. Not because Gophers are magic, but because Go’s concurrency model and memory management are suited to the request-per-goroutine pattern that WAF inspection fits into naturally. ModSecurity’s C code was written for a different era.
Why Coraza matters for self-hosters
If you’re running a reverse proxy in front of your home lab — Caddy for your internal services, Traefik for your Kubernetes cluster, or even Envoy if you’re fancy — you now have a clear path to adding WAF protection without the build friction and performance overhead of ModSecurity.
The rule compatibility is near-perfect. You can drop the OWASP CRS ruleset directly into Coraza and it works. Not “mostly works.” Works. This is because the CRS folks were involved in Coraza’s design from day one.
The configuration is approachable. Instead of fiddling with Apache conf directives or awkward language bindings, you configure Coraza via your proxy’s native config format. We’ll look at examples in a moment.
The debugging is sane. Coraza logs audit events in a format that’s nearly identical to ModSecurity’s. You get rule IDs, matched patterns, anomaly scores, everything. When a legitimate user gets blocked by a false positive, you can pinpoint the exact rule and tune it.
Deployment patterns: Caddy with Coraza
Let’s start with Caddy, since it’s the easiest and most self-hosted-friendly option.
example.com { encode gzip
coraza { uri /modsecurity.conf }
reverse_proxy localhost:8080 { header_up X-Forwarded-For {http.request.header.X-Forwarded-For} header_up X-Forwarded-Proto {http.request.proto} }}That uri /modsecurity.conf points to your OWASP CRS ruleset. Caddy’s Coraza module reads that file at startup and uses it for every incoming request.
The config file format is ModSecurity-compatible. You can grab CRS straight from GitHub:
mkdir -p /etc/caddy/crscd /etc/caddy/crsgit clone https://github.com/coreruleset/coreruleset.git .cp crs-setup.conf.example crs-setup.confThen reference it in your Caddyfile like above. On the next caddy reload, your WAF is live.
Caddy with tuning and audit logs
Here’s a more realistic config with paranoia level tuning and audit logging:
example.com { coraza { uri /etc/caddy/crs/crs-setup.conf audit-log /var/log/caddy/modsecurity-audit.log audit-level all }
reverse_proxy localhost:8080}The audit log captures every request that triggered a rule. You’ll see entries like:
--1234567890-A--[18/Jun/2026:14:23:45 +0000] example.com 127.0.0.1 - - "GET /api/users?id=1' HTTP/1.1" 200 - "-" "-" - - 0 0 5 5 0 0--1234567890-B--GET /api/users?id=1' HTTP/1.1Host: example.comUser-Agent: curl/7.64.1
--1234567890-C--HTTP/1.1 200 OKContent-Length: 142
--1234567890-D--mod_security-message: Warning. Pattern match at TX:paramcounter. [File "/etc/modsecurity/rules/crs/rules/REQUEST-32-xss-rules.conf"] [line 60] [id "941150"] [msg "XSS Attack Detected via libinjection"] [severity "CRITICAL"] [ver "OWASP_CRS/3.4.0"]That [id "941150"] is your rule ID. You can look it up in the CRS documentation and decide: is this a false positive I should whitelist, or is this a real threat I should monitor?
Traefik integration (via Coraza middleware)
If you’re running Traefik, the setup is similar but uses Traefik’s middleware syntax:
version: '3.8'services: traefik: image: traefik:latest ports: - "80:80" - "443:443" volumes: - /var/run/docker.sock:/var/run/docker.sock - ./traefik.yaml:/traefik.yaml - ./crs-rules:/etc/traefik/crs networks: - web
app: image: my-app:latest labels: - "traefik.enable=true" - "traefik.http.routers.app.rule=Host(`example.com`)" - "traefik.http.routers.app.middlewares=coraza" - "traefik.http.middlewares.coraza.plugin.coraza.rulesfile=/etc/traefik/crs/crs-setup.conf" networks: - web
networks: web: driver: bridgeTraefik supports Coraza via its plugin system. The middleware runs on every request before it hits your app.
A real SecRule example
Here’s what the actual rules look like. This is from the OWASP CRS:
# Detect SQL injection attemptsSecRule ARGS "@rx (?i:(\^|\|\||--|#|\/\*|\*\/|xp_))" \ "id:942251,\ phase:2,\ block,\ msg:'SQL Injection Attack',\ tag:'attack/sql_injection',\ severity:CRITICAL"
# Detect XSS attemptsSecRule ARGS "@contains <script" \ "id:941130,\ phase:2,\ block,\ msg:'Possible XSS Detected',\ tag:'attack/xss',\ severity:HIGH"These are simple pattern matches, but they compose into a sophisticated detection system. The @rx operator does regex matching. The @contains operator does string matching. There are operators for Base64 detection, antivirus scanning, data-uri patterns, and more.
When a request matches, the phase:2,block directive tells Coraza to stop it. The msg becomes the audit log message. The id is how you reference it later.
Paranoia levels: tuning false positives
One thing that trips people up: the OWASP CRS comes with paranoia levels. Level 1 is the default and catches maybe 80% of real attacks but has very few false positives. Level 2 is stricter and catches more attacks but also flags more legitimate requests. Level 3 and 4 are for high-security environments (financial institutions, healthcare) and will block a lot of normal stuff.
Set your paranoia level in the CRS config:
# In crs-setup.confSecAction "id:900000,\ phase:1,\ nolog,\ pass,\ setenv:tx.paranoia_level=2"Start at level 1. Run for a few weeks, monitor your audit logs, and identify false positives. Then decide if you need to tune specific rules or move to level 2.
Debugging and whitelisting false positives
When a real user gets blocked by a WAF rule, you need to whitelist them. This is where the audit logs shine.
Say your legitimate API client is getting blocked on rule 942251. You whitelist it like this:
# Whitelist specific endpoint from SQL injection rulesSecRule REQUEST_FILENAME "@streq /api/safe-endpoint" \ "id:10000,\ phase:1,\ nolog,\ pass,\ ctl:ruleEngine=Off"Or, more surgical:
# Whitelist specific parameterSecRule ARGS:search_term "@rx ^[a-z0-9_]+$" \ "id:10001,\ phase:1,\ pass,\ ctl:RuleRemoveById=942251"The ctl:RuleRemoveById directive disables a specific rule for matching requests. Much better than turning off the entire WAF.
Integration with fail2ban and CrowdSec
A WAF doesn’t replace IP-level blocking. You still want to catch brute-force attacks at the firewall level.
With fail2ban, you can parse Coraza’s audit logs and ban IPs that trigger too many high-severity rules:
[Definition]failregex = ^<YOUR_IP> - -.*".*".*severity:CRITICAL.*$ignoreregex =CrowdSec is even better — it’s a modern threat intelligence system that works beautifully with WAF logs. Point it at your Coraza audit logs and it’ll not only ban local threats but also share and receive IP intel from the community.
Performance: why Coraza wins
Here’s an honest take on performance: both ModSecurity and Coraza can inspect HTTPS traffic. Both understand the same rulesets. But Coraza, being Go, has some advantages:
- Concurrency: Each request gets a goroutine. ModSecurity uses thread pools or processes. Goroutines are lighter.
- Memory: Go’s garbage collector is generally more predictable than C’s manual memory management (especially in a multi-threaded context).
- Startup: Coraza loads into your proxy’s memory space. ModSecurity requires separate processes or language bindings. Coraza starts faster and uses less memory.
Real-world benchmark? Coraza typically handles 20-30% more requests per second than ModSecurity on the same hardware, when both are running the full OWASP CRS ruleset. Your mileage will vary based on rule complexity, request size, and hardware.
The migration from ModSecurity to Coraza
If you’re currently running ModSecurity (either via Apache, Nginx, or a connector), here’s the path:
- Export your current ruleset — Grab your
/etc/modsecurity/rules directory or your Apache/Nginx ModSecurity config. - Test with the OWASP CRS baseline — Start with the official CRS from GitHub, run it in audit-only mode (no blocking) for a week.
- Review audit logs — Identify false positives and create whitelists for legitimate traffic.
- Move to blocking mode — Once you’re confident in your rules, set Coraza to actually block.
If you’ve customized ModSecurity rules heavily, they’ll mostly work with Coraza, but test thoroughly. Some obscure operator variations might not exist in Coraza, but 95% of real-world rulesets are compatible.
Should you migrate right now?
Here’s my take: if you’re running Caddy or Traefik, switch to Coraza immediately. The gain in simplicity and performance is real, and the risk is minimal because the ruleset is identical.
If you’re running ModSecurity on Apache, you have more to consider. Apache + ModSecurity is a stable, proven stack. If it’s working and you’re not hitting pain points, you don’t have to rush. But for new deployments, I’d reach for Caddy + Coraza instead.
If you’re building a new reverse proxy setup for your self-hosted infrastructure, Coraza + Caddy is the obvious choice. It’s 2026. You deserve a WAF that doesn’t feel like you’re maintaining C code from 2003.
The open-source ecosystem is moving this direction. Envoy uses Coraza. Kubernetes ingress controllers use Coraza. Cloudflare is even looking at Coraza for some use cases. ModSecurity’s time as the default WAF has passed.
Time to upgrade.
Resources
- Coraza on GitHub: https://github.com/coreruleset/coraza
- OWASP CRS: https://github.com/coreruleset/coreruleset
- Caddy + Coraza plugin: https://caddyserver.com/docs/modules/http.handlers.coraza
- Traefik Coraza middleware: https://doc.traefik.io/traefik/middlewares/http/coraza/
- CRS rule documentation: https://coreruleset.org/
Set up a WAF. Your future self at 2 AM will thank you when some bot tries to inject SQL into your app and gets blocked silently instead of corrupting your database.