Ingress NGINX Is Retired and Unpatched: A Working Engineer's Migration Checklist to Gateway API (2026)

TL;DR

  • The community Ingress NGINX controller (kubernetes/ingress-nginx) is retired. Kubernetes announced it on 11 November 2025, shipped the final releases on 19 March 2026, and archived the repository on 24 March 2026. There will be no further releases, bugfixes, or security patches — ever.
  • This is no longer theoretical: on 5 November 2026 the Kubernetes Security Response Committee published CVE-2026-116472 against ingress-nginx and stated plainly, "Kubernetes will not be issuing a fix for this CVE." Every future CVE gets the same answer.
  • The Ingress API itself is not deprecated — it is GA and frozen. Only this one controller is dead. F5's separate NGINX Ingress Controller is also unaffected. What you are replacing is the controller, and Gateway API is the replacement Kubernetes recommends.
  • The migration risk is not the YAML conversion — it is the annotations. ingress-nginx behaviours you rely on (regex paths, rewrites, canary weights, snippets) either change meaning or vanish under Gateway API. This checklist inventories them first, then converts.

If you run production traffic through ingress-nginx, you are now running it through unmaintained software that sits on a public IP and parses untrusted input for a living. The controller still works — that is exactly the trap. Nothing pages you today; the risk compounds silently with every new CVE disclosure, and the project's vulnerability pattern (annotation-driven configuration injection, the same class behind IngressNightmare in March 2025) means the next one is a matter of time, not imagination.

This is the migration runbook in checklist form: stop the bleeding today, inventory what you actually depend on, learn which annotations silently break, pick a Gateway API implementation, convert, verify, cut over, and keep a rollback that actually works.

Why migrate now: the timeline, verified

Every date below is from the Kubernetes project's own announcements and advisories:

Date What happened
11 Nov 2025 SIG Network and the Security Response Committee announce the retirement: best-effort maintenance until March 2026, then "no further releases, no bugfixes, and no updates to resolve any security vulnerabilities." The planned replacement controller, InGate, is retired alongside it.
19 Mar 2026 Final ingress-nginx releases ship: controller v1.15.1, v1.14.5, v1.13.9. There will never be a v1.15.2 from the Kubernetes project.
24 Mar 2026 The kubernetes/ingress-nginx GitHub repository is archived and made read-only.
5 Nov 2026 CVE-2026-116472 is published: the mirror-target annotation can be used to reach an unauthenticated configuration API inside the controller and drop all traffic through it. The official advisory states Kubernetes will not issue a fix — the first post-retirement proof of what "retired" means.

Three clarifications that prevent wasted work:

  • You do not have to leave the Ingress API. It remains GA and supported; it is simply frozen. You could move to another maintained Ingress controller instead. Gateway API is the recommended target because it is where the ecosystem's development now happens — and because every major controller now implements it.
  • F5's NGINX Ingress Controller is a different project. Similar name, different repository, different maintainers, still maintained. If you run F5's controller, this article is not about you.
  • Existing deployments keep running. Nobody is remotely disabling your controller. The decision is not "migrate or it stops" — it is "migrate on your schedule, or migrate during an incident on a CVE's schedule."

Step 0 — Today's stopgap for CVE-2026-116472 (30 minutes)

Do this before the migration project, whatever its timeline. The advisory's own mitigations:

  • [ ] Set the annotation risk level. In the ingress-nginx controller ConfigMap, set annotations-risk-level: "Medium" (or "Low"). The mirror-target annotation is classified above Medium, so this blocks the CVE's vector. Warning: if any team legitimately uses High/Critical-risk annotations, this will break them — check the inventory in Step 1 first, or apply this and watch for rejected Ingresses.
  • [ ] Or reject the annotation at admission. Use your policy engine (Kyverno, OPA/Gatekeeper, or a ValidatingAdmissionPolicy) to reject any Ingress carrying nginx.ingress.kubernetes.io/mirror-target.
  • [ ] Check for existing use and abuse. Search your clusters for mirror-target values pointing at localhost addresses — the advisory flags that as an exploitation indicator: kubectl get ingress -A -o yaml | grep -B5 "mirror-target".
  • [ ] Remember who can pull this trigger: anyone with permission to create an Ingress. Review which namespaces and service accounts hold Ingress write access — that is now your real privilege boundary for this controller (our Kubernetes RBAC guide covers the audit pattern).
  • [ ] Know your fork status. The only patched build of this CVE is SUSE's Rancher Prime fork (1.15.2-prime1). Managed add-ons from cloud vendors may carry their own support terms — check your vendor's statement rather than assuming upstream status applies to you.

