How to Set Up Coraza WAF with Traefik on k3s Kubernetes

Complete guide to deploying Coraza Web Application Firewall as a Traefik plugin on k3s Kubernetes. Protect your cluster from SQL injection, XSS, path traversal, and OWASP Top 10 attacks with zero additional pods.

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


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:

  1. Native Traefik plugin - Coraza runs inside Traefik’s process as a WebAssembly (WASM) module. No sidecars, no additional pods, no extra reverse proxies.
  2. ModSecurity compatible - Uses the same SecRule directive language, so existing ModSecurity knowledge transfers directly.
  3. Lightweight - Adds roughly 1-5ms of latency and ~50m CPU per request. Perfect for resource-constrained k3s clusters.
  4. 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
        Internet Traffic
     ┌────────▼─────────┐
     │     Traefik       │
     │  (k3s built-in)   │
     │                   │
     │  ┌─────────────┐  │
     │  │ Coraza WAF  │  │  ← WASM plugin (in-process)
     │  │ (SecRules)  │  │
     │  └──────┬──────┘  │
     │         │         │
     └────────┬──────────┘
     ┌────────▼─────────┐
     │  Backend Services │
     │  (your apps)      │
     └───────────────────┘

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)
  • kubectl configured and connected to your cluster
  • Familiarity with Kubernetes Ingress and Traefik concepts
  • At least one service exposed via Ingress or IngressRoute

Verify your setup:

1
2
3
4
5
6
7
8
# Check k3s version
k3s --version

# Verify Traefik is running
kubectl get pods -n kube-system -l app.kubernetes.io/name=traefik

# Check Traefik version
kubectl get deployment traefik -n kube-system -o jsonpath='{.spec.template.spec.containers[0].image}'

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 SolutionHow It WorksProsCons
Coraza (WASM plugin)Runs inside Traefik as a WASM moduleZero extra pods, native Traefik integration, ModSecurity compatibleOWASP CRS rules not bundled in WASM build, younger project
ModSecurityRequires an Nginx or Apache sidecar proxy20+ years of battle testing, massive rule ecosystemHeavy - needs extra pods/sidecars, adds latency and complexity
CrowdSec AppSecTraefik bouncer plugin for IP reputation and behavioral detectionCollaborative threat intelligence, community blocklistsHTTP inspection is newer (2024), requires LAPI infrastructure, not a full WAF replacement
Cloudflare WAFCloud proxy (DNS-level)Fully managed, includes DDoS protectionCosts money, cannot protect internal traffic, vendor lock-in
AWS WAFTied to ALB/CloudFrontManaged rules, AWS-nativeOnly 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:

1
2
3
4
5
6
7
1. HTTP request arrives at Traefik
2. Traefik routes it to the matching Ingress rule
3. Before forwarding, Traefik passes the request through the Coraza middleware
4. Coraza evaluates the request against all SecRule directives
5a. If no rules match → request passes through to the backend
5b. If a rule matches in DetectionOnly mode → request passes through, match is logged
5c. If a rule matches in blocking mode → request is denied with 403 Forbidden

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
---
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: traefik
  namespace: kube-system
spec:
  valuesContent: |-
    ports:
      web:
        redirections:
          entryPoint:
            to: websecure
            scheme: https
            permanent: true
      websecure:
        tls:
          enabled: true

    additionalArguments:
      - "--providers.kubernetescrd.allowCrossNamespace=true"
      - "--experimental.plugins.coraza-http-wasm-traefik.modulename=github.com/jcchavezs/coraza-http-wasm-traefik"
      - "--experimental.plugins.coraza-http-wasm-traefik.version=v0.3.0"

    additionalVolumeMounts:
      - name: plugins-storage
        mountPath: /plugins-storage

    deployment:
      additionalVolumes:
        - name: plugins-storage
          emptyDir: {}

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 - not ports.web.http.redirections.entryPoint. The chart template adds the http prefix 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 in kube-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:

1
2
3
Plugins are disabled because an error has occurred.
error="unable to create plugins client: unable to create directory /plugins-storage/sources:
mkdir plugins-storage: read-only file system"

Apply and Verify

Apply the configuration:

1
kubectl apply -f k8s/releases/traefik/ssl.yml

This triggers a Traefik pod restart (approximately 10-30 seconds of ingress downtime). Wait for the rollout:

1
kubectl rollout status -n kube-system deployment/traefik

Verify the plugin loaded successfully:

1
kubectl logs -n kube-system -l app.kubernetes.io/name=traefik --tail=20 | grep -i plugin

You should see:

1
2
Loading plugins... plugins=["coraza-http-wasm-traefik"]
Plugins loaded. plugins=["coraza-http-wasm-traefik"]

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:

1
kubectl get deployment traefik -n kube-system -o yaml | grep allowCrossNamespace

