On March 24, 2026, Kubernetes SIG Network and the Security Response Committee retired Ingress NGINX. The GitHub repositories went read-only the same day. No more releases. No more bug fixes. No more security patches.
Roughly half the cloud-native estate depends on this controller. The retirement does not break anything immediately, but every new CVE in the NGINX core, Lua, OpenResty, or the controller code itself stays unpatched. My own staging cluster ran Ingress NGINX behind every service. I migrated it the weekend the announcement landed, and this post is the playbook I used: which replacement to pick, the Traefik drop-in path for the panicked migration, the Gateway API with Cilium path for the proper migration, and the zero-downtime cutover sequence.
What happened to Ingress NGINX?
Ingress NGINX hit end of life on March 24, 2026, when Kubernetes SIG Network and the Security Response Committee formally retired the kubernetes/ingress-nginx project. The retirement was telegraphed in the November 2025 announcement on kubernetes.io, confirmed in the January 2026 SRC statement, and made final at the March 2026 retirement date.
What changed on that date:
- The GitHub repos became read-only.
- The controller no longer receives bug fixes, feature changes, or CVE patches.
- Helm charts and container images that already exist stay published, but new ones will not be cut.
- The planned successor InGate was also abandoned. The community did not show up to maintain it.
What did not change:
- Existing in-cluster Ingress NGINX deployments keep running.
- Existing
Ingressresources keep working against any controller that implements the Ingress API. - Cloud-managed alternatives (AWS ALB Controller, GCE Ingress, Azure Application Gateway) are unaffected because they are separate codebases.
The official recommendation from SIG Network is direct: begin migration to Gateway API or to another actively maintained Ingress controller immediately. The Kubernetes blog's own words are that staying on Ingress NGINX after retirement "leaves you and your users vulnerable to attack". If you ship anything to production behind this controller, this is the patch you cannot defer.
Who needs to migrate and how urgent is it?
Anyone running the kubernetes/ingress-nginx controller needs to migrate. The urgency depends on what kind of traffic the controller fronts. Public-facing clusters with any auth surface should treat this as a same-quarter migration. Internal-only clusters get one cycle of grace at most.
Use this matrix to scope your own work.
| Cluster type | Traffic | Urgency | Suggested target |
|---|---|---|---|
| Public production | Auth, payments, user data | Same quarter | Gateway API + Cilium / Istio / Envoy Gateway |
| Public production | Static or CDN-fronted only | One quarter | Traefik drop-in, then Gateway API |
| Internal staging | Dev traffic | One cycle | Traefik drop-in |
| Edge / IoT | Mixed | Same quarter | Gateway API + Cilium (if already on Cilium CNI) |
| Cloud-managed | Already on ALB/GCE/AGIC | None | No action |
The thing to watch on the urgent rows is your own CVE feed. Every Nginx core CVE, every OpenResty advisory, every Lua module disclosure now stays unpatched against the retired controller. A WAF in front of it buys you time but not safety, because most of the meaningful vulnerabilities are at the HTTP layer the WAF needs to let through.
I disagree with one piece of common advice making the rounds: that you can ride out the retirement with a strict WAF policy and pinned image hashes. That works for a few weeks. It does not work for a year. The same logic that retired the controller (limited maintainer bandwidth, growing CVE backlog) applies to the WAF policy too, because the WAF authors are not building new rules against a controller nobody else uses anymore.
Which replacement should you pick?
Pick Gateway API as the long-term destination and Traefik as the safe staging point if you cannot reach Gateway API immediately. The community has aligned on Gateway API as the next-generation traffic management standard. Traefik is the only realistic drop-in replacement because its NGINX Ingress Provider reads existing nginx.ingress.kubernetes.io annotations natively.
Here is the short comparison.
| Replacement | Drop-in for nginx annotations | Gateway API support | Where it fits |
|---|---|---|---|
| Gateway API + Cilium | No (manifest rewrite) | Native, GA | Long-term standard. Best if already on Cilium CNI. |
| Gateway API + Istio | No (manifest rewrite) | Native, GA | Long-term standard. Best with existing service mesh. |
| Gateway API + Envoy Gateway | No (manifest rewrite) | Native, GA | Long-term standard. Minimal extra footprint. |
| Traefik | Yes (NGINX Ingress Provider) | Yes | Fastest cutover. Manifest changes optional. |
| HAProxy Ingress | Partial | Yes | Performance focus, HTTP/3 native. |
| Kong | Partial | Yes | If you also want an API gateway, not just ingress. |
For my own staging cluster I ran a two-phase migration: Traefik first, Gateway API second. The Traefik phase took two hours. The Gateway API phase took a week. Both shipped without downtime.
Cilium is the right Gateway API implementation if your cluster is already on Cilium CNI. The functionality is built in. You do not deploy a second component, and the eBPF datapath that already routes pod traffic also handles ingress.
How do you migrate to Traefik with the drop-in adapter?
You migrate to Traefik by installing the chart with the NGINX Ingress Provider enabled, leaving Ingress NGINX running side by side, and switching your DNS or LoadBalancer one host at a time. Traefik reads your existing Ingress resources directly. You do not rewrite manifests in the first pass.
The minimum Helm install looks like this.
helm repo add traefik https://traefik.github.io/charts
helm repo update
helm install traefik traefik/traefik \
--namespace traefik-system \
--create-namespace \
--set providers.kubernetesIngress.enabled=true \
--set providers.kubernetesIngress.publishedService.enabled=true \
--set providers.kubernetesIngress.ingressClass=traefik \
--set ingressClass.enabled=true \
--set ingressClass.isDefaultClass=false \
--set ingressRoute.dashboard.enabled=falseTwo settings to call out. First, providers.kubernetesIngress.enabled=true turns on the NGINX-compatible Ingress reader. Second, ingressClass.isDefaultClass=false keeps Traefik from grabbing every existing Ingress resource at install time. You opt resources in one by one by changing their class.
Now tag the first Ingress to move.
# my-app-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app
annotations:
kubernetes.io/ingress.class: traefik # was: nginx
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: traefik # was: nginx
rules:
- host: my-app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app
port:
number: 80The nginx.ingress.kubernetes.io/rewrite-target annotation stays. Traefik reads it. So do most of the popular annotations in that namespace, including auth-url, auth-signin, enable-cors, force-ssl-redirect, and ssl-redirect. The Traefik documentation lists the supported set and clearly marks the ones that are not handled. Read that page once before you start, because the annotations that do not translate cleanly tend to be the regex-based rewrites and the more exotic auth flows.
Apply the manifest. Now the resource is owned by Traefik. The Ingress NGINX controller stops reconciling it the moment the class flips.
Repeat for every Ingress in the cluster. When the last one is over, scale Ingress NGINX to zero.
kubectl scale -n ingress-nginx deployment/ingress-nginx-controller --replicas=0Leave it at zero for a week. If nothing breaks, delete the namespace. If something breaks, scale it back up and the failed resource resumes traffic the moment the class flips back.
How do you migrate to Gateway API with Cilium?
You migrate to Gateway API by installing the CRDs, enabling the Gateway API feature in your chosen implementation, declaring a Gateway resource per listener, then rewriting each Ingress as an HTTPRoute. The Cilium path is the most compact because Cilium already runs as your CNI and the Gateway API support is a flag, not a new component.
Install the standard Gateway API CRDs first.
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yamlThen enable Gateway API in Cilium. For an existing Cilium install this is a Helm value update.
helm upgrade cilium cilium/cilium \
--namespace kube-system \
--reuse-values \
--set gatewayAPI.enabled=true \
--set gatewayAPI.enableAlpn=true \
--set gatewayAPI.enableAppProtocol=trueRestart the Cilium operator. The Gateway API controller comes up as part of the existing Cilium daemon set.
Declare the Gateway. This replaces what used to be the Ingress NGINX LoadBalancer Service plus the Ingress host blocks.
# gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: public
namespace: gateway-system
spec:
gatewayClassName: cilium
listeners:
- name: https
port: 443
protocol: HTTPS
hostname: '*.example.com'
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: wildcard-example-com
allowedRoutes:
namespaces:
from: AllNow rewrite the per-app Ingress as an HTTPRoute.
# my-app-route.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-app
spec:
parentRefs:
- name: public
namespace: gateway-system
hostnames:
- my-app.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: my-app
port: 80The Ingress resource and the HTTPRoute resource can coexist for the same host. Apply the route while the Ingress is still serving traffic. The Gateway API controller picks it up but does not become authoritative for the host until you redirect DNS or the LoadBalancer.
Three things the Gateway API does cleanly that Ingress did not. First, listener configuration (TLS, hostname, port) is a separate first-class resource, not a stack of annotations. Second, route attachment is cross-namespace by default, gated by allowedRoutes. Third, traffic-management features (weighted backends, header rewrites, request mirroring) are part of the spec, not vendor-specific annotations.
The Cilium-specific docs cover the eBPF datapath details and the Gateway API conformance matrix. The Cilium release notes confirm Gateway API GA in v1.15. If you are on a Cilium version older than that, upgrade Cilium first.
How do you cut over without downtime?
You cut over without downtime by running the old and new controllers side by side, swinging traffic at the DNS or LoadBalancer layer rather than inside the cluster, and watching the new controller for one full traffic cycle before scaling the old one down. The wrong order is to delete the old controller first.
Here is the sequence I used on my own cluster.
- Install the new controller in its own namespace. Do not change any
IngressorIngressClassNameyet. - Verify the new controller comes up healthy with a synthetic backend. A small
echodeployment behind one test Ingress (orHTTPRoute) is enough. - For the first real workload, point a low-traffic DNS record at the new controller's LoadBalancer IP. Use a per-host record, not a wildcard, so the blast radius is bounded.
- Watch for one full traffic cycle. At my scale that is 24 hours. For a high-traffic app it is one peak window.
- If error rates and latency match the old controller, move the next host. Repeat.
- When every host is on the new controller, drop the old LoadBalancer service and scale the controller to zero. Leave the deployment in place for a week as a rollback.
- Delete the old namespace.
The DNS-level swing is the part that matters. Changing IngressClassName on a live resource also works, but it cuts traffic instantly. A DNS swing lets you bleed traffic over the TTL window, and if something is wrong with the new controller the rollback is a single record change.
For services that depend on the LoadBalancer source IP for security policy, the cutover needs a coordinated step where you tell the upstream firewall about both LoadBalancer IPs for the duration of the migration. I learned this the loud way when a third-party webhook started returning 403 from one provider after I moved their host. The provider's allowlist still pointed at the old LoadBalancer.
How do you verify the migration?
You verify the migration by running the conformance suite for your new controller, replaying a representative sample of production traffic, and watching golden signals during a full traffic cycle. If any of those three reveals a regression, do not delete the old controller yet.
For Gateway API specifically, the conformance suite is the first signal.
git clone --depth=1 https://github.com/kubernetes-sigs/gateway-api
cd gateway-api/conformance
go test ./... \
-args -gateway-class=cilium \
-conformance-profiles=GATEWAY-HTTP,GATEWAY-TLSEvery test that passes against your cluster certifies a piece of the spec that your config relies on. The full suite takes about ten minutes. Failures point at exactly which HTTPRoute feature your cluster does not handle.
For Traefik, the in-tree integration tests are the equivalent.
kubectl logs -n traefik-system deploy/traefik | grep -E "error|fail"This is less rigorous than the Gateway API suite, but the Traefik controller is verbose about misconfigured Ingress resources. A clean log over 24 hours is the practical signal that your annotations translated correctly.
For traffic replay, the cleanest tool is goldpinger plus a TLS-aware HTTP probe.
# verify-route.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: verify-my-app
spec:
template:
spec:
restartPolicy: Never
containers:
- name: probe
image: curlimages/curl:8.7.1
command:
- sh
- -c
- |
for i in $(seq 1 100); do
curl -fsS \
-H "Host: my-app.example.com" \
https://my-app.example.com/healthz \
|| { echo "fail at $i"; exit 1; }
sleep 1
doneRun that against both the old and new LoadBalancer IPs while traffic is still going through the old controller. A divergence shows up fast.
The final check is the golden signals. Watch p99 latency, 4xx rate, and 5xx rate on the affected hosts for one full traffic cycle. Compare against the day before the cutover, not against the day of, because the day of will show DNS-propagation artifacts.
For deeper context on related infrastructure shifts, see the Postgres connection pool pgbouncer survival guide for the same pattern applied to database fronting, the SSH tunnel local database access guide for a non-cluster fallback when you need direct backend reach during the migration, and the Zero trust microservices with Spring Security for the auth-binding implications of the controller change.
For the original sources, see the Ingress NGINX retirement announcement, the SRC statement on the retirement, the Traefik drop-in migration guide, and the Cilium Gateway API documentation.
Keep Reading
- Postgres Connection Pool: pgbouncer Survival Guide. The same pattern of fronting a critical backend with the right adapter, applied to Postgres.
- SSH Tunnel for Local Database Access. The non-cluster fallback you reach for when you need direct backend reach during a controller migration.
- Zero Trust Microservices with Spring Security. The auth-binding model that has to follow the controller through the migration.