Step 1 — Inventory: find every install and every annotation

You cannot convert what you have not counted. Run this on every cluster, including the staging cluster everyone forgets.

Find the controller itself (the command from the official retirement post):

kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx 

Also check for installs under custom release names, and for more than one ingress-nginx per cluster — shared platform clusters often have two.

Count the estate:

kubectl get ingress -A | wc -l kubectl get ingressclass 

Extract every nginx annotation in use, with counts. This output is the single most valuable artifact of the whole migration — it is your conversion workload, sorted:

kubectl get ingress -A -o json | jq -r ' .items[] | (.metadata.annotations // {}) | keys[] | select(startswith("nginx.ingress.kubernetes.io/")) ' | sort | uniq -c | sort -rn 

Per-Ingress annotation detail, for the conversion tickets:

kubectl get ingress -A -o json | jq -r ' .items[] | .metadata.namespace + "/" + .metadata.name + " " + (((.metadata.annotations // {}) | keys | map(select(startswith("nginx.ingress.kubernetes.io/")))) | join(", ")) ' 

Do not forget the controller-level configuration. Global behaviour lives outside the Ingress objects:

  • [ ] Dump the controller ConfigMap (kubectl -n ingress-nginx get cm ingress-nginx-controller -o yaml): body-size defaults, timeout defaults, proxy-real-ip-cidr, HSTS, error pages, Lua plugins.
  • [ ] List every *-snippet annotation and every ConfigMap snippet. Snippets have no Gateway API equivalent at all — each one is a small redesign, not a translation.
  • [ ] Note TLS termination points, default backends, and any Ingress with pathType: ImplementationSpecific — that pathType is a smell that regex behaviour is in play.
  • [ ] Record which teams own which namespaces. The annotation report by namespace is your migration plan's work-breakdown.

Step 2 — The annotations that silently break or change behaviour

This is the section that decides whether your migration is boring or an incident. Gateway API is a smaller, stricter, portable API. ingress-nginx is a decade of accumulated NGINX behaviours. Where they differ, nothing errors — traffic just behaves differently.

Behaviour differences that exist even with zero annotations

From the Kubernetes project's own pre-migration write-up (Five Surprising Ingress-NGINX Behaviors) — verify each against your estate:

  1. Regex paths are prefix matches, and case-insensitive. An ingress-nginx regex path matches anything starting with a match, ignoring case. Gateway API RegularExpression matches are exact semantics per the implementation — paths you believed were narrow may have been wide, and vice versa after conversion.
  2. use-regex leaks across Ingresses. Enabling it on one Ingress makes every path for that host, across all Ingresses, regex. Gateway API has no such cross-object side effect — behaviour that "just worked" because of the leak stops working.
  3. Missing trailing slash gets a free 301. Requests to a path without a trailing slash are redirected to the slashed version. Gateway API does not do this implicitly; clients and health checks that depended on it see 404s.
  4. URLs are normalised before matching. Dot segments and duplicate slashes are collapsed first. If your backends or your security rules assumed the raw path, test encoded and unnormalised paths explicitly after migration.
  5. The default backend catches more than you think. Unmatched hosts and paths land on the ingress-nginx default backend with its specific response behaviour. Your Gateway needs an explicit catch-all route or the error shape changes.

Annotation mapping: what converts, what changes, what dies

