Edge and WAF

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.

Updated Houssam Hammoudi, CTOTested with coraza-proxy-wasm 0.6.0 (OWASP CRS 4.14.0), Envoy Gateway v1.9.1, Envoy 1.39.1, Kubernetes 1.34 (kind)

On this page
  1. What goes wrong
  2. What the docs say
  3. The secure configuration
  4. Prove it
  5. Mistakes people make
  6. Checklist

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

yaml
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: default

The 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).

text
== 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):

text
$ 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:

text
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%271

And in the Envoy Gateway log:

text
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:

bash
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 url and set sha256.
  • Set failOpen: false explicitly.
  • Put SecRuleEngine On after Include @recommended-conf.
  • Turn off SecResponseBodyAccess unless 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 free

The 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