Coraza WAF vs ModSecurity in 2026: Migrating to a Modern Linux WAF

A practical 2026 guide to migrating from ModSecurity to Coraza WAF on Linux. Includes install recipes for Caddy and Nginx (OpenResty + lua-resty-coraza), OWASP CRS 4.15 rule tuning, FTW regression testing, and SIEM shipping tips drawn from production edges.

Coraza vs ModSecurity 2026: WAF Guide

Updated: August 11, 2026

Coraza is the OWASP-hosted, Go-based rewrite of ModSecurity, and in 2026 it's the practical default for a new open-source web application firewall (WAF) on Linux. It speaks the same SecLang rules, runs OWASP Core Rule Set 4.x unmodified, and ships as first-class plugins for Caddy, Traefik, HAProxy, and Envoy, while ModSecurity's Nginx connector is still the mature choice for existing installs. This guide walks through the migration decision, side-by-side rule loading, benchmark data, and the deployment recipes I use on production edges today.

  • Coraza 3.3 (July 2026) is the OWASP-graduated successor to libmodsecurity and runs the same SecLang directives, so OWASP CRS 4.15 loads without edits.
  • ModSecurity v3 is still maintained by the community after Trustwave's 2024 handoff, but pace has slowed. Coraza has more active commits and issue turnover.
  • For Nginx today, ModSecurity's libmodsecurity3 + ModSecurity-nginx connector remains the fastest path. Coraza on Nginx works via OpenResty and lua-resty-coraza.
  • Coraza is the clear winner on Caddy, Traefik, HAProxy (SPOA), and Envoy (proxy-wasm), because it's a Go library rather than a compiled C module.
  • Always start in DetectionOnly, tune false positives against the CRS paranoia levels 1–4, then flip SecRuleEngine On.
  • Use the OWASP FTW test framework to regression-check any rule changes before promoting them to enforce mode.

Coraza vs ModSecurity in 2026: what actually changed

Two events reshaped the open-source WAF landscape between 2024 and 2026. First, Trustwave handed maintenance of ModSecurity to the OWASP Core Rule Set project in early 2024, ending nearly two decades of commercial stewardship. Second, Coraza graduated to a top-level OWASP project in 2024 and shipped 3.x, its production-ready line that supports OWASP CRS 4 without regressions. Both projects share the same rule language (SecLang) and the same rule library (CRS 4.x), so the choice today is architectural, not semantic. You pick the engine that fits your reverse proxy and your operational model.

I run both in production. On legacy Nginx edges that were already speaking libmodsecurity, I've kept ModSecurity in place; it's stable, well-instrumented, and there's no functional benefit to re-tooling. On every new deployment (Caddy edges, Envoy sidecars, HAProxy load balancers) I default to Coraza because it embeds cleanly, ships as a Go module or Wasm filter, and doesn't require rebuilding a C connector on every OS upgrade. The rest of this guide gives you the concrete recipes for each path so you can make the same call.

Feature comparison at a glance

Below is the head-to-head I use with clients when they ask which engine to standardise on. The columns show real support matrices as of August 2026, not roadmap promises.

DimensionModSecurity v3.0.14Coraza v3.3
Implementation languageC++ (libmodsecurity3)Go 1.22+ (pure Go, no CGO)
Rule languageSecLang (native)SecLang (compatible)
OWASP CRS 4.xSupportedSupported
Nginx integrationNative connector (ModSecurity-nginx)Experimental C connector; production via OpenResty + lua-resty-coraza
Caddy / Traefik / HAProxy / EnvoyNone officialFirst-class plugins/filters
Wasm proxy filter (proxy-wasm)NoYes (coraza-proxy-wasm)
Memory footprint (idle, CRS loaded)~110 MB per worker~70 MB per worker
GovernanceOWASP CRS project (since 2024)OWASP top-level project
Best fit in 2026Legacy Apache/Nginx estatesCloud-native and mixed-proxy estates

Both projects agree on the rule format, which is the important point. Any CRS exclusion, custom SecRule, or custom action you write on one engine ports to the other with only path-and-include changes. That symmetry is what makes migration realistic rather than a rewrite.

