Introduction
The ingress controller is the gateway for all external traffic entering the cluster. If the gateway falls, every service behind it is at risk, so the security configuration of ingress-nginx is not an add-on but the front line of cluster security.
In particular, a series of CVEs reported in 2025 (the so-called IngressNightmare family) showed that abusing annotations and the admission webhook handling path enables code execution with controller privileges. Since the controller can usually read cluster-wide Secrets (including TLS certificates), compromising the controller can lead straight to compromising the cluster.
This article gathers, in one place, the hardening techniques to run ingress-nginx safely. From blocking dangerous annotations to WAF integration, mTLS, external auth, TLS policy, multi-tenant isolation, CVE response, and an operations checklist.
Disabling Snippet Annotations and RBAC
The first thing to lock down is snippet annotations. Annotations like configuration-snippet and server-snippet can inject arbitrary nginx config into the controller's nginx.conf, effectively providing a channel to execute arbitrary code with controller privileges.
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
allow-snippet-annotations: "false"
annotations-risk-level: "Critical"
strict-validate-path-type: "true"
In recent versions the default of allow-snippet-annotations is false. annotations-risk-level constrains the allowed scope of high-risk annotations; setting it to Critical blocks the most dangerous class.
The next key is RBAC. The permission to create an ingress is the permission to change routing, so it must be restricted to trusted subjects.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ingress-editor
namespace: team-a
rules:
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
Grant ingress permissions per namespace, and govern special cases that need snippets through a separate review process and a policy engine (e.g., OPA Gatekeeper, Kyverno).
ModSecurity / Coraza WAF Integration
A Web Application Firewall (WAF) blocks attacks such as SQL injection, XSS, and path traversal at the request stage. ingress-nginx supports a WAF via ModSecurity. Note that ModSecurity is gradually migrating to its successor engine, Coraza, but the operational concepts are similar.
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
enable-modsecurity: "true"
enable-owasp-modsecurity-crs: "true"
modsecurity-snippet: |
SecRuleEngine On
SecRequestBodyAccess On
Enabling the OWASP Core Rule Set (CRS) alongside it blocks common attack patterns broadly. Early in adoption it is safer to run in detection-only mode (DetectionOnly) instead of blocking (On) to filter out false positives. Once false positives are caught, add exceptions for specific rules and gradually switch to blocking mode.
Client-Certificate mTLS
For routes where the caller must be strongly restricted — internal services or partner APIs — apply client-certificate-based mutual TLS (mTLS). Register the trusted CA certificate as a Secret and enable verification with auth-tls annotations.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: secure-api
annotations:
nginx.ingress.kubernetes.io/auth-tls-verify-client: "on"
nginx.ingress.kubernetes.io/auth-tls-secret: "default/client-ca"
nginx.ingress.kubernetes.io/auth-tls-verify-depth: "1"
nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- api.example.com
secretName: api-tls
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 443
Setting auth-tls-verify-client to on rejects requests without a valid client certificate. Turning on pass-certificate-to-upstream lets the backend receive certificate info via headers to use for additional authorization.
external auth and oauth2-proxy
If you want to delegate authentication to an auth service outside the controller, use external auth. The endpoint specified by auth-url validates every request, and based on the result it allows or blocks. Combined with oauth2-proxy, you can enforce OIDC-based SSO at the ingress level.
metadata:
annotations:
nginx.ingress.kubernetes.io/auth-url: "https://oauth2-proxy.example.com/oauth2/auth"
nginx.ingress.kubernetes.io/auth-signin: "https://oauth2-proxy.example.com/oauth2/start?rd=https://$host$request_uri"
The variable notation used here (host, request_uri) are nginx variables and must only be used inside code fences so the MDX build does not break. external auth adds an extra round trip to every request, so consider caching via auth-cache options, and keep in mind that the availability of the auth service becomes the availability of the whole.
TLS Policy — Protocols and Ciphers
TLS configuration is tied directly to compliance. Explicitly block old protocols (TLS 1.0/1.1) and weak ciphers in the global ConfigMap.
data:
ssl-protocols: "TLSv1.2 TLSv1.3"
ssl-ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384"
ssl-prefer-server-ciphers: "true"
hsts: "true"
hsts-max-age: "31536000"
hsts-include-subdomains: "true"
Enabling HSTS forces the browser to always connect over HTTPS thereafter. Automatically issue and renew the certificates themselves with cert-manager to prevent expiry incidents.
Header Security
Security-related response headers can be added globally. In environments where snippets are disabled, use custom-http-errors or a separate header ConfigMap.
data:
hide-headers: "Server,X-Powered-By"
add-headers: "ingress-nginx/custom-headers"
apiVersion: v1
kind: ConfigMap
metadata:
name: custom-headers
namespace: ingress-nginx
data:
X-Frame-Options: "SAMEORIGIN"
X-Content-Type-Options: "nosniff"
Referrer-Policy: "strict-origin-when-cross-origin"
Hiding server-version exposure (Server, X-Powered-By) reduces the information given to attackers. Keep X-Frame-Options to prevent clickjacking and X-Content-Type-Options to block MIME sniffing on by default.
Defending Against Abuse with Rate Limit
Unauthenticated endpoints and login routes are targets for brute-force and DoS. Set a baseline defense with rate limit.
metadata:
annotations:
nginx.ingress.kubernetes.io/limit-rps: "10"
nginx.ingress.kubernetes.io/limit-connections: "5"
nginx.ingress.kubernetes.io/limit-whitelist: "10.0.0.0/8"
Exclude internal network ranges via whitelist and place conservative limits on externally exposed routes. If you need more sophisticated global limiting or bot blocking, combine with a WAF or a separate API Gateway.
CVE Response Operations
The lesson of the IngressNightmare family of CVEs is clear: keep the controller on the latest patched version, reduce the attack surface, and minimize privileges.
[ CVE response operations flow ]
1. version inventory ── know current controller version/image digest
2. patch monitoring ── subscribe to security advisories/release notes
3. fast upgrade ────── roll out quickly after canary/staging
4. shrink surface ──── disable snippets, restrict admission webhook exposure
5. least privilege ── govern Secret access via controller RBAC/network policy
6. detection ──────── monitor abnormal config changes/requests
In particular, protect the admission webhook with network policy so it is not directly reachable from outside, and narrow the scope of Secrets the controller can read as much as possible.
IP Access Control and the Real Client IP
For routes with a limited set of callers — admin consoles or internal tools — source-IP-based access control is effective.
metadata:
annotations:
nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8,192.168.0.0/16"
But for IP control to work properly, the controller must know the real client IP. Behind a cloud LB or CDN, the source IP the controller sees may be the proxy's IP. In that case, register the trusted proxy ranges and configure the controller to trust forwarded headers.
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
use-forwarded-headers: "true"
proxy-real-ip-cidr: "10.0.0.0/8"
enable-real-ip: "true"
Trusting forwarded headers is a double-edged sword. If you do not scope the trusted ranges narrowly, an attacker can forge X-Forwarded-For to bypass the IP whitelist, so always limit proxy-real-ip-cidr to your actual proxy ranges. When using proxy protocol (as with AWS NLB), enable use-proxy-protocol to preserve the original IP.
Audit Logging and Detection
Security includes detection, not just blocking. Structuring access logs lets you analyze anomalies in a SIEM.
data:
log-format-escape-json: "true"
log-format-upstream: '{"time":"$time_iso8601","remote_addr":"$remote_addr","status":"$status","request":"$request","upstream_status":"$upstream_status"}'
The variable notation used here are all nginx variables and are safe only inside code fences. JSON structured logs are useful for detecting abnormal status-code spikes, scanning of specific paths, and abnormal User-Agent patterns. Monitored together with WAF detection logs, they let you catch attack attempts early.
Multi-Tenant Isolation
If multiple teams share one cluster, ingress isolation matters. The key tools are IngressClass separation, namespace RBAC, and a policy engine.
| Isolation means | Purpose | Mechanism |
| --- | --- | --- |
| IngressClass separation | Separate per-team controllers | Controller per ingressClassName |
| Namespace RBAC | Restrict ingress-create permission | Role/RoleBinding |
| Policy engine | Block risky annotations | Kyverno/OPA Gatekeeper |
| Host validation | Prevent host hijacking | Restrict host via admission policy |
An example of blocking risky annotations (snippet, arbitrary hosts, etc.) with a policy engine follows.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: block-snippet-annotations
spec:
validationFailureAction: Enforce
rules:
- name: no-configuration-snippet
match:
any:
- resources:
kinds: ["Ingress"]
validate:
message: "configuration-snippet annotation is not allowed"
pattern:
metadata:
=(annotations):
X(nginx.ingress.kubernetes.io/configuration-snippet): "null"
Operations Checklist
Finally, here is a hardening checklist.
- allow-snippet-annotations is false and annotations-risk-level is set appropriately
- ingress-create permission is minimized via RBAC
- a policy engine enforces rules blocking risky annotations
- a WAF (ModSecurity/Coraza + CRS) runs at least in detection mode
- mTLS or external auth is applied on sensitive routes
- TLS allows only 1.2/1.3, weak ciphers blocked, HSTS enabled
- security headers added, server-version headers hidden
- rate limit applied on externally exposed routes
- controller on the latest patched version, admission webhook network-protected
- if multi-tenant, IngressClass/namespace isolation applied
Secret Management and Certificate Automation
The controller reads TLS certificates as Secrets. If a certificate expires, the whole service halts with TLS errors, so automation is stability. Automate issuance and renewal with cert-manager, and detect impending expiry via alerts.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: api-tls
namespace: default
spec:
secretName: api-tls
duration: 2160h
renewBefore: 360h
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- api.example.com
A sufficient renewBefore leaves room to retry even if a renewal fails. You should also minimize the Secrets the controller can read. If the controller can read Secrets in all namespaces, compromising it exposes cluster-wide certificates. Where possible, scope certificates to specific namespaces and narrow the controller's Secret access with RBAC.
Supply Chain and Image Security
The integrity of the controller itself is part of security. Use signed images from a trusted registry and pin by digest to prevent tampering.
example of pinning the image digest
image: registry.k8s.io/ingress-nginx/controller@sha256:abcdef...
Pinning the image by digest rather than a tag (such as latest) prevents the risk of the same tag changing to a different image. Enforce signature verification (e.g., cosign/sigstore) with a policy engine, and minimize privileges on the controller pod via securityContext.
example controller pod securityContext
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
readOnlyRootFilesystem and capability drop greatly reduce what an attacker can do even if the container is breached. Only NET_BIND_SERVICE is added because of binding privileged ports like 80/443.
Isolation with Network Policy
The last line of defense is network policy. Explicitly restrict traffic into and out of the controller to block lateral movement upon a breach.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ingress-nginx-restrict
namespace: ingress-nginx
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
policyTypes:
- Ingress
- Egress
ingress:
- from:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 443
egress:
- to:
- namespaceSelector: {}
ports:
- protocol: TCP
port: 8080
Exclude the admission webhook port from the ingress rules so it is not directly reachable from outside, and narrow egress to allow only the namespaces where backend services live. Network policy applies only if your CNI (Calico, Cilium, etc.) supports it.
Relationship with Gateway API
From a security angle too, the 2026 direction matters. The annotation/snippet model of ingress-nginx has structural limits — a wide security surface and difficult standardization. Now that the Ingress API is frozen and ingress-nginx is in maintenance mode, the successor standard Gateway API expresses security policy via standard CRDs and policy resources (e.g., BackendTLSPolicy, ReferenceGrant), making role separation and access governance clearer. Maintain the security of existing ingresses with the checklist above, but for new designs, hardening on top of Gateway API's policy model is safer long term.
Response Procedure When a Breach Is Suspected
Even with good hardening, the probability of a breach is not zero. Define a procedure in advance for when you see signs the controller is compromised (abnormal config changes, unknown snippets, unexpected outbound traffic).
[ ingress controller breach response flow ]
1. isolate ────── block the suspect pod's egress with network policy
2. preserve evidence ── capture pod logs/nginx.conf/event snapshots
3. assess impact ── estimate the scope of exposable Secrets/certificates
4. rotate credentials ── immediately rotate certificates/tokens the controller accessed
5. redeploy ───── recreate the controller from a verified digest image
6. post-analysis ── identify the entry path, strengthen policy/RBAC
The most important step is credential rotation. Assume the TLS certificates and tokens the controller could read are already exposed and rotate them all. This is why "minimizing the Secrets the controller reads" earlier is not just a best practice but a control that directly reduces blast radius during an incident.
A Regular Security Review Routine
Security is not a set-and-forget configuration but ongoing operations. Make the following a regular routine.
| Cadence | Check item |
| --- | --- |
| Always | Alerts on abnormal status codes/request patterns |
| Weekly | Review new CVEs/security advisories |
| Monthly | Review controller version updates |
| Quarterly | Audit RBAC/network policy/certificate scope |
Automating such a routine (e.g., version-drift alerts, policy-violation reports) lets you maintain the security level without relying on human attention. Security is ultimately a matter of operational culture, not a one-time setting.
Conclusion
The essence of ingress-nginx security hardening is to reduce the attack surface and minimize privileges. Disable dangerous snippets and govern them with RBAC and a policy engine, stack defensive layers with a WAF, mTLS, and external auth, build the fundamentals with TLS and header policy, and create an operations routine that responds to CVEs quickly. The controller is the gateway to the cluster, and the gateway's security is the security of the whole.
References
- ingress-nginx security guide: https://kubernetes.github.io/ingress-nginx/user-guide/security/
- ingress-nginx annotations: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/
- ingress-nginx ModSecurity: https://kubernetes.github.io/ingress-nginx/user-guide/third-party-addons/modsecurity/
- Kubernetes RBAC: https://kubernetes.io/docs/reference/access-authn-authz/rbac/
- OWASP Core Rule Set: https://owasp.org/www-project-modsecurity-core-rule-set/
- Kyverno: https://kyverno.io/docs/
- cert-manager: https://cert-manager.io/docs/
- Gateway API: https://gateway-api.sigs.k8s.io/
- oauth2-proxy: https://oauth2-proxy.github.io/oauth2-proxy/
- Kubernetes Ingress concept: https://kubernetes.io/docs/concepts/services-networking/ingress/
현재 단락 (1/251)
The ingress controller is the gateway for all external traffic entering the cluster. If the gateway ...