A hands-on guide to protecting your k3s cluster with Coraza WAF - a lightweight, OWASP-compatible web application firewall that runs natively inside Traefik as a WASM plugin, with zero additional pods or sidecars.
Table of Contents
- Table of Contents
- Introduction
- Prerequisites
- WAF Alternatives for Kubernetes
- Understanding the Architecture
- Step 1 - Enable the Coraza Plugin in Traefik
- Step 2 - Create the WAF Middleware
- Step 3 - Attach the WAF to Your Services
- Step 4 - Testing the WAF
- Step 5 - Monitoring WAF Activity
- Step 6 - Switching to Blocking Mode
- Handling False Positives
- Production Best Practices
- Troubleshooting
- Conclusion
Introduction
What is a WAF?
A Web Application Firewall (WAF) is a security layer that sits between your users and your web applications. Unlike traditional firewalls that operate at the network level (blocking IPs, ports, and protocols), a WAF inspects the contents of HTTP requests - the headers, query parameters, request body, and URLs - looking for malicious patterns.
A WAF protects against application-layer attacks like:
- SQL Injection - Attackers inject SQL code through form fields or URL parameters to manipulate your database
- Cross-Site Scripting (XSS) - Malicious JavaScript injected into web pages to steal cookies or redirect users
- Path Traversal - Attempts to access files outside the web root using
../sequences - Remote Code Execution (RCE) - Injecting system commands through application inputs
- Server-Side Request Forgery (SSRF) - Tricking the server into making requests to internal services
These are all part of the OWASP Top 10, the industry-standard list of the most critical web application security risks.
Why Coraza?
Coraza is an open-source, OWASP-maintained web application firewall written in Go. It is a modern reimplementation of ModSecurity - the industry-standard WAF that has protected web applications for over 20 years.
What makes Coraza special for Kubernetes environments:
- Native Traefik plugin - Coraza runs inside Traefik’s process as a WebAssembly (WASM) module. No sidecars, no additional pods, no extra reverse proxies.
- ModSecurity compatible - Uses the same
SecRuledirective language, so existing ModSecurity knowledge transfers directly. - Lightweight - Adds roughly 1-5ms of latency and ~50m CPU per request. Perfect for resource-constrained k3s clusters.
- OWASP project - Backed by the OWASP Foundation, actively maintained, and community-driven.
What We Will Build
By the end of this guide, you will have:
- Coraza WAF running as a Traefik middleware plugin on your k3s cluster
- Protection rules covering SQL injection, XSS, path traversal, command injection, SSRF, and more
- The WAF applied to your services via a single Kubernetes annotation
- Monitoring and logging of WAF events
- A clear path from detection-only mode to full blocking
Here is what the architecture looks like:
| |
No sidecars, no extra pods - just Traefik with Coraza loaded as a plugin.
Prerequisites
Before starting, make sure you have:
- A running k3s cluster (v1.30+ recommended, which ships Traefik v3.x)
kubectlconfigured and connected to your cluster- Familiarity with Kubernetes Ingress and Traefik concepts
- At least one service exposed via Ingress or IngressRoute
Verify your setup:
| |
You should see Traefik v3.x. This guide is tested with Traefik 3.5.1 on k3s v1.32.
WAF Alternatives for Kubernetes
Before diving into Coraza, it is worth understanding the landscape of WAF options available for Kubernetes:
| WAF Solution | How It Works | Pros | Cons |
|---|---|---|---|
| Coraza (WASM plugin) | Runs inside Traefik as a WASM module | Zero extra pods, native Traefik integration, ModSecurity compatible | OWASP CRS rules not bundled in WASM build, younger project |
| ModSecurity | Requires an Nginx or Apache sidecar proxy | 20+ years of battle testing, massive rule ecosystem | Heavy - needs extra pods/sidecars, adds latency and complexity |
| CrowdSec AppSec | Traefik bouncer plugin for IP reputation and behavioral detection | Collaborative threat intelligence, community blocklists | HTTP inspection is newer (2024), requires LAPI infrastructure, not a full WAF replacement |
| Cloudflare WAF | Cloud proxy (DNS-level) | Fully managed, includes DDoS protection | Costs money, cannot protect internal traffic, vendor lock-in |
| AWS WAF | Tied to ALB/CloudFront | Managed rules, AWS-native | Only works with AWS load balancers, recurring costs |
For k3s with Traefik, Coraza is the clear winner. It runs in-process with zero overhead, no additional infrastructure, and uses the industry-standard SecRule language. If you also want IP reputation blocking, you can layer CrowdSec as a separate middleware in front of Coraza later.
Understanding the Architecture
How Coraza Works with Traefik
Traefik supports plugins through its experimental plugin system. When Traefik starts, it downloads the Coraza plugin from the Traefik Plugin Catalog and loads it as a WASM (WebAssembly) module.
The plugin registers itself as a middleware type. You then create a Traefik Middleware custom resource with your security rules (SecRule directives). When you attach this middleware to an Ingress or IngressRoute, every incoming HTTP request passes through Coraza’s rule engine before reaching your backend service.
The flow for each request:
| |
What Gets Protected
The WAF middleware inspects:
- Request URI - The URL path and query string
- Request headers - User-Agent, Content-Type, cookies, etc.
- Request body - POST data, JSON payloads, form submissions
- Request arguments - Query parameters and POST parameters
It does not inspect responses by default (we disable SecResponseBodyAccess for performance), but this can be enabled if needed.
Step 1 - Enable the Coraza Plugin in Traefik
Understand the k3s Traefik Configuration
k3s ships with Traefik as its default ingress controller. Unlike a standalone Traefik installation, k3s manages Traefik through a Helm chart that is applied automatically. To customize it, you create a HelmChartConfig resource - a k3s-specific custom resource that merges your values with the built-in chart defaults.
The important thing to know: k3s’s Traefik runs with readOnlyRootFilesystem: true for security. The Coraza plugin needs to download files on startup, so we must mount a writable volume at /plugins-storage.
Add the Plugin Configuration
Create or update the file k8s/releases/traefik/ssl.yml:
| |
Let’s break down each section:
ports.web.redirections - Redirects all HTTP traffic on port 80 to HTTPS on port 443. The permanent: true sends a 301 status code.
Important note about the Traefik Helm chart: The values path for redirections is
ports.web.redirections.entryPoint- notports.web.http.redirections.entryPoint. The chart template adds thehttpprefix internally when rendering the Traefik CLI arguments. This is a common gotcha.
ports.websecure.tls.enabled - Enables TLS on the HTTPS entry point.
additionalArguments - Three critical flags:
--providers.kubernetescrd.allowCrossNamespace=true- Allows Ingress resources in any namespace to reference a Middleware defined inkube-system. Without this, you would need to duplicate the WAF middleware in every namespace.--experimental.plugins.coraza-http-wasm-traefik.modulename=...- Tells Traefik to download the Coraza plugin from the plugin catalog. The module name must match exactly.--experimental.plugins.coraza-http-wasm-traefik.version=v0.3.0- Pins the plugin to a specific version. Check the GitHub releases for the latest.
additionalVolumeMounts and deployment.additionalVolumes - Mounts an emptyDir volume at /plugins-storage inside the Traefik container. This is essential because Traefik’s filesystem is read-only by default. Without this volume, plugin download fails with:
| |
Apply and Verify
Apply the configuration:
| |
This triggers a Traefik pod restart (approximately 10-30 seconds of ingress downtime). Wait for the rollout:
| |
Verify the plugin loaded successfully:
| |
You should see:
| |
If you see Plugins are disabled because an error has occurred, check that the plugins-storage volume is mounted correctly.
Verify the cross-namespace flag:
| |
Step 2 - Create the WAF Middleware
Understanding SecRule Directives
Coraza uses SecRule directives - the same language as ModSecurity. Each rule has this format:
| |
Where:
- VARIABLES - What to inspect (e.g.,
REQUEST_URI,ARGS,REQUEST_BODY,REQUEST_HEADERS) - OPERATOR - How to match (e.g.,
@rxfor regex,@streqfor exact string match,@beginsWith) - ACTIONS - What to do when matched (e.g.,
deny,status:403,allow,log)
Example:
| |
This rule:
- Inspects all request arguments (
ARGS) - Looks for the regex pattern
union.*select(a common SQL injection signature) - If matched: logs the event, denies the request with HTTP 403, and records the message “SQL Injection”
Key actions:
phase:1- Runs during request header processing (fast, use for URI/header checks)phase:2- Runs during request body processing (needed for POST data inspection)log- Writes to the audit logdeny,status:403- Blocks the requestallow,nolog- Permits the request and skips remaining rules (used for health check exclusions)
Create the Middleware Manifest
Create the directory and middleware file:
| |
Create k8s/releases/waf/coraza-middleware.yml:
| |
Let’s walk through the key sections:
Engine configuration:
SecRuleEngine DetectionOnly- This is the most important setting. InDetectionOnlymode, the WAF logs every rule match but does not block any traffic. This lets you observe what the WAF would catch without risking false positives breaking your applications. Start here, always.SecRequestBodyAccess On- Enables inspection of POST request bodies (required for detecting SQLi in form submissions).SecRequestBodyLimit 8388608- Limits inspected body size to 8MB. Adjust based on your largest expected payload.SecResponseBodyAccess Off- We skip response inspection to reduce latency overhead.
Health check exclusions (IDs 10001-10005):
Kubernetes health probes (/healthz, /ready, /livez, etc.) fire constantly. These rules use allow,nolog to bypass all WAF processing for these paths - reducing overhead and preventing noise in your logs.
SQL Injection rules (IDs 20001-20004):
Four layers of SQL injection detection:
20001- Catches classic SQL keywords combinations (UNION SELECT,INSERT INTO,DROP TABLE)20002- Detects boolean-based injection (' OR 1=1,AND "a"="a")20003- Blocks SQL functions commonly used in attacks (SLEEP(),BENCHMARK(), stored procedures)20004- Catches SQL comment sequences (--,#,/*) often used to terminate injected queries
XSS rules (IDs 20101-20103):
20101- Detects<script>tags,javascript:URIs, and inline event handlers (onclick=,onerror=)20102- Catches HTML elements commonly used in XSS (<img onerror=,<svg onload=,<iframe>)20103- Blocks DOM manipulation attempts (document.cookie,eval(),alert())
Path traversal rules (IDs 20201-20203):
20201- Catches../directory traversal sequences20202- Blocks access to sensitive Unix files (/etc/passwd,/proc/self)20203- Prevents access to configuration files (.env,.git/,wp-config.php)
SSRF rule (ID 20701):
Detects attempts to access internal network addresses through request parameters - a common technique to reach metadata services (like AWS 169.254.169.254) or internal APIs.
Why not OWASP CRS? The Coraza WASM plugin for Traefik does not bundle the OWASP Core Rule Set (CRS) files. The
Include @owasp_crs/...directives that work with standalone Coraza are not available in the WASM build. That is why we write inlineSecRuledirectives. These rules cover the most critical attack patterns from the OWASP Top 10.
Apply the Middleware
| |
Verify it was created:
| |
| |
Step 3 - Attach the WAF to Your Services
Now that the middleware exists in kube-system, you can attach it to any service in any namespace (thanks to allowCrossNamespace=true).
For Kubernetes Ingress Resources
Add the middleware annotation to your Ingress:
| |
The key line is:
| |
The format is <namespace>-<middleware-name>@kubernetescrd. Since our middleware is named waf-coraza in namespace kube-system, the reference is kube-system-waf-coraza@kubernetescrd.
For Traefik IngressRoute Resources
If you use Traefik’s native IngressRoute CRD instead of standard Kubernetes Ingress:
| |
Chaining with Existing Middleware
If your service already uses middleware (e.g., OAuth2 proxy, basic auth), you can chain multiple middlewares. They execute in the order listed.
Ingress annotation (comma-separated):
| |
IngressRoute (array):
| |
Step 4 - Testing the WAF
With the middleware applied to a service, let’s test the rules. Since we are in DetectionOnly mode, all requests will still pass through - but rule matches will be logged.
Replace myapp.example.com with your actual hostname.
Test SQL Injection Detection
| |
Test XSS Detection
| |
Test Path Traversal Detection
| |
Test Scanner Detection
| |
Verify Legitimate Traffic Still Works
| |
In DetectionOnly mode, all of these will return the normal HTTP status code from your backend. The difference is that malicious requests will generate log entries in Traefik’s output.
Step 5 - Monitoring WAF Activity
Viewing Traefik Logs
Check Traefik’s logs directly for WAF activity:
| |
Loki Queries for WAF Events
If you have Loki and Promtail set up (which collects container logs), you can query WAF events in Grafana:
All WAF detections:
| |
High-severity detections:
| |
Consider creating a Grafana dashboard with panels for:
- WAF detections per minute (time series)
- Top triggered rule IDs (table)
- Top source IPs triggering WAF rules (table)
Step 6 - Switching to Blocking Mode
After running in DetectionOnly mode for 1-2 weeks and confirming no false positives affect legitimate traffic, switch to blocking mode.
Edit the middleware:
| |
Apply the change:
| |
The change takes effect immediately - no Traefik restart needed. Coraza will now return HTTP 403 for any request matching a deny rule.
Rollback plan: If blocking causes issues, change SecRuleEngine On back to SecRuleEngine DetectionOnly and re-apply. The switch is instant.
Handling False Positives
Common False Positive Scenarios
When protecting real applications, you will encounter legitimate requests that trigger WAF rules:
| Scenario | Triggered Rule | Example | |
|---|---|---|---|
| JSON APIs with SQL-like content | SQLi rules (20001-20004) | A search field containing select from dropdown | |
| Rich text editors | XSS rules (20101-20103) | Users saving HTML content like <img src="..."> | |
| Webhooks with URLs in body | RFI rule (20401) | Slack/GitHub webhooks containing https:// URLs | |
| GraphQL queries | SQLi rules | GraphQL query { user(id: 1) { name } } | |
| Pipe characters in data | RCE rule (20302) | Log search queries containing ` | ` |
Adding Rule Exclusions
To exclude a specific rule for a specific path, add a targeted exclusion before the rule:
| |
Place these exclusion rules before the rules they disable in the directives list, or use ctl:ruleRemoveById to selectively disable rules for specific paths.
Production Best Practices
Always start in DetectionOnly mode. Never go straight to blocking. Run detection for at least 1-2 weeks while monitoring logs.
Exclude health check endpoints. Kubernetes probes fire constantly. Without exclusions, they generate enormous log volumes and add unnecessary latency.
Roll out gradually. Apply the WAF to staging or non-critical services first. Once validated, roll to production services one at a time.
Use cross-namespace middleware. Define the WAF once in
kube-systemand reference it from all namespaces viaallowCrossNamespace=true. Avoid duplicating middleware definitions.Pin the plugin version. Always specify an exact version (e.g.,
v0.3.0) rather thanlatest. Plugin updates could introduce breaking changes.Monitor latency impact. The WASM runtime adds 1-5ms per request. Monitor your p99 latency after enabling the WAF, especially on latency-sensitive APIs.
Plan for CrowdSec later. Coraza handles payload inspection (what is in the request). CrowdSec handles IP reputation (who is making the request). Together they provide comprehensive protection. When adding CrowdSec, chain it before Coraza so bad IPs are rejected before the more expensive rule evaluation runs.
Troubleshooting
Plugin fails to load - read-only filesystem
| |
Fix: Add the plugins-storage emptyDir volume as shown in Step 1.
Middleware not found - 404 on all routes
| |
Causes:
- The plugin did not load (check startup logs for
Plugins loaded) - The Middleware CRD was not applied (
kubectl get middleware -n kube-system) - Wrong middleware name in the annotation (check for typos)
WAF initialization error - CRS not found
| |
Cause: The WASM build of Coraza does not bundle OWASP CRS rule files. The Include @owasp_crs/... directives do not work.
Fix: Use inline SecRule directives as shown in this guide instead of Include statements.
HTTP to HTTPS redirect not working
If using the k3s Traefik Helm chart, the correct values path for redirections is:
| |
The Helm chart template adds the http nesting internally. Including http in your values causes the redirect to be silently ignored.
Services return 404 after adding WAF annotation
This happens when you add the middleware annotation before the plugin is loaded and the Middleware CRD is created. Traefik hard-fails the route when a referenced middleware doesn’t exist.
Fix: Always deploy in order: (1) update Traefik config, (2) apply middleware CRD, (3) then add annotations to services.
Conclusion
You now have a working Coraza WAF protecting your k3s services through Traefik. To recap what we built:
- Coraza WASM plugin loaded into Traefik with a writable plugin storage volume
- WAF middleware with inline SecRule directives covering SQL injection, XSS, path traversal, command injection, RFI, SSRF, scanner detection, and JNDI injection
- Cross-namespace middleware so all services reference a single WAF definition
- Health check exclusions to avoid unnecessary overhead on Kubernetes probes
- Detection-only mode for safe initial deployment with a clear path to blocking
The WAF runs entirely in-process - no sidecars, no extra pods, no external services. It adds minimal overhead to your resource-constrained k3s cluster while providing meaningful protection against the most common web application attacks.
Next steps:
- Monitor Traefik logs for 1-2 weeks to identify false positives
- Add exclusion rules for any legitimate traffic that triggers rules
- Switch to
SecRuleEngine Onto enable blocking - Consider adding CrowdSec as a complementary IP reputation layer
- Set up a Grafana dashboard for WAF metrics visualization