Step 2 - Create the WAF Middleware

Understanding SecRule Directives

Coraza uses SecRule directives - the same language as ModSecurity. Each rule has this format:

1
SecRule VARIABLES "OPERATOR" "ACTIONS"

Where:

  • VARIABLES - What to inspect (e.g., REQUEST_URI, ARGS, REQUEST_BODY, REQUEST_HEADERS)
  • OPERATOR - How to match (e.g., @rx for regex, @streq for exact string match, @beginsWith)
  • ACTIONS - What to do when matched (e.g., deny,status:403, allow, log)

Example:

1
SecRule ARGS "@rx union.*select" "id:20001,phase:2,log,deny,status:403,msg:'SQL Injection'"

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 log
  • deny,status:403 - Blocks the request
  • allow,nolog - Permits the request and skips remaining rules (used for health check exclusions)

Create the Middleware Manifest

Create the directory and middleware file:

1
mkdir -p k8s/releases/waf

Create k8s/releases/waf/coraza-middleware.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: waf-coraza
  namespace: kube-system
spec:
  plugin:
    coraza-http-wasm-traefik:
      directives:
        # -- Engine mode --
        # DetectionOnly = log but don't block. Change to "On" after tuning.
        - "SecRuleEngine DetectionOnly"
        - "SecRequestBodyAccess On"
        - "SecRequestBodyLimit 8388608"
        - "SecResponseBodyAccess Off"
        - "SecDebugLog /dev/stdout"
        - "SecDebugLogLevel 1"

        # -- Health/readiness endpoint exclusions --
        - 'SecRule REQUEST_URI "@rx ^/health(z|check)?$" "id:10001,phase:1,allow,nolog"'
        - 'SecRule REQUEST_URI "@streq /ready" "id:10002,phase:1,allow,nolog"'
        - 'SecRule REQUEST_URI "@streq /ping" "id:10003,phase:1,allow,nolog"'
        - 'SecRule REQUEST_URI "@streq /livez" "id:10004,phase:1,allow,nolog"'
        - 'SecRule REQUEST_URI "@streq /readyz" "id:10005,phase:1,allow,nolog"'

        # =============================================
        # Inline WAF rules
        # =============================================

        # -- SQL Injection (SQLi) --
        - 'SecRule ARGS|ARGS_NAMES|REQUEST_BODY "@rx (?i)(\bunion\b.*\bselect\b|\bselect\b.*\bfrom\b|\binsert\b.*\binto\b|\bdelete\b.*\bfrom\b|\bdrop\b.*\btable\b|\bupdate\b.*\bset\b)" "id:20001,phase:2,log,deny,status:403,msg:''SQL Injection attempt''"'
        - 'SecRule ARGS|ARGS_NAMES|REQUEST_BODY "@rx (?i)(''|\b(or|and)\b\s+[\d''\"]=)" "id:20002,phase:2,log,deny,status:403,msg:''SQL Injection - boolean logic''"'
        - 'SecRule ARGS|ARGS_NAMES|REQUEST_BODY "@rx (?i)(\bexec\b|\bexecute\b|\bsp_|\bxp_|\bsleep\s*\(|\bbenchmark\s*\(|\bwaitfor\b)" "id:20003,phase:2,log,deny,status:403,msg:''SQL Injection - function call''"'
        - 'SecRule ARGS|ARGS_NAMES|REQUEST_BODY "@rx (;|\b(--|#|\/\*))" "id:20004,phase:2,log,deny,status:403,msg:''SQL Injection - comment/termination''"'

        # -- Cross-Site Scripting (XSS) --
        - 'SecRule ARGS|ARGS_NAMES|REQUEST_BODY "@rx (?i)(<script[\s>]|<\/script>|javascript\s*:|on\w+\s*=)" "id:20101,phase:2,log,deny,status:403,msg:''XSS - script injection''"'
        - 'SecRule ARGS|ARGS_NAMES|REQUEST_BODY "@rx (?i)(<img[^>]+onerror|<svg[^>]+onload|<iframe|<object|<embed|<applet)" "id:20102,phase:2,log,deny,status:403,msg:''XSS - HTML injection''"'
        - 'SecRule ARGS|ARGS_NAMES|REQUEST_BODY "@rx (?i)(document\.(cookie|location|write)|window\.(location|open)|eval\s*\(|alert\s*\()" "id:20103,phase:2,log,deny,status:403,msg:''XSS - DOM manipulation''"'

        # -- Path Traversal / Local File Inclusion (LFI) --
        - 'SecRule REQUEST_URI|ARGS|ARGS_NAMES "@rx (\.\.\/|\.\.\\\\)" "id:20201,phase:1,log,deny,status:403,msg:''Path traversal attempt''"'
        - 'SecRule REQUEST_URI|ARGS|ARGS_NAMES "@rx (?i)(\/etc\/passwd|\/etc\/shadow|\/proc\/self|\/var\/log)" "id:20202,phase:1,log,deny,status:403,msg:''LFI - sensitive file access''"'
        - 'SecRule REQUEST_URI|ARGS "@rx (?i)(\.htaccess|\.htpasswd|\.env|wp-config\.php|\.git\/)" "id:20203,phase:1,log,deny,status:403,msg:''LFI - config file access''"'

        # -- Remote Code Execution (RCE) / Command Injection --
        - 'SecRule ARGS|ARGS_NAMES|REQUEST_BODY "@rx (?i)(;\s*(ls|cat|id|whoami|uname|pwd|wget|curl|nc|bash|sh|python|perl|ruby|php)\b)" "id:20301,phase:2,log,deny,status:403,msg:''Command injection''"'
        - 'SecRule ARGS|ARGS_NAMES|REQUEST_BODY "@rx (\||\$\(|`)" "id:20302,phase:2,log,deny,status:403,msg:''Shell metacharacter injection''"'

        # -- Remote File Inclusion (RFI) --
        - 'SecRule ARGS|ARGS_NAMES "@rx (?i)(https?:\/\/|ftp:\/\/|php:\/\/|data:\/\/|expect:\/\/)" "id:20401,phase:2,log,deny,status:403,msg:''Remote File Inclusion attempt''"'

        # -- HTTP Protocol Violations --
        - 'SecRule REQUEST_METHOD "!@rx ^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)$" "id:20501,phase:1,log,deny,status:405,msg:''Invalid HTTP method''"'

        # -- Scanner / Bot Detection --
        - 'SecRule REQUEST_HEADERS:User-Agent "@rx (?i)(nikto|sqlmap|nmap|masscan|dirbuster|gobuster|wfuzz|nuclei|acunetix|nessus|burpsuite)" "id:20601,phase:1,log,deny,status:403,msg:''Security scanner detected''"'

        # -- Server-Side Request Forgery (SSRF) --
        - 'SecRule ARGS|ARGS_NAMES|REQUEST_BODY "@rx (?i)(127\.0\.0\.1|localhost|0\.0\.0\.0|169\.254\.169\.254|::1|10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+)" "id:20701,phase:2,log,deny,status:403,msg:''SSRF - internal IP access''"'

        # -- Log4Shell / JNDI Injection --
        - 'SecRule ARGS|ARGS_NAMES|REQUEST_BODY|REQUEST_HEADERS "@rx (?i)(\$\{.*j.*n.*d.*i.*:)" "id:20801,phase:2,log,deny,status:403,msg:''JNDI injection attempt''"'