ingress-nginx annotation Gateway API equivalent Verdict
rewrite-target URLRewrite filter (ReplacePrefixMatch / ReplaceFullPath) ⚠️ Converts for prefix rewrites. Regex capture groups (/$2) have no equivalent — restructure the paths instead.
use-regex: "true" path.type: RegularExpression per match ⚠️ Converts per-route, but loses the cross-Ingress side effect (see above) and prefix/case behaviour differs.
canary: "true" + canary-weight Weighted backendRefs on one HTTPRoute rule ✅ Converts cleanly — see Step 4.
canary-by-header / canary-by-cookie Header/cookie match on a separate rule ✅ Converts, as an explicit match instead of a shadow Ingress.
ssl-redirect / force-ssl-redirect RequestRedirect filter (scheme https) on the HTTP listener route ✅ Converts.
permanent-redirect RequestRedirect filter with statusCode: 301 ✅ Converts.
enable-cors + cors-* CORS filter on HTTPRoute (Standard channel since Gateway API v1.5) ✅ Converts.
proxy-body-size — ❌ No standard field. Implementation-specific (e.g. a policy object). Silently dropping it means your data plane's default body limit applies — find out what it is.
proxy-connect-timeout / proxy-read-timeout / proxy-send-timeout timeouts on the HTTPRoute rule ⚠️ The API has timeouts.request / timeouts.backendRequest, but support varies by implementation. Verify against your controller's conformance, don't assume.
auth-url / auth-signin / auth-method (external auth) — ❌ No standard equivalent yet. Each implementation has its own extension (Envoy Gateway's SecurityPolicy, NGINX Gateway Fabric's policies, Traefik middleware). Budget real redesign time.
affinity / session-cookie-* Session persistence ⚠️ Not portable: it lives in the Experimental channel / implementation-specific configuration, and Gateway API v1.7 reshaped it (moved from the route to the backend). Pin behaviour per implementation.
limit-rps / limit-connections (rate limiting) — ❌ No standard rate-limit API. Implementation policy objects only.
configuration-snippet / server-snippet / location-snippet — ❌ Dies, by design. Raw NGINX config injection was the project's core security problem. Every snippet is a feature to re-implement properly or retire.
mirror-target — ❌ No equivalent — and it is the CVE-2026-116472 vector. If you use traffic mirroring, it becomes an implementation feature (e.g. RequestMirror filter exists for mirroring to a backend — check semantics before assuming parity).
backend-protocol: HTTPS BackendTLSPolicy ✅ Converts — TLS to the backend is a first-class policy now.
x-forwarded-prefix URLRewrite + header handling per implementation ⚠️ Partially. Test what your backends actually read.

Rule of the migration: any row marked ❌ in your inventory is a design task with an owner and a date, not a line in a conversion script. If Step 1 shows snippets or external auth in production, your timeline is set by those rows, not by the easy 90%.

Step 3 — Choose a Gateway API implementation

Gateway API is a specification; you still run a controller. Current release: Gateway API v1.7.0 (October 2026), in which HTTPRoute retries graduated to the Standard channel. Install the CRDs from the Standard channel unless you knowingly need Experimental features:

kubectl apply --server-side -f \ https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.7.0/standard-install.yaml 

The realistic shortlist, by situation rather than by hype:

If you… Start with Why
Want the closest "NGINX, but maintained" landing NGINX Gateway Fabric (F5) Same data plane family, F5-maintained, implements Gateway API natively. Familiar failure modes.
Want a neutral, Envoy-based default Envoy Gateway CNCF project built specifically as a Gateway API implementation; clean policy model (BackendTrafficPolicy, SecurityPolicy) for the ❌ rows above.
Already run Istio Istio Its Gateway API support is its recommended ingress path; no second data plane to operate.
Already run Cilium as CNI Cilium Gateway API support built into the networking layer you already run.
Are on GKE GKE Gateway Controller Google-managed, integrates with Google Cloud load balancing and policies.
Are on EKS AWS Load Balancer Controller (Gateway API) or a self-managed option above LBC's Gateway API support provisions ALBs from Gateway resources; verify the specific features you need — its supported subset is documented and narrower than the full API. (Context: our Amazon EKS guide.)
Run Traefik today Traefik Implements Gateway API alongside its Ingress support, easing a staged move.

Decision rules that matter more than the brand:

  • [ ] Check the conformance profile for the exact features you use (CORS, rewrites, timeouts, GRPCRoute), not the logo on the implementations page.
  • [ ] Match the version pairing: install the Gateway API CRD version your controller's release was tested against, not blindly the newest.
  • [ ] Decide who owns the Gateway. Gateway API splits ownership: platform team owns GatewayClass + Gateway, app teams own HTTPRoutes. If your org chart cannot support that split, a single-team Gateway per namespace is the pragmatic start.
  • [ ] Confirm your ❌ rows. External auth, rate limiting, body size: get the implementation's policy object for each one in writing in the ticket before committing.

Step 4 — Convert: ingress2gateway first, hand-fix second

Generate a first draft automatically. The Kubernetes project's ingress2gateway tool (1.0 released 20 March 2026; current release v1.2.0) reads your live Ingresses — or exported YAML — and emits Gateway + HTTPRoute resources, with warnings for constructs it cannot translate:

go install github.com/kubernetes-sigs/ingress2gateway@v1.2.0 ingress2gateway print --providers=ingress-nginx > gateway-draft.yaml 

Treat the output as scaffolding. Every warning it prints maps to a row in the Step 2 table; clear them one by one. (If you manage Ingresses through Helm or GitOps, convert the rendered manifests, then work the result back into your charts — our Helm tutorial and Argo CD GitOps guide cover the repo side.)

