Skip to main content

Ingress-NGINX Is Dead: Your Per-Route Migration Playbook Before the November 2026 Cliff

13 min readDora NodaDora Noda
Share
On this page

Your ingress controller is unmaintained, and there are two clocks ticking on it. Clock one already rang: the ingress-nginx project was archived in March 2026, which means no more releases and no more CVE patches — every day you still serve traffic through it, you are running a network-facing component with a permanently open vulnerability window. Clock two rings in November 2026, when Microsoft stops even critical security patches for the managed NGINX inside the AKS application-routing add-on.

Here is the verdict up front. Do not rewrite every route on day one, and do not swap the cluster-wide ingress class in one shot. Land a drop-in first — Traefik's NGINX-compatibility provider translates the annotations you already have at runtime, covering more than 90% of what real migration teams actually use — then convert routes to Gateway API one host at a time with the community ingress2gateway tool, and retire the old controller only when nothing references it. The destination is Gateway API. The first step is surviving the trip.

The two clocks, stated plainly

The retirement was announced in November 2025 by Kubernetes SIG Network together with the Security Response Committee, and upstream maintenance ended in March 2026 when the repository was archived. The project's own site now says it plainly: ingress-nginx has reached end of life. The planned successor, InGate, never matured and was retired too, so there is no maintained NGINX-flavored controller to upgrade to — only a choice of where to go next. The Kubernetes project's recommendation is the Gateway API, the official successor to the Ingress API.

The second clock belongs to Azure. Microsoft is providing a support bridge: critical security patches for the application-routing add-on's managed NGINX continue through November 2026, and production workloads on it remain fully supported until then. After that date, managed NGINX gets nothing, and AKS stops deploying NGINX by default for new app-routing setups. Azure's own replacement path is already live — Gateway API support for the app-routing add-on was announced in March 2026 and went GA in June 2026, built on an Istio-based control plane running next to the legacy NGINX in your cluster.

Two consequences follow that shape everything below. First, if you run the OSS controller yourself, your vulnerability window is already open — the archive means the next NGINX CVE is yours to mitigate alone. Second, if you run on AKS app routing, you have a deadline with a supported landing zone, not just a warning. Either way the end state is the same set of Gateway API objects. The only question is how you cross over without taking every domain down at once.

Why Gateway API is the destination but not the first step

It is tempting to read "ingress-nginx is dead, Gateway API is the successor" as "rewrite all your Ingress objects as HTTPRoutes this sprint." Resist that. The Ingress API itself is frozen, not removed — your existing Ingress objects keep working, and Kubernetes has no plans to delete the API. Nothing forces a flag-day rewrite at the API level, which means you can run the old and new controllers side by side in the same cluster and move routes over gradually.

That parallel-run pattern is the single most important migration mechanic in this post. Two ingress classes coexist — say, nginx and traefik — each backed by its own controller Deployment, and each Ingress object declares which one serves it via ingressClassName. Traffic moves route by route, host by host, with the old controller still serving everything you have not moved yet. If a converted route misbehaves, you flip one object's class back instead of rolling back a cluster-wide change. Every serious migration guide — GKE's Ingress-to-Gateway docs, the VMware "procrastinator's guide" for moving to Istio-backed Gateway API, the community runbooks — converges on this shape: both stacks live, routes drain from one to the other, the old stack dies last.

The other reason not to start with a hand rewrite is role separation, which is Gateway API's real structural win over Ingress. In the Ingress world, whoever can edit an Ingress object can touch shared load-balancer configuration — annotations are a single flat bag where routing rules, TLS settings, rate limits, and controller tuning all live together. Gateway API splits this into three roles: the platform team owns the Gateway (listeners, addresses, TLS termination posture), tenants own HTTPRoute objects that attach to it, and the mesh/policy layer owns attachment policies. For a fleet that routes every tenant domain through one gateway, that split is the difference between "any tenant annotation can break the edge" and an RBAC boundary you can actually enforce. You want to end up there. You do not need to get there in one jump.

The drop-in: what Traefik's 90% actually covers

