- Published on
ingress-nginx Deep Dive — Architecture, Annotations, and Templates
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- Introduction
- ingress-nginx vs the NGINX Inc. Controller — Distinguish Them First
- Dissecting the Internal Architecture
- Essential Annotation Catalog
- ConfigMap Global Tuning
- Snippet Annotations and Security Risks
- Maintenance Mode and the Gateway API Recommendation
- IngressClass and Multiple Controllers
- pathType and Path Matching
- Default Backend and Custom Error Pages
- Troubleshooting
- Installation and Deployment Topology
- Key Takeaways
- Conclusion
- References
Introduction
In Kubernetes, the most common way to route external traffic to in-cluster services is the Ingress resource, and among the controllers that actually implement Ingress, the overwhelmingly popular one is ingress-nginx. If your platform team has touched ingress even once, you have almost certainly met this component.
Yet operating it surfaces plenty of confusion. Search for "nginx ingress" and you get two different projects; you add an annotation and it does nothing; a single rewrite-target causes a flood of 404s; and using snippet annotations gets you flagged in a security review. On top of that, as of 2026 ingress-nginx has entered maintenance mode, the Ingress API itself is frozen, and Gateway API is taking over as the successor standard.
This article dissects the internals of ingress-nginx, catalogs the annotations you must know in practice, and covers global tuning, security risks, and the transition strategy ahead — all in one place.
ingress-nginx vs the NGINX Inc. Controller — Distinguish Them First
The first source of confusion to address is that there are actually two "nginx-based ingress controllers". The names are similar enough that mixing up documentation and applying a non-working annotation is extremely common.
| Aspect | ingress-nginx (community) | NGINX Ingress Controller (F5/NGINX Inc.) |
|---|---|---|
| Owner | Kubernetes project (SIG) | F5 NGINX (commercial vendor) |
| Repository | kubernetes/ingress-nginx | nginxinc/kubernetes-ingress |
| Annotation prefix | nginx.ingress.kubernetes.io | nginx.org / nginx.com |
| Config extension | Lua + templates, snippets | VirtualServer/VirtualServerRoute CRDs |
| License | Apache 2.0, fully open source | OSS edition + commercial NGINX Plus |
| Dynamic config | Lua-based, avoids reload | Plus uses API, OSS reloads |
The key is the annotation prefix. If you followed an internet tutorial and nothing works, nine times out of ten you applied an annotation from the other project. This article deals exclusively with the community ingress-nginx (prefix nginx.ingress.kubernetes.io).
Dissecting the Internal Architecture
An ingress-nginx pod performs two roles at once inside a single container. One is the controller process; the other is the nginx process that actually handles traffic.
┌──────────────────────────────────────────────┐
│ ingress-nginx Pod │
│ │
K8s API │ ┌────────────┐ ┌───────────────┐ │
─────────▶│ │ Controller │ watch │ nginx master │ │
(watch │ │ (Go) │────────▶│ + workers │ │
Ingress, │ │ │ reload/ │ │ │
Service, │ │ Lua sync │ Lua API │ Lua modules │ │
Endpoints)│ └────────────┘ └───────┬───────┘ │
│ │ │
└──────────────────────────────────┼───────────┘
│
┌────────────────▼────────────────┐
│ Upstream Pods (Endpoints) │
└──────────────────────────────────┘
What the Controller Process Does
The controller is written in Go and watches the Kubernetes API server. It observes changes to Ingress, Service, Endpoints (or EndpointSlices), Secrets, and ConfigMaps, and translates them into a configuration model that nginx understands. The model is applied through two paths.
- Structural changes (new hosts, new paths, TLS changes) re-render the nginx.conf template and reload nginx.
- Simple endpoint changes (pods scaling in/out) update upstreams dynamically through Lua, with no reload.
Lua and Dynamic Config — the Key to Avoiding Reloads
The biggest pain in traditional nginx operation was that every upstream change required a reload. In a Kubernetes environment where pods churn constantly, reloads spike, and during a reload connections can drop and memory usage surges.
ingress-nginx solves this with OpenResty-based Lua. The list of upstream endpoints is stored in Lua shared memory; when endpoints change, only the Lua data is updated rather than reloading nginx. Load-balancing decisions are also made dynamically at the balancer_by_lua phase. As a result, routine scaling almost never triggers a reload.
Reloads are still required in these cases.
[ reload happens ] [ no reload (Lua dynamic) ]
- new server/location added - pod scale in/out
- TLS certificate/Secret change - endpoint IP change
- ConfigMap global option change - simple weight update (some canary)
- snippet annotation change
Essential Annotation Catalog
The real power of ingress-nginx is in its annotations. Here are the frequently used ones organized by category. Every annotation prefix is nginx.ingress.kubernetes.io.
| Annotation | Purpose | Example value |
|---|---|---|
| rewrite-target | Rewrite the path | rewrite via capture group |
| ssl-redirect | Redirect HTTP to HTTPS | "true" |
| force-ssl-redirect | Force redirect even without TLS | "true" |
| backend-protocol | Specify backend protocol | HTTPS, GRPC |
| proxy-body-size | Max request body size | 50m |
| proxy-read-timeout | Backend read timeout (seconds) | "60" |
| canary | Enable canary ingress | "true" |
| canary-weight | Canary traffic percentage | "20" |
| affinity | Session affinity mode | cookie |
| whitelist-source-range | Source IP allow list | 10.0.0.0/8 |
rewrite-target and Regex Paths
This is the most commonly mishandled annotation. To strip part of the path before passing it to the backend, you must combine it with a regex capture group.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-rewrite
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/use-regex: "true"
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /svc(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: app-svc
port:
number: 80
Here a request to /svc/foo forwards only foo (the second capture group, (.*)) to the backend. The dollar notation in rewrite-target refers to an nginx regex capture and must only be used inside code fences so the MDX build does not break.
TLS Redirect and Backend Protocol
metadata:
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
Setting backend-protocol to HTTPS makes the controller connect to the backend over HTTPS. For a gRPC service you must set GRPC so the HTTP/2-based gRPC proxy works correctly.
Canary Deployments
ingress-nginx places a main ingress and a canary ingress on the same host/path simultaneously and splits traffic between them.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "20"
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-v2
port:
number: 80
Beyond weight, header/cookie-based branching is possible.
metadata:
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-by-header: "x-canary"
nginx.ingress.kubernetes.io/canary-by-header-value: "always"
ConfigMap Global Tuning
If annotations are per-ingress settings, the ConfigMap is the global configuration applied to the entire controller. Through the ConfigMap specified at controller deployment (usually ingress-nginx-controller), you control nginx options at the http block level.
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
use-gzip: "true"
gzip-level: "5"
proxy-body-size: "20m"
keep-alive: "75"
keep-alive-requests: "1000"
upstream-keepalive-connections: "320"
ssl-protocols: "TLSv1.2 TLSv1.3"
enable-real-ip: "true"
use-forwarded-headers: "true"
A ConfigMap change almost always triggers an nginx reload, so manage it carefully through GitOps rather than changing it frequently.
Snippet Annotations and Security Risks
The most powerful and most dangerous feature of ingress-nginx is the snippet annotation. It lets you inject an arbitrary nginx configuration fragment into an ingress.
metadata:
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
more_set_headers "X-Custom: value";
The problem is that this snippet is rendered verbatim into the controller's nginx.conf. Anyone with permission to create an Ingress resource can inject arbitrary configuration with the privileges of the controller process. In a multi-tenant cluster this becomes a privilege-escalation path.
In fact, a series of CVEs reported in 2025 (the so-called IngressNightmare family) showed that abusing the admission webhook handling and snippet injection enabled code execution with controller privileges. As a result, ingress-nginx changed its defaults to be conservative.
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
allow-snippet-annotations: "false"
annotations-risk-level: "Critical"
In recent versions the default of allow-snippet-annotations is false. If you truly need snippets, lock down RBAC tightly so only trusted operators can create ingresses, and constrain the allowed scope with annotations-risk-level.
Maintenance Mode and the Gateway API Recommendation
As of 2026, the most important context is the future of ingress-nginx and the Ingress API itself.
- The Ingress API is frozen. No new features are added, and its expressiveness limits remain — L7 routing, traffic splitting, header matching all have to be worked around via annotations.
- A structure dependent on annotations and snippets is non-standard and widens the security surface.
- The successor standard is Gateway API. It expresses routing with first-class CRDs such as Gateway, GatewayClass, and HTTPRoute, and provides role separation (infrastructure operator vs app developer) and traffic splitting as standard spec.
For new projects, it is recommended to evaluate a Gateway API implementation first (for example Envoy Gateway, Contour, or NGINX Gateway Fabric). Existing ingress-nginx deployments need not be abandoned immediately, but reduce snippet dependence, consolidate annotations to standard features, and plan a gradual transition.
[ routing standard evolution ]
Ingress API (frozen) Gateway API (successor)
─────────────────── ────────────────────────
single Ingress resource ──▶ GatewayClass / Gateway
extended via annotations HTTPRoute / TLSRoute
vendor-specific behavior role-based separation (RBAC-friendly)
IngressClass and Multiple Controllers
Running several ingress-nginx controllers in one cluster is common — separating external from internal, or splitting controllers per team. Which ingress is handled by which controller is decided by IngressClass.
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: nginx-internal
spec:
controller: k8s.io/ingress-nginx
In the ingress you specify the class to use via spec.ingressClassName.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: internal-app
spec:
ingressClassName: nginx-internal
rules:
- host: internal.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: internal-svc
port:
number: 80
In the past the class was specified via the kubernetes.io/ingress.class annotation, but it is now deprecated and you should use the spec.ingressClassName field. Since a controller only handles ingresses of the IngressClass it owns, omitting ingressClassName in a multi-controller environment is a common mistake that creates an ingress "handled by no controller".
You can also designate a default IngressClass. Marking one as default via annotation makes ingresses that omit ingressClassName get handled by that class. For explicitness, however, it is recommended to write ingressClassName on every ingress.
pathType and Path Matching
The Ingress spec's pathType decides the path matching mode, and the behavioral difference is surprisingly large.
| pathType | Meaning |
|---|---|
| Exact | Path must match exactly |
| Prefix | Prefix match at path-segment granularity |
| ImplementationSpecific | Delegated to controller implementation (regex, etc.) |
Prefix works at segment granularity. A Prefix rule of /foo matches /foo and /foo/bar but not /foobar. To use regex or rewrite, combine ImplementationSpecific with the use-regex annotation. When a host has multiple paths, ingress-nginx prefers the more specific (longer) path.
Default Backend and Custom Error Pages
Requests that match no ingress go to the default backend. Also, to show a user-friendly page when a backend returns an error, you can wire up custom-http-errors with a separate error-page service.
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
custom-http-errors: "404,503"
proxy-intercept-errors: "true"
When a status code listed in custom-http-errors occurs, the controller sends the request to default-backend to fetch a custom error page. This is useful for services that need branded maintenance pages or a consistent error UX.
Troubleshooting
Symptoms you commonly meet in operations, and where to look.
# Inspect the actual nginx.conf the controller rendered
kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- cat /etc/nginx/nginx.conf
# Check which backend an ingress maps to
kubectl describe ingress app-rewrite
# Controller logs (trace reloads and config errors)
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller --tail=200
| Symptom | Common cause | Check |
|---|---|---|
| 404 Not Found | rewrite-target regex/path mismatch | Verify use-regex and path capture groups |
| Annotation ignored | Used another project's prefix | Confirm nginx.ingress.kubernetes.io |
| 413 Payload Too Large | proxy-body-size not set | Adjust annotation or ConfigMap |
| Snippet not applied | allow-snippet-annotations false | Review RBAC/risk-level policy |
| Reload storm | Frequent ConfigMap/Secret changes | Govern change frequency via GitOps |
Most problems narrow quickly once you directly verify which configuration was actually rendered into nginx.conf. When an annotation seems ignored, suspect the prefix and ingressClassName first.
Installation and Deployment Topology
ingress-nginx is usually installed via Helm, and the deployment topology greatly affects how traffic enters and whether the external IP is preserved.
helm upgrade --install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.service.type=LoadBalancer \
--set controller.replicaCount=3
Service type and traffic policy break down as follows.
| Topology | Characteristic | External IP preservation |
|---|---|---|
| Service LoadBalancer | Cloud LB in front | Controlled via externalTrafficPolicy |
| Service NodePort | Exposed directly via node port | Usually incurs SNAT |
| hostNetwork DaemonSet | Uses node network directly | Easy to preserve source IP |
Setting externalTrafficPolicy to Local removes the extra inter-node hop and preserves the client source IP, but a node only receives traffic if it has a controller pod. Cluster lets any node receive it, but SNAT may mask the source IP. This choice matters when you need IP-based policy (whitelist, logging).
apiVersion: v1
kind: Service
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app.kubernetes.io/name: ingress-nginx
ports:
- name: https
port: 443
targetPort: https
Key Takeaways
To recap everything covered here in one place:
- ingress-nginx (community) and the NGINX Inc. controller are different projects. Distinguish them by the annotation prefix (nginx.ingress.kubernetes.io).
- Inside one pod, the Go controller process watches the API, nginx handles actual traffic, and Lua avoids reloads via dynamic config.
- The core annotations are rewrite-target/use-regex regex captures, ssl-redirect, backend-protocol, and canary.
- The ConfigMap holds global defaults; annotations are per-ingress overrides.
- Snippets are powerful but dangerous, so keep them disabled by default and govern with RBAC and a policy engine.
- Distinguish multiple controllers with IngressClass, and understand pathType and deployment topology (externalTrafficPolicy).
- The Ingress API is frozen and ingress-nginx is in maintenance mode. Design new routing on Gateway API.
With this skeleton in place, you can quickly narrow causes and make the right decision in most real-world situations.
Conclusion
ingress-nginx is a sophisticated system where the controller process, nginx, and Lua-based dynamic config all interlock. Understand the annotation catalog and pin global behavior with the ConfigMap, and you can meet most requirements. But snippets are as dangerous as they are powerful, so keep them disabled by default and govern them with RBAC.
Above all, the 2026 big picture is clear. The Ingress API is frozen and ingress-nginx is in maintenance mode. The pragmatic dual strategy is to keep your running ingresses stable while designing new routing needs on Gateway API.
References
- Kubernetes Ingress concept: https://kubernetes.io/docs/concepts/services-networking/ingress/
- ingress-nginx official docs: https://kubernetes.github.io/ingress-nginx/
- ingress-nginx annotations reference: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/
- ingress-nginx ConfigMap options: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/
- Gateway API: https://gateway-api.sigs.k8s.io/
- NGINX Gateway Fabric: https://docs.nginx.com/nginx-gateway-fabric/
- Contour (Envoy-based): https://projectcontour.io/
- Traefik docs: https://doc.traefik.io/traefik/
- HAProxy Kubernetes Ingress: https://www.haproxy.com/documentation/kubernetes-ingress/
- cert-manager: https://cert-manager.io/docs/