Scenario 03 · WAF for regulated apps

"The auditor wants a WAF. Finance won't sign the WAF bill."

A public storefront handles cardholder data, so PCI-DSS 6.4.1 mandates a web application firewall in front of it. The team doesn't want to rack an appliance, run yet another sidecar, or pay a cloud-WAF vendor per million requests. They need the OWASP Top 10 blocked at the edge — auditable, in the request path, with a metric to prove it's on.

Architecture

Attacks bounce off; clean traffic flows

SQL injection ?id=1' OR '1'='1 Reflected XSS <script>alert(1)</script> Path traversal /../../etc/passwd Legitimate request GET /products/42 ZenoIngress Coraza WAF OWASP CRS proxy-wasm · wasmtime SecRuleEngine On inspect: hdr · uri · body waf_blocked_total ↑ storefront clean traffic only ✗ 403 scanned ✓ pass

The WAF runs as a WebAssembly module inside the proxy — no extra hop. A bug in a rule is contained by the wasmtime sandbox; it cannot corrupt the data plane.

The setup

Load the CRS, then attach it to a route

A WafPolicy points at the bundled Coraza module and loads the OWASP Core Rule Set in order. Request-body inspection is opt-in (it costs latency), and detectionOnly lets you roll out in log-only mode first:

wafpolicy.yaml zenoingress.io/v1alpha1
apiVersion: zenoingress.io/v1alpha1
kind: WafPolicy
metadata:
  name: storefront-waf
  namespace: default
spec:
  # Coraza proxy-wasm module shipped inside the proxy image
  modulePath: /waf/coraza.wasm
  rules:                              # loaded in order
    - "Include @recommended-conf"
    - "Include @crs-setup-conf"
    - "Include @owasp_crs/*.conf"
    - "SecRuleEngine On"
  inspectRequestBody: true
  maxRequestBodyBytes: 131072         # 128 KiB cap
  detectionOnly: false                # true = log only (rollout)

Attach it to the storefront route with an ExtensionRef filter — the WAF now runs before anything is forwarded:

route.yaml gateway.networking.k8s.io/v1
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: storefront
  namespace: default
spec:
  parentRefs:
    - name: zeno-gateway
  hostnames:
    - shop.example.com
  rules:
    - matches:
        - path: { type: PathPrefix, value: / }
      filters:
        - type: ExtensionRef
          extensionRef:
            group: zenoingress.io
            kind: WafPolicy
            name: storefront-waf
      backendRefs:
        - name: storefront
          port: 80

What happens

The attack walkthrough

  1. A request arrives. Before routing, the WAF filter runs inside the proxy-wasm sandbox — no network hop, no sidecar.
  2. Coraza evaluates the OWASP CRS against the request headers and URI — and, with inspectRequestBody: true, the body up to 128 KiB.
  3. The SQLi probe trips CRS rule 942100; the request is rejected with 403 and zenoingress_waf_blocked_total{rule_id="942100"} increments. blocked
  4. The XSS and traversal probes trip their own CRS rules and are blocked the same way — the backend never sees them.
  5. GET /products/42 matches no rule, so it's forwarded untouched to the storefront. 200 · forwarded
  6. Rollout: start with detectionOnly: true, watch the metrics, silence false positives with SecRuleRemoveById, then flip to enforce.

Outcome

PCI-DSS satisfied, no appliance

OWASP CRSindustry-standard rule set
0 hopsin-process, no appliance
PCI-DSS 6.4.1WAF in the request path
Why ZenoIngress

A real Coraza + OWASP CRS engine runs inside the ingress in a memory-safe wasm sandbox — the auditor gets a WAF in the request path with a metric to prove it, and finance never sees a per-request WAF bill.

Run this yourself

Throw an attack, watch the 403

Apply the WafPolicy and route, then send a textbook SQL-injection probe and a clean request, and read the counter off the metrics endpoint:

terminal — prove the WAF is enforcing
$ # SQL injection probe
$ curl -s -o /dev/null -w "%{http_code}\n" \
      "https://shop.example.com/items?id=1'%20OR%20'1'='1"
403

$ # legitimate request
$ curl -s -o /dev/null -w "%{http_code}\n" https://shop.example.com/products/42
200

$ # the auditor's proof: blocks counted by rule id
$ curl -s http://127.0.0.1:9090/metrics | grep waf_blocked_total
zenoingress_waf_blocked_total{rule_id="942100"} 1

A WAF in the request path, not on the invoice.

Coraza + OWASP CRS, in-process. Free for one production cluster.

Deploy Free