Let’s walk through the key sections:

Engine configuration:

  • SecRuleEngine DetectionOnly - This is the most important setting. In DetectionOnly mode, 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 sequences
  • 20202 - 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 inline SecRule directives. These rules cover the most critical attack patterns from the OWASP Top 10.

Apply the Middleware

1
kubectl apply -f k8s/releases/waf/coraza-middleware.yml

Verify it was created:

1
kubectl get middleware -n kube-system waf-coraza
1
2
NAME         AGE
waf-coraza   5s

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  namespace: default
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod-issuer
    traefik.ingress.kubernetes.io/router.middlewares: kube-system-waf-coraza@kubernetescrd
spec:
  ingressClassName: traefik
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path: /
            pathType: ImplementationSpecific
            backend:
              service:
                name: my-app
                port:
                  number: 8080
  tls:
    - secretName: my-app-tls
      hosts:
        - myapp.example.com

The key line is:

1
traefik.ingress.kubernetes.io/router.middlewares: kube-system-waf-coraza@kubernetescrd

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: my-app
  namespace: default
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`myapp.example.com`)
      kind: Rule
      middlewares:
        - name: waf-coraza
          namespace: kube-system
      services:
        - name: my-app
          port: 8080
  tls:
    secretName: my-app-tls

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

1
traefik.ingress.kubernetes.io/router.middlewares: default-oauth2-proxy@kubernetescrd,kube-system-waf-coraza@kubernetescrd

IngressRoute (array):

1
2
3
4
5
middlewares:
  - name: oauth2-proxy
    namespace: default
  - name: waf-coraza
    namespace: kube-system

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

1
2
3
4
5
6
7
8
# Classic UNION SELECT injection
curl -s -o /dev/null -w "%{http_code}" "https://myapp.example.com/?id=1%20UNION%20SELECT%20username,password%20FROM%20users"

# Boolean-based injection
curl -s -o /dev/null -w "%{http_code}" "https://myapp.example.com/?id=1%20OR%201=1"

# Time-based injection
curl -s -o /dev/null -w "%{http_code}" "https://myapp.example.com/?id=1;%20SLEEP(5)"