Is Coraza a replacement for ModSecurity?

Yes. Coraza is explicitly positioned as the modern successor to ModSecurity and shares its rule syntax, phases, transformations, and operator library. If you have SecLang rules today, you can point Coraza at the same coraza.conf-recommended, crs-setup.conf, and rules/*.conf tree that libmodsecurity used and expect them to load. The official Coraza documentation tracks the small syntactic differences that do exist (a handful of directives like SecUnicodeMapFile are no-ops, and Coraza doesn't support Lua transformations natively yet).

The replacement caveat is deployment surface. ModSecurity has a battle-tested Nginx connector; Coraza's Nginx story is still catching up. For every other proxy on a modern Linux server (Caddy, Traefik, HAProxy, Envoy), Coraza is either the only viable option or the far more ergonomic one. My advice is functional, not tribal: keep ModSecurity where the connector is already installed and stable, and pick Coraza for anything new.

If your goal is a hardened Nginx edge more broadly (TLS, rate limits, sandboxing, WAF), pair whichever engine you choose with the discipline described in the Nginx security hardening guide. A WAF alone isn't a hardened edge.

How do you install Coraza on Caddy with OWASP CRS?

Caddy is where Coraza is happiest. The coraza-caddy module is a first-party OWASP project and ships as a Caddy plugin you compile in with xcaddy. The build takes about 30 seconds and produces a single static binary you can drop into any Ubuntu 24.04, Debian 12, or RHEL 9 host.

Step 1: Build a Caddy binary with the Coraza module

# Install Go 1.22+ and xcaddy on Ubuntu 24.04
sudo apt-get update
sudo apt-get install -y golang-go
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

# Build a Caddy binary that includes Coraza
~/go/bin/xcaddy build \
  --with github.com/corazawaf/coraza-caddy/v2

sudo install -m 0755 ./caddy /usr/local/bin/caddy
caddy version

Step 2: Fetch OWASP CRS 4.15 and the recommended Coraza config

sudo mkdir -p /etc/coraza
cd /etc/coraza

# CRS 4.15 (August 2026 release)
sudo curl -fsSL \
  https://github.com/coreruleset/coreruleset/archive/refs/tags/v4.15.0.tar.gz \
  | sudo tar -xz --strip-components=1 -C /etc/coraza

sudo cp crs-setup.conf.example crs-setup.conf

# Recommended engine config shipped by Coraza
sudo curl -fsSL -o /etc/coraza/coraza.conf-recommended \
  https://raw.githubusercontent.com/corazawaf/coraza/main/coraza.conf-recommended

Step 3: Wire it into your Caddyfile

{
    order coraza_waf first
}

app.example.com {
    coraza_waf {
        load_owasp_crs
        directives `
            Include /etc/coraza/coraza.conf-recommended
            Include /etc/coraza/crs-setup.conf
            Include /etc/coraza/rules/*.conf
            SecRuleEngine DetectionOnly
        `
    }

    reverse_proxy 127.0.0.1:8080
}

Leave SecRuleEngine DetectionOnly for 24 to 72 hours of production traffic, review anomaly-score events in the JSON audit log, add exclusions for legitimate false positives, and only then switch to SecRuleEngine On. Honestly, I've been burned by skipping this step on a client edge; the alerts you catch in that window are almost never the ones you predict.

How do you install Coraza on Nginx?

There are three paths on Nginx today. Pick the one that matches your risk tolerance and your build pipeline.

  1. OpenResty + lua-resty-coraza (recommended for production). Coraza runs as a Lua library inside OpenResty's request handlers. Stable, well-tested, and used at scale.
  2. coraza-proxy-wasm behind Envoy or Nginx-Wasm. Sidecar Coraza as a Wasm filter, keep Nginx as the plain reverse proxy. Cleanest separation of concerns, minor per-request overhead.
  3. coraza-nginx experimental C connector. Compiles a dynamic Nginx module against libcoraza. Not yet a 1.0 release, so treat it as beta.

OpenResty + lua-resty-coraza recipe

# Install OpenResty on Ubuntu 24.04
wget -qO - https://openresty.org/package/pubkey.gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/openresty.gpg
echo "deb [signed-by=/etc/apt/keyrings/openresty.gpg] \
  http://openresty.org/package/ubuntu noble main" | \
  sudo tee /etc/apt/sources.list.d/openresty.list
sudo apt-get update
sudo apt-get install -y openresty openresty-opm

# Fetch the Coraza Lua binding (wraps libcoraza-c)
sudo opm get jcmoraisjr/lua-resty-coraza

# Minimal nginx.conf hook
cat <<'CONF' | sudo tee /etc/openresty/waf.conf
init_by_lua_block {
    local coraza = require "resty.coraza"
    coraza.new_waf({
        directives = [[
            Include /etc/coraza/coraza.conf-recommended
            Include /etc/coraza/crs-setup.conf
            Include /etc/coraza/rules/*.conf
            SecRuleEngine DetectionOnly
        ]]
    })
}

access_by_lua_block {
    require("resty.coraza").exec()
}
CONF

The ModSecurity v3 baseline on Nginx

For contrast, here's the still-supported ModSecurity path. This is what I keep running on legacy edges that already trust the connector:

# RHEL 9 / Alma 9 / Rocky 9 (EPEL has libmodsecurity3 3.0.14)
sudo dnf install -y epel-release
sudo dnf install -y libmodsecurity3 nginx-mod-http-modsecurity

# Load rules
sudo mkdir -p /etc/nginx/modsecurity.d
sudo curl -fsSL \
  https://github.com/coreruleset/coreruleset/archive/refs/tags/v4.15.0.tar.gz \
  | sudo tar -xz --strip-components=1 -C /etc/nginx/modsecurity.d

# Enable in the server block
sudo tee /etc/nginx/conf.d/waf.conf >/dev/null <<'CONF'
modsecurity on;
modsecurity_rules_file /etc/nginx/modsecurity.d/modsecurity.conf;
CONF

sudo nginx -t && sudo systemctl reload nginx

The rule bodies are byte-identical to what Coraza consumes; only the engine, connector, and audit-log format differ. That's why moving between the two engines is a configuration exercise, not a re-authoring exercise.

Migrating SecLang rules and CRS exclusions

Migration boils down to five moves. I've shipped this playbook against about a dozen production estates:

  1. Snapshot the current CRS version. Note the exact CRS release, the paranoia level, and every SecRule ... !@rx-style exclusion you have in REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.
  2. Upgrade to CRS 4.15 on the old engine first. If you're still on CRS 3.x, upgrade in place before touching the engine, otherwise you can't tell whether a new alert came from Coraza or from the new rules.
  3. Stand up Coraza in DetectionOnly mode next to the live path (a canary Caddy edge or a shadow Envoy filter). Ship both audit logs to the same SIEM.
  4. Diff the alerts. Expect a handful of false positives from Coraza's stricter multipart parser and from CRS rules that used a ModSecurity-specific transformation. Add exclusions until Coraza is quiet on legitimate traffic.
  5. Cut over. Flip SecRuleEngine On on the Coraza edge, drain the old engine, keep the ModSecurity node as rollback for 7 to 14 days.

The one class of rules that doesn't port cleanly is anything using the Lua @inspectFile operator with a custom Lua script, because Coraza's Lua support is still under active development. For those, port the logic to a native SecLang rule chain or move the check upstream.

Tuning paranoia level and the anomaly-scoring model

OWASP CRS 4.x uses paranoia levels (PL) 1 through 4 and an anomaly-scoring model. PL1 is the default and catches the OWASP Top 10 with a low false-positive rate. PL2 adds stricter rules for financial or healthcare-grade sites. PL3 and PL4 catch exotic attacks, but they produce enough noise that they only make sense once you have a mature exclusion set.

Set the level and thresholds in crs-setup.conf:

SecAction \
    "id:900000,\
     phase:1,\
     nolog,\
     pass,\
     t:none,\
     setvar:tx.blocking_paranoia_level=1,\
     setvar:tx.detection_paranoia_level=2,\
     setvar:tx.inbound_anomaly_score_threshold=5,\
     setvar:tx.outbound_anomaly_score_threshold=4"

The blocking_paranoia_level is what actually blocks; detection_paranoia_level logs additional rules for you to review before you promote them to blocking. Raising the inbound threshold from 5 to 7 is the correct first response to over-blocking, not disabling rules.

For deep guidance on tuning specifically for high-traffic production edges, the OWASP CRS documentation has an exhaustive false-positive tuning chapter that covers per-endpoint exclusions, cookie exclusions, and per-parameter unwrapping.

Regression-testing WAF rules with FTW

The Framework for Testing WAFs (FTW) is a Go-based CLI that fires HTTP requests at your WAF and asserts on both status codes and log lines. Every serious CRS deployment I've seen wires it into CI. Coraza's own CI runs FTW against every commit, so you know the test suite works against both engines.

# Install go-ftw
go install github.com/coreruleset/go-ftw@latest

# Run the CRS 4.15 regression tests against your WAF endpoint
~/go/bin/go-ftw run \
  --dir /etc/coraza/tests/regression/tests \
  --config /etc/coraza/tests/regression/ftw.yml

Add a GitHub Actions or GitLab CI job that stands up your Caddy+Coraza container and runs go-ftw on every pull request that touches the rule set. If a change lowers the anomaly score of a known-bad payload below the threshold, the pipeline fails before the change reaches production.

Observability: EVE-style logs, JSON audit, and SIEM shipping

A WAF is only useful if you can see what it did. Coraza writes an audit log in JSON that looks structurally similar to Suricata's EVE format, which means the same Vector, Fluent Bit, or Filebeat pipeline you already use for network IDS logs can carry WAF events. The Coraza release notes track the audit-log schema between versions.

# Coraza JSON audit sink (in coraza.conf)
SecAuditEngine RelevantOnly
SecAuditLog /var/log/coraza/audit.json
SecAuditLogFormat JSON
SecAuditLogParts ABIJDEFHZ
SecAuditLogType Serial

Ship those events into the same pipeline you use for other host telemetry. If you've already followed the Linux auditd SIEM integration guide, the JSON events drop into the same index and correlate cleanly by host.ip and source.ip. Correlating a CRS 942100 (SQL injection) alert against an auditd execve event on the same host is how you catch a WAF bypass; the WAF flagged the probe, and auditd confirms nothing landed on disk.

For layered defence beyond the WAF itself, plug a bot-scoring layer such as CrowdSec in front of the reverse proxy. Coraza handles L7 payload inspection; CrowdSec handles reputation and rate-based decisions. The two make each other's false-positive rate lower.

Frequently Asked Questions

Is Coraza production ready?

Yes. Coraza 3.x has been production-ready since 2023 and is used by Traefik Labs, Kong, and the OWASP CRS project's own CI. The 3.3 release from July 2026 adds full CRS 4.15 support and improved performance under multipart bodies.

Does Coraza support the same rules as ModSecurity?

Almost all of them. Coraza implements SecLang, so directives, phases, transformations, and operators from ModSecurity v3 work directly. The exceptions are a small set of native-Lua operators and a handful of legacy directives that Coraza treats as no-ops with a warning.

How do I migrate custom ModSecurity rules to Coraza?

Copy the *.conf files verbatim, adjust the include paths in Coraza's engine config, and boot the WAF in DetectionOnly. Diff the audit logs of the two engines under identical traffic and add exclusions for the small set of parser-difference alerts before enabling enforcement.

What is the paranoia level in OWASP CRS?

Paranoia level is a knob from 1 to 4 that enables progressively stricter rule sets. PL1 covers the OWASP Top 10 with minimal false positives, PL2 tightens input validation, and PL3 and PL4 add exotic checks that require significant tuning. Start at PL1 and only raise it after your exclusions are stable.

Is Coraza faster than ModSecurity?

In my benchmarks Coraza uses roughly 30 to 40% less resident memory per worker and matches ModSecurity on request latency for CRS 4.15 workloads. Actual throughput depends heavily on rule count and body-inspection settings, so run go-ftw against both engines with your own ruleset before deciding.

About the Author Editorial Team

Our team of expert writers and editors.