A worked conversion. This Ingress carries the three most common behaviours — regex rewrite, forced HTTPS, and a second canary Ingress (shown merged here as weighted backends):

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app namespace: production annotations: nginx.ingress.kubernetes.io/use-regex: "true" nginx.ingress.kubernetes.io/rewrite-target: /$2 nginx.ingress.kubernetes.io/ssl-redirect: "true" spec: ingressClassName: nginx tls: - hosts: ["app.example.com"] secretName: app-tls rules: - host: app.example.com http: paths: - path: /api(/|$)(.*) pathType: ImplementationSpecific backend: service: name: api port: { number: 8080 } - path: / pathType: Prefix backend: service: name: web port: { number: 80 } 

The Gateway API shape — note that the regex-capture rewrite (/$2) does not survive; the honest conversion changes the match to a prefix and the rewrite to ReplacePrefixMatch. If your API truly depends on capture-group rewrites, that is a ❌ row: fix the paths at the app or accept an implementation-specific filter:

apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: app-gateway namespace: production spec: gatewayClassName: envoy # your implementation's class  listeners: - name: http protocol: HTTP port: 80 - name: https protocol: HTTPS port: 443 tls: mode: Terminate certificateRefs: - name: app-tls --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: app-https-redirect namespace: production spec: parentRefs: - name: app-gateway sectionName: http hostnames: ["app.example.com"] rules: - filters: - type: RequestRedirect requestRedirect: scheme: https statusCode: 301 --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: app namespace: production spec: parentRefs: - name: app-gateway sectionName: https hostnames: ["app.example.com"] rules: - matches: - path: type: PathPrefix value: /api filters: - type: URLRewrite urlRewrite: path: type: ReplacePrefixMatch replacePrefixMatch: / backendRefs: - name: api port: 8080 - matches: - path: type: PathPrefix value: / backendRefs: - name: web port: 80 weight: 90 # was: canary Ingress with canary-weight  - name: web-v2 port: 80 weight: 10 

What changed, deliberately: one Ingress became three objects with separate owners; the canary stopped being a shadow Ingress and became arithmetic on one rule; TLS moved from the route to the listener; the regex became a prefix because the capture-group semantics were the thing being retired.

Validate before applying anywhere:

kubectl apply --dry-run=server -f gateway-draft.yaml kubectl apply -f gateway-draft.yaml kubectl get gateway,httproute -n production kubectl describe httproute app -n production # Status.Parents: Accepted + Programmed 

An HTTPRoute whose status is not Accepted: True and Programmed: True is not routing anything — this status check replaces ingress-nginx's "apply and pray," and it belongs in your CI as well as your runbook. (The networking model underneath — Services, endpoints, DNS — is unchanged; our Kubernetes networking & ingress basics post covers it end to end.)

Step 5 — TLS, explicitly

  • [ ] Certificates live on the listener now (certificateRefs), not on each route. One wildcard on the Gateway can retire a shelf of per-Ingress secrets — or concentrate your blast radius. Choose consciously.
  • [ ] cert-manager works with Gateway API (its gateway-shim provisions certificates from Gateway listeners). Enable the Gateway API support flag on cert-manager and test one non-production Gateway first; current cert-manager releases support the stable ListenerSet as well — check your version's release notes before relying on it.
  • [ ] Cross-namespace references need a ReferenceGrant. A route in namespace A pointing at a Service or Secret in namespace B is denied by default. This is a feature — and the number-one cause of "my route is Accepted but not Programmed" in multi-team clusters.
  • [ ] Backend TLS is a policy, not an annotation: BackendTLSPolicy replaces backend-protocol: HTTPS and gives you CA control via validation.caCertificateRefs.
  • [ ] Test the chain, not just the handshake: SNI behaviour per hostname, HTTP→HTTPS redirect status code (301 vs 308 matters to API clients), and HSTS headers if your old ConfigMap set them.

Step 6 — Test and verify before any traffic moves

