Coraza WAF with OWASP CRS v4 on Envoy Gateway, the secure way
You deployed the WAF, the pods are green, the dashboard shows rule hits, and a SQL injection walks straight through with a 200. The WAF is working exactly as configured: it is watching, not blocking.
The short answer
Load coraza-proxy-wasm in Envoy Gateway with an EnvoyExtensionPolicy that pins the OCI image by digest and sets failOpen false. In its directives, include the recommended config, then SecRuleEngine On, then CRS setup and rules. The recommended config alone runs in DetectionOnly mode and logs attacks without blocking them.
On this page
What goes wrong
Coraza is an open source WAF engine that speaks the ModSecurity rule language. coraza-proxy-wasm packages it as an Envoy Wasm filter with the OWASP Core Rule Set (CRS) built in. Envoy Gateway loads it through an EnvoyExtensionPolicy.
The trap is in the first line most people copy. Include @recommended-conf
loads Coraza's recommended base config, and that config sets
SecRuleEngine DetectionOnly. Every rule runs, every match is logged, and
nothing is blocked. The logs look busy, so the WAF looks alive.
Two more traps sit in the policy itself. Without failOpen: false stated,
readers of the manifest cannot tell what happens when the filter fails to
load. And an image referenced by tag can change under you; the WAF you
tested is not guaranteed to be the one that runs.
What the docs say
Enable Coraza, attaching it to every transaction. Use detection only to start with, because that minimises the chances of post-installation disruption.
Source: coraza-proxy-wasm, coraza.conf-recommended.conf
Mind that a WAF is not a plug-and-play security solution. It requires a configuration and tuning tailored to the environment and traffic the WAF is meant to protect to be effective.
Source: coraza-proxy-wasm README
If it is set to false or not set (defaulting to false), the system blocks the traffic and returns an HTTP 5xx error.
Source: Envoy Gateway API reference, Wasm
The README shows the recommended include but not that it disables blocking; you only learn that by reading the included file. Neither project says which CRS version the filter embeds: coraza-proxy-wasm 0.6.0 carries CRS 4.14.0, while CRS itself is at 4.29.0 in September 2026.
The secure configuration
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyExtensionPolicy
metadata:
name: coraza-waf
namespace: edge
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: edge # every route on this Gateway
wasm:
- name: coraza-waf
rootID: coraza
# Fail closed: if the module cannot load, return 5xx instead of
# passing traffic uninspected. This is the default; state it anyway.
failOpen: false
code:
type: Image
image:
# coraza-proxy-wasm 0.6.0 (CRS 4.14.0), linux/amd64 image manifest,
# pinned by digest. Envoy Gateway checks the image against sha256.
url: ghcr.io/corazawaf/coraza-proxy-wasm@sha256:65d6009b9da2e8965e592a08b74a86725435fc01aa39c756dce0bd5ea64b3f4e
sha256: 65d6009b9da2e8965e592a08b74a86725435fc01aa39c756dce0bd5ea64b3f4e
config:
directives_map:
default:
- Include @recommended-conf # base config: body access, limits ...
- SecRuleEngine On # ... but it says DetectionOnly: override it
- SecResponseBodyAccess Off # request-side WAF; keeps memory down
- Include @crs-setup-conf # CRS defaults: PL 1, threshold 5
- Include @owasp_crs/*.conf # the rules
default_directives: defaultThe order matters: a directive later in the list overrides an earlier one,
so SecRuleEngine On must come after @recommended-conf.
Size the Envoy memory for it. Envoy runs one Wasm VM per worker thread, and each holds the full rule set. In the lab, with 4 worker threads, Envoy's allocated memory went from 6 MiB to 93 MiB when the policy was attached, and its heap from 12 MiB to about 285 MiB.
Prove it
The same filter on a standalone Envoy
The test in secure-tests/coraza-waf-owasp-crs-v4-envoy/run.sh downloads
coraza-proxy-wasm 0.6.0, checks its sha256, loads it into Envoy 1.39.1 with
the directives above, and sends five requests: two normal, three attacks
(SQL injection in the query, XSS in the query, SQL injection in a JSON body).
== 1. Include @recommended-conf, then CRS (no SecRuleEngine line)
GET / -> 200
GET /search?q=running+shoes -> 200
GET /?id=1%27%20OR%20%271%27%3D%271 -> 200
GET /?q=%3Cscript%3Ealert(1)%3C/script%3E -> 200
POST /api/login -> 200
== 2. The same, with SecRuleEngine On after the recommended config
GET / -> 200
GET /search?q=running+shoes -> 200
GET /?id=1%27%20OR%20%271%27%3D%271 -> 403
GET /?q=%3Cscript%3Ealert(1)%3C/script%3E -> 403
POST /api/login -> 403
== Why request 3 was blocked (Envoy log, trimmed)
[id "942100"] [msg "SQL Injection Attack Detected via libinjection"]
[id "949111"] [msg "Inbound Anomaly Score Exceeded in phase 1 (Total Score: 5)"]The first block is the trap: three attacks, three 200s. The second block is one directive later.
On a cluster
The policy above, unchanged, on a Gateway edge in front of an nginx
Service, tested from a pod in the cluster (plain HTTP in the lab):
$ kubectl get envoyextensionpolicy coraza-waf -n edge \
-o jsonpath='{range .status.ancestors[*].conditions[*]}{.type}={.status} {.message}{"\n"}{end}'
Accepted=True Policy has been accepted.
before the policy: 200 GET /
200 GET /?id=1%27%20OR%20%271%27%3D%271
with the policy: 200 GET /
403 GET /?id=1%27%20OR%20%271%27%3D%271
403 GET /?q=%3Cscript%3Ealert(1)%3C/script%3E
403 POST /api/login {"user":"admin' OR 1=1 --","pass":"x"}Fail closed, checked. The same policy with a wrong sha256:
Accepted=False Wasm: module downloaded from oci://ghcr.io/corazawaf/coraza-proxy-wasm@sha256:65d6009b9da2e8965e592a08b74a86725435fc01aa39c756dce0bd5ea64b3f4e has checksum 65d6009b9da2e8965e592a08b74a86725435fc01aa39c756dce0bd5ea64b3f4e, which does not match: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff.
500 GET /
500 GET /?id=1%27%20OR%20%271%27%3D%271And in the Envoy Gateway log:
setting 500 direct response in routes due to errors in EnvoyExtensionPolicy {"runner": "gateway-api", "policy": "edge/coraza-waf", "routes": ["httproute/edge/web/rule/0/match/0/www_example_com"], ...}A broken or tampered module takes the site down instead of letting traffic through uninspected. That is the right failure for a WAF, and it is why the status check belongs in your deploy pipeline.
On your own cluster, run the status check above, then:
curl -s -o /dev/null -w "%{http_code}\n" "https://www.example.com/?id=1%27%20OR%20%271%27%3D%271"
curl -s -o /dev/null -w "%{http_code}\n" "https://www.example.com/"What you should see: 403 for the first request and your normal status for
the second. A 200 on the first means the engine is in DetectionOnly or the
policy is not attached to the route that served you.
Mistakes people make
Trusting the dashboard
Rule hits in the log prove the rules run. They do not prove anything is blocked. Send a known attack and look at the status code.
Pinning by tag
coraza-proxy-wasm:0.6.0 can be re-pushed. Pin the digest in url and set
sha256 so Envoy Gateway refuses anything else. See
fail-closed Wasm filters.
Assuming the embedded CRS is current
The filter ships the CRS version it was built with. Check the ver
field in the logs (OWASP_CRS/4.14.0 here) and track CRS releases for rule
fixes you are missing.
Turning on response body inspection by default
@recommended-conf enables response body access. It costs memory and CPU on
every response and rarely helps an edge WAF. Turn it off unless you have a
rule that needs it.
Going to blocking mode on day one without a look at the logs
Blocking is the goal, and false positives are the price. Run the first week with the tuning approach on the paranoia levels page: blocking at PL 1, detection at PL 2, exclusions reviewed.
Checklist
- Pin the coraza-proxy-wasm image by digest in
urland setsha256. - Set
failOpen: falseexplicitly. - Put
SecRuleEngine OnafterInclude @recommended-conf. - Turn off
SecResponseBodyAccessunless a rule needs it. - Record the embedded CRS version and watch CRS releases.
- Check the policy status is
Accepted=True. - Send a known SQL injection and expect
403.
A WAF in DetectionOnly is a very well-read bystander. One directive makes it a bouncer.
H2-CPQE
Learn it on a live range
WAF, rate limiting and DDoS, in Edge and Post-Quantum Networking: a real host in your browser, and every objective checked on the machine.
Start freeThe Dome
Want it run for you?
The Dome puts post-quantum TLS, a WAF that blocks, signed DNS and a zero-trust mesh in front of your application. Tell us what you run.
See the Dome