Traefik's answer to the retirement is an Ingress NGINX compatibility layer — a dedicated provider mode (KubernetesIngressNGINX) that reads your existing Ingress objects, NGINX annotations included, and translates them into Traefik configuration at runtime. You change the ingress class, not the objects. Traefik Labs calls it the industry's only true drop-in replacement, and the substance behind the marketing is a coverage number: more than 90% of the annotations actively used by real migration teams. Note the phrasing — not 90% of every annotation ever documented, but 90% of what shows up in real clusters. That is the honest denominator, and it is the one you should audit yours against.

Here is the table that decides your migration budget. For each annotation family, three fates: translates at runtime under Traefik's provider, converts mechanically via ingress2gateway into Gateway API objects, or must be hand-authored (or redesigned) as HTTPRoute.

Annotation family (examples)Traefik provideringress2gatewayHand-authored HTTPRoute
Host/path routing, TLS hosts (spec.rules, spec.tls)Translates cleanly — this is the core pathConverts to Gateway + HTTPRoute hostnames, rules, backendRefsRarely needed; the tool output plus review suffices
Rewrites and redirects (rewrite-target, permanent-redirect)Translates cleanlyConverts to URLRewrite / RequestRedirect filtersOnly for rewrite logic with regex subtleties the converter flags
Timeouts, retries, body size (proxy-*-timeout, proxy-body-size)Translates cleanly via Traefik equivalents (e.g. forwarding timeouts, buffered request limits)Partial — emits the route, drops or annotates what has no filter equivalentMay need BackendTLSPolicy / implementation-specific policy objects
Traffic splitting and canaries (canary-*, canary-weight)Translates cleanlyPartial — weights map to backendRef weights where the semantics line upVerify weight semantics per implementation; canary-by-header usually needs hand work
Auth (auth-url, auth-signin, basic-auth secrets)Translates cleanly for the common external-auth shapeEmits structure, not behavior — there is no core Gateway API auth filterHand-author: implementation extension, SecurityPolicy, or an auth sidecar in front of the route
Arbitrary config snippets (server-snippet, configuration-snippet)Not covered — this is the heart of the missing 10%Cannot convert — a raw NGINX config fragment has no Gateway API equivalent by constructionRedesign each snippet: find the filter, policy object, or middleware that expresses the intent, or drop behavior you no longer need
WAF-ish and Lua hacks (ModSecurity annotations, inline Lua)Not coveredCannot convertReplatform onto a real policy layer (WAF product, admission-time checks, service-mesh policy) — this was never portable Ingress config

Two rows deserve emphasis because they are where migrations actually stall. The snippet row first: configuration-snippet was always an escape hatch out of the Ingress model, and the bill for using it comes due now. Audit yours with kubectl get ingress -A -o yaml | grep -c snippet and treat every hit as a line item with an owner and a redesign, not as config that "should just work." The auth row second: external auth is extremely common and fully expressible, but it lives in implementation-specific policy objects rather than portable core API — which means it converts fine if you standardize on one implementation, and becomes your lock-in surface if you ever switch again. Choose knowing that.

Traefik's second half matters as much as the first: the same controller speaks Gateway API natively (current builds track spec v1.6, with full HTTPRoute core plus standard-channel extras like GRPCRoute and TLSRoute), so the drop-in is also the landing zone — you do not migrate twice. That is why platform vendors are converging on it: Gardener's enhancement proposal to replace the nginx-ingress shoot add-on with a Traefik extension cites exactly this annotation-compatibility provider mode as the migration path, and community charts like selenium-grid's document the mechanical class rename (ingress.className: nginxtraefik) as a starting point.

What about the paths that are not Traefik? A community fork that freezes the NGINX codebase is a stopgap, not a strategy: no new features, a shrinking maintainer pool, and the same Gateway API migration waiting at the end of it. Other controllers offer annotation-mapping guides, not runtime compatibility — you rewrite everything before you can cut over, which is precisely the flag-day this post argues against. And hand-rewriting every route on day one is the most expensive option on the table: it spends your scarcest resource (engineers who understand both your traffic and the new API) before you have de-risked anything.

The cutover: per-route, in six steps