Test XSS Detection

1
2
3
4
5
# Script tag injection
curl -s -o /dev/null -w "%{http_code}" "https://myapp.example.com/?q=<script>alert('xss')</script>"

# Event handler injection
curl -s -o /dev/null -w "%{http_code}" "https://myapp.example.com/?q=<img%20onerror=alert(1)>"

Test Path Traversal Detection

1
2
3
4
5
# Directory traversal
curl -s -o /dev/null -w "%{http_code}" "https://myapp.example.com/?file=../../../etc/passwd"

# Config file access
curl -s -o /dev/null -w "%{http_code}" "https://myapp.example.com/.env"

Test Scanner Detection

1
2
3
4
5
# Fake SQLMap user agent
curl -s -o /dev/null -w "%{http_code}" -A "sqlmap/1.0" "https://myapp.example.com/"

# Fake Nikto scanner
curl -s -o /dev/null -w "%{http_code}" -A "Nikto/2.1.6" "https://myapp.example.com/"

Verify Legitimate Traffic Still Works

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Normal page request - should return 200
curl -s -o /dev/null -w "%{http_code}" "https://myapp.example.com/"

# Health check - should return 200
curl -s -o /dev/null -w "%{http_code}" "https://myapp.example.com/healthz"

# Normal API call - should return 200
curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" \
  -d '{"name":"John","email":"[email protected]"}' \
  "https://myapp.example.com/api/users"

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:

1
kubectl logs -n kube-system -l app.kubernetes.io/name=traefik --tail=100 | grep -i "coraza\|waf\|SecRule"

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:

1
{app="traefik"} |= "coraza"

High-severity detections:

1
{app="traefik"} |= "coraza" |= "deny"

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:

1
2
3
4
# Change this line:
- "SecRuleEngine DetectionOnly"
# To:
- "SecRuleEngine On"

Apply the change:

1
kubectl apply -f k8s/releases/waf/coraza-middleware.yml

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:

ScenarioTriggered RuleExample
JSON APIs with SQL-like contentSQLi rules (20001-20004)A search field containing select from dropdown
Rich text editorsXSS rules (20101-20103)Users saving HTML content like <img src="...">
Webhooks with URLs in bodyRFI rule (20401)Slack/GitHub webhooks containing https:// URLs
GraphQL queriesSQLi rulesGraphQL query { user(id: 1) { name } }
Pipe characters in dataRCE 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:

1
2
3
4
5
6
7
8
# Exclude SQL injection rules for your API search endpoint
- 'SecRule REQUEST_URI "@beginsWith /api/v2/search" "id:10100,phase:1,nolog,allow,ctl:ruleRemoveById=20001-20004"'

# Exclude RFI check for webhook endpoints
- 'SecRule REQUEST_URI "@beginsWith /webhooks/" "id:10101,phase:1,nolog,allow,ctl:ruleRemoveById=20401"'

# Completely disable a specific rule globally
- "SecRuleRemoveById 20302"

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

  1. Always start in DetectionOnly mode. Never go straight to blocking. Run detection for at least 1-2 weeks while monitoring logs.

  2. Exclude health check endpoints. Kubernetes probes fire constantly. Without exclusions, they generate enormous log volumes and add unnecessary latency.

  3. Roll out gradually. Apply the WAF to staging or non-critical services first. Once validated, roll to production services one at a time.

  4. Use cross-namespace middleware. Define the WAF once in kube-system and reference it from all namespaces via allowCrossNamespace=true. Avoid duplicating middleware definitions.

  5. Pin the plugin version. Always specify an exact version (e.g., v0.3.0) rather than latest. Plugin updates could introduce breaking changes.

  6. Monitor latency impact. The WASM runtime adds 1-5ms per request. Monitor your p99 latency after enabling the WAF, especially on latency-sensitive APIs.

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

1
2
Plugins are disabled because an error has occurred.
error="unable to create directory /plugins-storage/sources: mkdir plugins-storage: read-only file system"

Fix: Add the plugins-storage emptyDir volume as shown in Step 1.

Middleware not found - 404 on all routes

1
2
invalid middleware "kube-system-waf-coraza@kubernetescrd" configuration:
invalid middleware type or middleware does not exist

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

1
2
Failed to initialize WAF: invalid WAF config from string:
failed to readfile: open @owasp_crs/crs-setup.conf.example: file does not exist

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:

1
2
3
4
5
6
7
ports:
  web:
    redirections: # NOT ports.web.http.redirections
      entryPoint:
        to: websecure
        scheme: https
        permanent: true

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 On to enable blocking
  • Consider adding CrowdSec as a complementary IP reputation layer
  • Set up a Grafana dashboard for WAF metrics visualization
comments powered by Disqus
Citizix Ltd
Built with Hugo
Theme Stack designed by Jimmy