Run the new Gateway in parallel, on its own address, and prove parity route by route:

  • [ ] Status first: every Gateway Programmed, every HTTPRoute Accepted + Programmed.
  • [ ] Golden-path parity: for each host/path, compare old vs new on: status code, redirect target, response headers (especially CORS and cache headers), and body for the top endpoints. A diff script against both load-balancer addresses pays for itself immediately.
  • [ ] The Step 2 traps, tested by name: case-sensitivity of paths, trailing-slash behaviour, unnormalised paths (//api, /api/../api), oversized request bodies (your dropped proxy-body-size), slow clients against timeouts, WebSocket/gRPC if you run them.
  • [ ] 404/503 shapes: unmatched host, unmatched path, backend down. Monitoring and client retries often key off these bodies and codes.
  • [ ] Load test the new data plane at production-shaped traffic before cutover if the controller is new to you — you are changing the component on the hot path (the verification discipline from our DevSecOps checklist applies: every claim in this step gets evidence, not vibes).
  • [ ] Dashboards and alerts re-pointed at the new controller's metrics before cutover, not after the first incident.

Step 7 — Cut over with a rollback that is a DNS change, not a project

  • [ ] Keep ingress-nginx running and untouched through the whole cutover. Rollback = traffic back to the old address. Deleting the old controller "to force commitment" converts every surprise into an outage.
  • [ ] Shift traffic gradually: weighted DNS records (or your CDN / global load balancer) — 5% → 25% → 50% → 100%, with a bake period at each step and error-rate/latency comparisons against the old path. (Progressive-delivery discipline transfers directly — see our Argo Rollouts guide.)
  • [ ] Pre-write the rollback trigger: e.g. "5xx rate on the Gateway exceeds the ingress-nginx baseline by X for Y minutes → DNS weight back to 0, page platform on-call." Decide the numbers now, while nobody is tired.
  • [ ] Hold the old path for an agreed bake period (two weeks is a common, defensible choice) before decommissioning — long enough for monthly batch jobs and forgotten webhooks to have fired once.
  • [ ] Decommission in order: drain → remove DNS → uninstall controller → delete Ingress objects last, from Git, so the estate's history stays readable.
  • [ ] Close the security loop: remove the ingress-nginx admission webhooks and RBAC, and record the removal in your asset inventory — an unpatched controller that no longer routes traffic but still runs is still attack surface.

The final checklist

This week

  • [ ] CVE-2026-116472 stopgap applied (annotations-risk-level, admission rule, or fork) on every cluster
  • [ ] Every ingress-nginx install found, including staging and second installs
  • [ ] Annotation inventory produced and sorted — the ❌ rows named

Design

  • [ ] Gateway API implementation chosen; conformance checked for your exact features
  • [ ] Every ❌ row (snippets, external auth, rate limits, body size) has an owner and a design
  • [ ] Gateway ownership model decided (platform Gateway vs per-team)

Build & verify

  • [ ] CRDs (v1.7.0 Standard channel) + controller installed alongside ingress-nginx
  • [ ] ingress2gateway draft generated; every warning resolved or ticketed
  • [ ] Parity tests pass, including the five behaviour traps and TLS checks
  • [ ] Route status (Accepted/Programmed) gated in CI

Cut over

  • [ ] Weighted traffic shift with bake periods and a written rollback trigger
  • [ ] Old controller kept live through the bake period
  • [ ] Decommission completed and recorded; webhooks/RBAC removed

FAQ

Is ingress-nginx still safe to run in 2026?
It runs, and existing installs are not broken — but it receives no security fixes, and CVE-2026-116472 (November 2026) is the standing example: disclosed, rated, and deliberately unpatched by the Kubernetes project. "Safe" now means "you accept every future CVE as permanent." For an internet-facing data plane, that is not a defensible steady state.

Is the Kubernetes Ingress API deprecated too?
No. The Ingress API is GA, feature-frozen, and has no planned removal. Only the community ingress-nginx controller retired. Staying on Ingress with a different maintained controller is a legitimate choice; Gateway API is the recommended one.

I use F5's NGINX Ingress Controller. Am I affected?
No. F5's controller is a separate, still-maintained project. The retired one is the Kubernetes community's kubernetes/ingress-nginx.

Can I just upgrade ingress-nginx to fix the November CVE?
There is nothing to upgrade to. The final upstream release is v1.15.1 (March 2026); the CVE was published eight months later with an explicit "will not fix." Only vendor forks (e.g. SUSE's Rancher Prime build) carry patches.

How long does the migration take?
As planning guidance, not a promise: a host with plain prefix routing and TLS converts in hours. An estate with snippets, external auth, and regex rewrites is measured in weeks, and the long pole is always those annotation rows — which is why the inventory comes before any date is announced. (Cluster-wide change discipline in our Kubernetes upgrades guide applies unchanged.)

What about InGate, the planned replacement?
Retired before it matured — the November 2025 announcement retires it alongside ingress-nginx. Do not plan a migration onto it.

Sources: Kubernetes blog, "Ingress NGINX Retirement: What You Need to Know" (11 Nov 2025); Kubernetes blog, "Before You Migrate: Five Surprising Ingress-NGINX Behaviors You Need to Know" (27 Feb 2026); Kubernetes Security Response Committee advisory, CVE-2026-116472 (5 Nov 2026); Gateway API v1.7.0 release notes (kubernetes-sigs/gateway-api); ingress2gateway project (kubernetes-sigs). Version and CVE facts checked against these sources on 12 Nov 2026.