With the drop-in deployed beside the old controller, the migration is a loop over hosts, not a single event. Here is the playbook:

  1. Deploy Traefik alongside ingress-nginx under its own ingress class. Nothing changes for existing traffic. Confirm the new controller is healthy and serving a canary hostname end to end before touching anything real.
  2. Convert one low-risk host with ingress2gateway. Pick an internal tool or staging domain — something with real traffic shape and no external customers. Run the converter scoped to that namespace, review the generated Gateway and HTTPRoute objects (especially rewrites and weights), and apply them while the old Ingress still exists.
  3. Mirror and diff. With both stacks live, send shadow or low-weight traffic through the new route and compare: status codes, redirect targets, header behavior, TLS handshake results. The converter handles structure; only live traffic proves behavior.
  4. Shift that host. Flip the Ingress object's ingressClassName (or repoint its DNS) so the new stack owns the host. Watch error rates and p99 latency for at least one full traffic cycle — a deploy window is not a cycle.
  5. Repeat per host, hardest last. Work outward from boring to critical: static sites, then APIs, then the tenant-facing domains with snippets and external auth you redesigned earlier. Each host is an independent rollback unit.
  6. Retire the old controller when nothing references it. Audit with kubectl get ingress -A grouped by class, delete the ingress-nginx Deployment and its LoadBalancer Service (watch for a lingering cloud LB bill), and remove the class. Only now is the migration done.

On AKS specifically, the same loop applies with Azure's pieces substituted: enable the Gateway API implementation on the app-routing add-on, stand up your Gateway on it, and drain hosts off the managed NGINX before November 2026 — while Microsoft still patches the thing you are draining. The AKS engineering blog's June 2026 GA walkthrough shows both stacks visible in the cluster (aks-istio-system next to app-routing-system), which is exactly the parallel-run posture this playbook assumes. Your deadline is real but so is your bridge; spend it on the snippet redesigns, not on panic rewrites of routes the drop-in already serves.

What changes when one gateway routes every tenant's domain

Everything above gets stricter when the gateway is shared infrastructure rather than one team's edge. If your platform routes every tenant domain through a single control plane — which is precisely what a self-hosted PaaS does — the flag-day option is not just risky, it is disallowed by arithmetic: one bad rollout touches every customer at once, and no maintenance window covers "all tenants." Per-route is mandatory, and three fleet-specific concerns come with it.

The shared Gateway is platform-owned; routes are tenant-scoped. Model it that way from the start: one (or a few) Gateway objects owned by the platform team, per-tenant HTTPRoute objects in tenant namespaces attaching via parentRefs, and RBAC that lets tenants edit their routes but not the listeners. This is the payoff for the role split described earlier — and it is also a migration constraint, because every tenant route converted is a small RBAC and namespace-placement decision, not just a YAML translation. Budget for it per tenant, not per cluster.

Conformance before reliance. Gateway API's core is portable; the extended features (auth-adjacent policies, session persistence, response rewriting) vary by implementation, and each implementation publishes conformance reports against specific spec versions. Before your tenants can rely on anything beyond core HTTPRoute behavior, check your chosen implementation's report for the version you run — Traefik tracks recent spec releases closely, but "the implementation supports it" and "your version conforms to it" are two different claims, and tenant-facing behavior should rest only on the second.

Snippets multiply by tenant count. The snippet audit from the previous section is per route, and in a fleet the long tail of configuration-snippet hacks belongs to tenants who wrote them years ago and forgot. Announce a deprecation window for snippet-bearing routes early, offer the two or three blessed replacements (redirect filters, middleware/auth policies, WAF at the edge), and convert cooperative tenants first — their clean routes validate the new stack before you negotiate the hard ones.

The durable contract

Step back and the ingress-nginx retirement looks less like an emergency and more like a forced upgrade to a better contract. Annotations were never an API — they were per-controller strings with no versioning, no validation, and no portability, which is why one archived project could strand the entire ecosystem's edge config. Gateway API replaces folklore with schema: typed routes, versioned channels, conformance reports you can read before you commit. The migration tax is real, concentrated in snippets and auth behavior, but it is a one-time tax that buys you out of ever paying it again the same way.

So mind the clocks — unpatched from March, unsupported on Azure from November — but spend the time they give you in order: drop-in first, hardest routes last, old controller deleted only when the audit says nothing references it. The teams that flag-day this will write the postmortems. The teams that cut over per route will barely have a story to tell.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Every tenant domain in this post's fleet model is exactly the kind of route a self-hosted PaaS has to cut over without downtime. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex