-
Notifications
You must be signed in to change notification settings - Fork 8.5k
Description
Summary
Currently, the nginx-ingress-controller template processes ModSecurity rules before the configuration-snippet directive in location blocks. This prevents rate limiting from being applied before WAF rules, causing unnecessary ModSecurity processing on rate-limited requests and resulting in incorrect HTTP status codes (406 from OWASP CRS instead of 429 from rate limiting).
Current Behavior
In the generated nginx.conf, the order of directives in a location block is:
location ~* "^/" {
# ... various set directives ...
modsecurity_rules '
SecRuleEngine On
';
modsecurity_transaction_id "$request_id";
# ... proxy headers ...
limit_req zone=globalzone burst=1 nodelay;
# ... proxy_pass ...
}
This means:
- Request enters location block
- ModSecurity processes the request (potentially blocking with 406 if OWASP CRS rules match)
- Rate limiting is checked (but never reached if ModSecurity already blocked)
- Request is proxied to backend
Expected Behavior
Rate limiting should be evaluated before ModSecurity processing:
location ~* "^/" {
# ... various set directives ...
limit_req zone=globalzone burst=1 nodelay;
modsecurity_rules '
SecRuleEngine On
';
modsecurity_transaction_id "$request_id";
# ... proxy headers and proxy_pass ...
}
This would ensure:
- Rate limiting is checked first
- If rate limit exceeded, return 429 immediately
- Only non-rate-limited requests are processed by ModSecurity
- Reduced CPU usage (no WAF processing for rate-limited requests)
- Correct HTTP status codes
Use Case
When protecting applications with both rate limiting and ModSecurity/OWASP CRS:
Current Configuration:
Global ConfigMap:
data:
enable-modsecurity: "true"
enable-owasp-modsecurity-crs: "true"
modsecurity-snippet: |
SecRuleEngine Off
SecDefaultAction "phase:2,deny,status:406,log"
http-snippet: |
limit_req_zone $binary_remote_addr zone=globalzone:10m rate=10r/m;
limit_req_status 429;
Ingress Annotations:
metadata:
annotations:
nginx.ingress.kubernetes.io/modsecurity-snippet: |
SecRuleEngine On
nginx.ingress.kubernetes.io/configuration-snippet: |
limit_req zone=globalzone burst=1 nodelay;
Problem:
When a client exceeds the rate limit while sending malicious payloads (e.g., XSS attempts), they receive:
HTTP 406 (from OWASP CRS blocking the malicious payload)
Instead of HTTP 429 (rate limit exceeded)
The rate limit is never enforced because ModSecurity blocks the request first.
Why This Matters
Performance: Rate-limited requests shouldn't consume CPU cycles processing ModSecurity rules
Correctness: Rate-limited requests should return 429, not 406
DDoS Protection: Rate limiting is a first line of defense and should execute before expensive WAF processing
Monitoring: Proper status codes (429 vs 406) are important for metrics and alerting
Workaround Limitations
ModSecurity v3 Limitations
ModSecurity v3 removed persistent collections (initcol, setvar on collections), making rate limiting within ModSecurity impossible. The v2 solution would have been:
modsecurity_rules '
SecAction "id:10001,phase:1,pass,nolog,initcol:ip=%{REMOTE_ADDR},setvar:ip.counter=+1,expirevar:ip.counter=60"
SecRule IP:COUNTER "@gt 50" "id:10002,phase:1,deny,status:429"
';
This doesn't work in ModSecurity v3.
Template Ordering
The current template order in nginx.tmpl places ModSecurity before configuration-snippet. The relevant section is around line 1400-1500 in the location block.
Alternative Approaches Tried
✗ server-snippet with limit_req - doesn't work, needs location context
✗ Using built-in nginx.ingress.kubernetes.io/limit-rps - generates code after ModSecurity
✗ ModSecurity phase 1 blocking - no persistent storage in v3
Proposed Solution
Modify the location block template to process configuration-snippet before modsecurity_rules:
Current template order (simplified):
{{ if $location.ModSecurity.Enable }}
modsecurity_rules '
{{ $location.ModSecurity.Snippet }}
';
modsecurity_transaction_id "$request_id";
{{ end }}
{{ if $location.ConfigurationSnippet }}
{{ $location.ConfigurationSnippet }}
{{ end }}
Proposed template order:
{{ if $location.ConfigurationSnippet }}
{{ $location.ConfigurationSnippet }}
{{ end }}
{{ if $location.ModSecurity.Enable }}
modsecurity_rules '
{{ $location.ModSecurity.Snippet }}
';
modsecurity_transaction_id "$request_id";
{{ end }}
Most users don't use both ModSecurity and configuration-snippet together
Those who do likely want rate limiting to run first anyway
Can be mitigated with a feature flag for backward compatibility
Benefits:
Better performance (less CPU on rate-limited requests)
Correct HTTP status codes
Aligns with nginx best practices (cheap checks before expensive ones)
Enables proper DDoS protection with WAF
References
Current template: https://github.com/kubernetes/ingress-nginx/blob/main/rootfs/etc/nginx/template/nginx.tmpl
Custom template documentation: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/custom-template/
ModSecurity v3 limitations: https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v3.x)#Collections
Environment
Ingress-nginx version: latest
ModSecurity version: v3.0.14
OWASP CRS version: 4.15.0
Kubernetes version: 1.31
Metadata
Metadata
Assignees
Labels
Type
Projects
Status