Earlier this month I pushed the first commit to coraza-kubernetes-operator, a Kubernetes operator for managing OWASP Coraza Web Application Firewalls declaratively. This post covers where it came from, why I built Portkullis in Rust first, how the operator works, and the testing and release infrastructure.
WAFs have been around for decades, but deploying one on Kubernetes still involves too much glue. You bolt ModSecurity onto an ingress controller and hope the config stays in sync, or hand-manage sidecar proxies. Neither is declarative. Neither integrates with Gateway API. Neither gives platform teams a clean API boundary between “what to protect” and “how to protect it.”
People keep asking for it. I found two pieces that almost fit: Coraza, a Go-based, ModSecurity-compatible WAF engine running the OWASP CRS with full SecLang support; and coraza-proxy-wasm, a WebAssembly filter embedding Coraza in Envoy’s filter chain. The engine existed. The enforcement mechanism existed. The control plane was missing.
Before committing to a full operator, I needed to answer a harder question: could a WAF do more than signature matching? I built Portkullis in Rust with two detection engines: traditional signatures with ModSecurity rule compatibility, and anomaly detection using ML for traffic analysis. Rust was the natural choice: WAF inspection is hot-path code where latency budgets are microseconds. Zero-cost abstractions, no GC pauses, and memory safety without trading performance for correctness.
Portkullis proved three things. First, Rust’s type system catches entire classes of parsing bugs at compile time (the kind that become CVEs in C-based WAFs). Second, a dual-engine architecture is viable without blowing the latency budget. Third, the WASM compilation target means Rust detection logic can run inside Envoy via proxy-wasm with no extra network hops.
The operator follows the standard Kubernetes controller pattern with a cache layer between rule compilation and enforcement.
Custom Resources. Four CRDs:
Cache server. RuleSets compile their rules and store results in a cache. Engines poll by namespace and name, decoupling rule authoring from enforcement. Rules update without restarting the proxy.
Dataplane: proxy-wasm on Envoy. Traffic inspection happens inside Envoy via coraza-proxy-wasm. On Istio, the operator creates a WasmPlugin resource; the Envoy sidecar loads the filter and inspects every inbound request before it reaches the application. The CRS is embedded in the Wasm binary, so baseline OWASP Top Ten protection works immediately.
This separation (operator for lifecycle, cache for distribution, Wasm for enforcement) keeps each layer independently testable and replaceable.
A WAF you cannot test is a WAF you cannot trust. The OWASP FTW suite has over 1,500 cases exercising every CRS rule. Each test sends a crafted HTTP request and asserts the WAF’s response.
We integrated go-ftw into CI in PR #92. Initially non-enforcing. Issue #95 tracked hardening every failing case: 33 subtasks covering false positives from Envoy headers, timing-dependent log flushes, multipart parsing edge cases. Each investigated, each resolved.
The investment paid off immediately. During v0.3.0, seemingly innocuous rule ordering changes broke detection categories. The FTW suite caught the regressions before review started.
The operator is open source under Apache 2.0. If you run Istio or OpenShift and want declarative WAF management with CRS compliance, try the Dev Preview and open an issue.