ModSecurity on Ingress-NGINX: Enable the WAF and OWASP CRS
Quick fix
In the ingress-nginx ConfigMap (Helm:controller.config) set enable-modsecurity: "true" and enable-owasp-modsecurity-crs: "true".
That loads the OWASP CRS in DetectionOnly mode. To block, add modsecurity-snippet with SecRuleEngine On in the same ConfigMap.
The per-Ingress nginx.ingress.kubernetes.io/modsecurity-snippet annotation is rejected by default (Critical risk), so keep rules in the ConfigMap.
Upstream ingress-nginx was retired in March 2026, and Traefik’s ingress-nginx compatibility does not support ModSecurity, so plan a WAF replacement.On this page
The Kubernetes Ingress-NGINX controller ships with ModSecurity (libmodsecurity v3) and the OWASP Core Rule Set (CRS), which turns the controller into a Web Application Firewall (WAF). This guide shows how to enable it cluster-wide or per Ingress, how to move from detection to blocking, how to test and read the logs, and what the 2025 to 2026 changes to ModSecurity and ingress-nginx mean for you.
2026 status: ModSecurity, the CRS and ingress-nginx
Three things changed since this guide was first written:
- ingress-nginx is retired. The Kubernetes project announced the retirement in November 2025. Best-effort maintenance ended in March 2026 and the repository was archived on March 24, 2026, so there are no more upstream releases or security fixes. Existing installs keep running.
- ModSecurity moved to OWASP. Trustwave, the original sponsor, transferred ModSecurity to OWASP starting in January 2024 and ended its own support on July 1, 2024. The engine is still maintained as an OWASP project.
- The OWASP CRS is active. CRS 4.x is the current major line (v4.30.0 was the newest release at the time of this update). The build script on the final ingress-nginx main branch pins libmodsecurity v3.0.14 and CRS v4.22.0, so the rules inside your controller image are frozen at whatever your image shipped with.
What this means in practice:
- If you stay on ingress-nginx for now, ModSecurity keeps working, but you will not get new CRS rules or engine fixes through upstream images. Vendor builds (for example the hardened images RKE2 ships, or SUSE’s extended support for Rancher Prime) may continue to patch it; check your vendor.
- Traefik’s ingress-nginx provider does not support
enable-modsecurity(Traefik docs). If you migrate to Traefik, as new RKE2 v1.36+ clusters do by default, your WAF does not come along automatically. - The usual replacement is OWASP Coraza, a Go WAF engine that runs CRS rules. It is available as a Traefik Wasm plugin and as coraza-spoa for HAProxy (still marked experimental). A WAF at the edge (cloud WAF or CDN) is the other common option.
What ModSecurity does in ingress-nginx
There are three ways to use it:
- Default configuration (DetectionOnly). With only
enable-modsecurityset, ModSecurity runs the recommended configuration in DetectionOnly mode. It logs, it does not block. - OWASP Core Rule Set. Generic attack detection for SQL injection, XSS, local and remote file inclusion, remote code execution, scanner detection, session fixation and data leakage.
- Custom rules (snippet). Your own
SecRuledirectives and CRS tuning, either in the ConfigMap or per Ingress.
Start in DetectionOnly, watch the logs for false positives, tune, then switch to blocking.
Option 1: Enable ModSecurity and the CRS for the whole controller
With the upstream Helm chart, set this in your values:
controller:
config:
enable-modsecurity: "true"
enable-owasp-modsecurity-crs: "true"
# Switch from DetectionOnly to blocking, and log JSON audit records to stdout
modsecurity-snippet: |
SecRuleEngine On
SecRequestBodyAccess On
SecAuditEngine RelevantOnly
SecAuditLogParts ABIJDEFHZ
SecAuditLogFormat JSON
SecAuditLogType Serial
SecAuditLog /dev/stdoutOn RKE2, put the same controller.config block in a HelmChartConfig named rke2-ingress-nginx in kube-system (see RKE2 Ingress-NGINX for the full example).
The ConfigMap-level modsecurity-snippet is set by cluster admins, so it is not subject to the annotation risk checks described below. This is the safest place for WAF rules.
Note that enabling ModSecurity in the ConfigMap turns it on for every Ingress served by the controller. Disable it for a specific Ingress with nginx.ingress.kubernetes.io/enable-modsecurity: "false".
Option 2: Enable ModSecurity per Ingress
If you do not want a cluster-wide WAF, leave enable-modsecurity off in the ConfigMap and use annotations:
metadata:
annotations:
nginx.ingress.kubernetes.io/enable-modsecurity: "true"
nginx.ingress.kubernetes.io/enable-owasp-core-rules: "true"
nginx.ingress.kubernetes.io/modsecurity-transaction-id: "$request_id"enable-modsecurity and enable-owasp-core-rules are rated Low risk and work with default settings. modsecurity-transaction-id is rated High, which the default annotations-risk-level: High still accepts.
Why the modsecurity-snippet annotation is rejected
nginx.ingress.kubernetes.io/modsecurity-snippet is rated Critical in the annotation risk table. Since controller v1.12 the default annotations-risk-level is High, so the admission webhook rejects Critical annotations, and snippet annotations are also gated by allow-snippet-annotations, which defaults to false since v1.9. To use it you must set both in the ConfigMap:
controller:
config:
allow-snippet-annotations: "true"
annotations-risk-level: "Critical"Only do that if you trust everyone who can create Ingress objects, because snippets can inject arbitrary NGINX configuration (this is the class of problem behind CVE-2025-1974).
If you use both enable-owasp-core-rules and modsecurity-snippet on the same Ingress, only the snippet takes effect. Include the CRS yourself:
Include /etc/nginx/owasp-modsecurity-crs/nginx-modsecurity.confDeploy a test application
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: meow
spec:
replicas: 2
selector:
matchLabels:
app: meow
template:
metadata:
labels:
app: meow
spec:
containers:
- name: meow
image: registry.k8s.io/e2e-test-images/agnhost:2.39
args: ["netexec", "--http-port=8080"]
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: meow-svc
spec:
selector:
app: meow
ports:
- port: 80
targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: meow-ingress
annotations:
nginx.ingress.kubernetes.io/enable-modsecurity: "true"
nginx.ingress.kubernetes.io/enable-owasp-core-rules: "true"
spec:
ingressClassName: nginx
rules:
- host: meow.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: meow-svc
port:
number: 80
EOFThe old gcr.io/kubernetes-e2e-test-images/echoserver image used in many older guides is no longer available; agnhost netexec from registry.k8s.io is a maintained replacement.
Test the WAF
Find an address for the controller (a LoadBalancer IP, or a node IP where the controller binds ports 80 and 443) and send requests with the right Host header:
IP=<ingress-ip>
# Normal request: expect 200
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: meow.example.com' "http://$IP/"
# Path traversal attempt: expect 403 once SecRuleEngine is On
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: meow.example.com' "http://$IP/?file=../../../../etc/passwd"In DetectionOnly mode both requests return 200, and the second one appears in the audit log. After you set SecRuleEngine On, the second request returns 403 Forbidden because the CRS anomaly score passes the blocking threshold.
Read the logs
With SecAuditLog /dev/stdout, audit records are in the controller logs:
# Upstream chart
kubectl -n ingress-nginx logs deploy/ingress-nginx-controller | grep -i modsecurity
# RKE2
kubectl -n kube-system logs ds/rke2-ingress-nginx-controller | grep -i modsecurityIf you kept the default file-based audit log, it is written under /var/log/audit/ inside the controller container. List and read it with kubectl exec, for example kubectl -n ingress-nginx exec deploy/ingress-nginx-controller -- ls -R /var/log/audit/, or use the path you set in SecAuditLog.
Check that the configuration was rendered:
kubectl -n ingress-nginx exec deploy/ingress-nginx-controller -- grep -n -i modsecurity /etc/nginx/nginx.conf | headTuning false positives
- Raise the CRS paranoia level only after a quiet period at level 1.
- Remove a rule for one application instead of disabling the WAF:
SecRuleRemoveById 942100in the snippet for that Ingress (or by URI in the ConfigMap with aSecRule REQUEST_URIplusctl:ruleRemoveByIdaction). - Keep
SecRequestBodyAccess Ononly where you need body inspection; very large uploads add latency.
FAQ
Is ModSecurity dead?
No. Trustwave ended its support on July 1, 2024, but the project moved to OWASP and is still maintained. What ended is commercial support from Trustwave, and NGINX Inc’s own ModSecurity WAF module for NGINX Plus.
Does ingress-nginx still get new CRS rules?
Not from upstream. The project is archived, so the CRS version in your image does not change unless your vendor rebuilds it. Plan a move to a maintained WAF.
Does Traefik support ModSecurity annotations?
No. Traefik’s ingress-nginx provider lists enable-modsecurity as unsupported. Use the Coraza plugin (which runs CRS rules) or a WAF in front of the cluster.
Why does my Ingress fail with “annotation … is forbidden” or a risk level error?
You are using a Critical annotation such as modsecurity-snippet while annotations-risk-level is High (the default since v1.12). Move the rules to the ConfigMap, or raise the risk level if you accept the risk.