Skip to main content

Still on ingress-nginx? Your Migration Starts With an Audit, Not a Tool

11 min readDora NodaDora Noda
Share
On this page

An Ingress with the path /[A-Z]{3} and use-regex: "true" looks like it routes exactly one thing: three uppercase letters. Point it at a backend and curl /uuid — four lowercase letters — and the request sails through with a 200. Ingress-nginx regex matches are prefix-based and case-insensitive, so /uuid matches, and a migration that ports that pattern to a Gateway API RegularExpression match (full match, case-sensitive on Envoy-based implementations) turns working traffic into a 404 on cutover day.

That is behavior number one of five in the Kubernetes project's companion guide, published February 27, 2026, one month before the retirement it was written for. Its recurring warning deserves quoting in full: a seemingly correct translation can still cause outages if it does not consider ingress-nginx's quirks. Seven months after the repository went read-only, the fleets still on ingress-nginx are not short on migration walkthroughs — this blog alone has published the annotation translation table, the four-phase cutover, and the Gateway API v1.5 payoff. What most of them skipped is the layer underneath all of that: read the January retirement statement closely, then audit what your fleet silently depends on before any conversion tool runs. This post is that layer.

What the January statement actually says​

On January 29, 2026, the Kubernetes Steering Committee and Security Response Committee issued a joint statement confirming the November 2025 retirement announcement. Maintenance ends March 2026. Datadog research puts ingress-nginx in about half of cloud-native environments, and the project had been held together by one or two people working in their free time. Fleets cite this statement constantly and misread it just as often. Here is what the two documents actually say, against the five assumptions that refuse to die:

What fleets assumeWhat the documents actually say
The Ingress API is going away, so we must leave Ingress-shaped routing entirelyIt retires one controller, kubernetes/ingress-nginx. It explicitly blesses migrating to "Gateway API or one of the many third-party Ingress controllers" — staying on the Ingress API with a maintained controller is a conforming outcome
F5's NGINX Ingress is affected tooThe February companion guide states plainly that ingress-nginx and F5 NGINX Ingress are separate, unrelated controllers sharing only a dataplane. Only the community project retired
Things will break visibly so we will notice in time"Existing deployments will continue to work, so unless you proactively check, you may not know you are affected until you are compromised." Silence is the failure mode, not the all-clear
Someone — a fork, a vendor, the community — will keep shipping security fixes"There will be no more releases for bug fixes, security patches, or any updates of any kind" — and maintaining the codebase is "no longer reasonable or even possible ... even if resources did materialize"
There is a drop-in replacement that makes this a swap, not a project"None of the available alternatives are direct drop-in replacements. This will require planning and engineering time."

Start with the statement's own detection step, which costs thirty seconds and settles whether any of the rest applies to you:

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

If that returns controller pods, also check for the image directly (catches nonstandard labels) and for Helm releases carrying the frozen chart:

bash
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{"  "}{.spec.containers[*].image}{"\n"}{end}' | grep 'ingress-nginx/controller'
helm list -A | grep -i ingress-nginx

What "no updates of any kind" has looked like since March​

The statement's bluntness invited a reasonable suspicion: surely critical fixes would still ship. The seven months since are the answer, and every entry is dated:

DateWhat happened
March 19, 2026Final patch releases ship (1.15.1 is the last community release). Whatever you run after this, you run as-is
March 20, 2026Ingress2Gateway 1.0 ships, expanding ingress-nginx annotation coverage from three annotations to 30+ with per-annotation Parsed/Unsupported/Unrecognized tracking
March 24, 2026The repository is archived read-only. No releases, bugfixes, or CVE fixes of any kind from that moment on
August 7, 2026O3 Security reports that CVE-2026-4342 — fixed in the March 19 release — still lurks in production clusters that never applied it, with no upstream left to send a second warning
September 2026CVE-2026-42533, a critical NGINX heap overflow, lands with the frozen controller's embedded NGINX in the affected range and no community release to move to
November 2026Microsoft's support bridge — critical security patches for the AKS application-routing add-on's managed NGINX — ends. After that, managed NGINX gets no Azure support either

"No updates of any kind" turned out to be literal, exactly as written. November 2026 is now one month away, and it is the last date on this timeline with anyone else's name on it. That is the decision the January statement demands: not whether to migrate, but whether the audit below starts this week on your schedule or later on a disclosure's schedule.

The audit, in order​

Do these in order, and do all of them before running a conversion tool. Each item ends with what "done" looks like — an audit without a definition of done is a vibe.

0. Confirm exposure. Item done when you can name every cluster running the controller, the exact image tag on each, and whether the admission webhook is still enabled. The commands from the previous section give you the first two; the webhook matters because it was the IngressNightmare attack surface (CVE-2025-1974, CVSS 9.8, unauthenticated RCE, March 2025), and disabling it is the single highest-leverage hardening step for any fleet that stays a month longer.

1. Inventory every annotation. Item done when you hold a sorted count of every nginx.ingress.kubernetes.io/* key across all namespaces, with each row marked portable, rewritable, or dead-end:

bash
kubectl get ingress -A -o json | jq -r '.items[].metadata.annotations // {} | keys[]' | sort | uniq -c | sort -rn

Most fleets find the same shape: a long tail of boring keys (issuer, timeouts, body size) and a short list carrying the entire migration risk. Treat these as guilty until proven portable: configuration-snippet and server-snippet (raw config injection — the bug class that killed the project), auth-url / auth-signin (external-auth chains), canary + canary-weight (traffic splitting), whitelist-source-range (IP allowlists), custom-http-errors (rewrites your error surface), and ssl-passthrough (L4 passthrough with its own controller-flag requirement). Rows without a Gateway API equivalent are your actual project plan, measured in rewrites owed, not manifests converted.

2. Probe the five surprising behaviors. This is the February guide's core, and the step migration walkthroughs assume someone already did. Item done when each behavior below has a verdict for your fleet: preserve deliberately (reproduce it in the new config) or fix deliberately (correct the manifests so the quirk stops mattering). The guide's curl demos prove each quirk; the fleet-scale question is where each one hides in your manifests:

#BehaviorWhere it hides in your fleet
1Regex matches are prefix-based and case-insensitiveEvery use-regex path: assume each pattern matches a larger, case-folded set than its author intended, then check access logs for what actually arrives
2One Ingress's use-regex silently regex-ifies all paths of that host across all IngressesList every Ingress with the annotation, then list every other Ingress sharing those hosts — their Exact paths are regexes today and will not be tomorrow
3rewrite-target silently implies use-regex, with all of behavior 2's side effectsSame host-sharing sweep as behavior 2, keyed on rewrite-target instead — run both sweeps, since either annotation flips the whole host to regex
4Exact or Prefix paths missing a trailing slash get a 301 to the slashed pathEvery path ending in /, plus controller access logs sampled for 301s — any client following those redirects needs an explicit redirect filter on the new side
5URLs are normalized (., .., //) before matchingAccess-log sampling for unnormalized request paths; most Envoy-based Gateway implementations normalize . and .. too, but verify yours rather than assuming it

The two sweeps worth scripting today:

bash
kubectl get ingress -A -o json | jq -r '.items[] | select(.metadata.annotations["nginx.ingress.kubernetes.io/use-regex"]=="true") | .spec.rules[].host' | sort -u
kubectl get ingress -A -o json | jq -r '.items[] | select(.metadata.annotations | has("nginx.ingress.kubernetes.io/rewrite-target")) | .spec.rules[].host' | sort -u

Every host either command prints is a host whose routing semantics change under a naive translation. Those hosts get migrated with explicit before/after route tests, not with a bulk conversion.

3. Document default-backend behavior. Item done when you can answer: what serves a request that matches no host today? Unmatched traffic in ingress-nginx lands on the default backend (a 404 page, unless you customized it via custom-http-errors or a custom default backend service). Gateway API has no default-backend concept — unmatched requests get whatever your implementation returns, typically its own 404. That gap is invisible in every translation table because there is no source field to mistranslate; it only shows up as a changed error surface your status pages, synthetic monitors, and API clients may depend on. Record today's behavior with one unmatched-host curl per environment before you migrate, so the new side can be held to it.

4. Document TLS-termination semantics. Item done when every TLS edge case below is written down with the config that produces it:

  • Which Secrets terminate which hosts (spec.tls blocks), and what serves clients with no SNI match — ingress-nginx falls back to its --default-ssl-certificate, a behavior you must consciously reproduce or retire.
  • Whether anything relies on ssl-passthrough (requires the controller's explicit enable flag; it is L4 passthrough, not termination, and ports to a TLSRoute-shaped answer, not an HTTPRoute one).
  • Whether backends expect re-encrypted traffic (backend-protocol: HTTPS), which ports to backend-TLS policy on the new implementation, not to anything in the route match.

TLS is where "the converter handled it" fails most quietly, because a translation can be syntactically perfect and still terminate, pass through, or re-encrypt differently. The audit output here is a per-host table — terminate where, with which cert, passthrough or re-encrypt — that the converted manifests get diffed against.

Where this ends and the walkthroughs begin​

The audit above is the input; conversion is a separate stage that consumes it. The sequence from here:

  1. Run ingress2gateway print against the audited manifests and triage its per-annotation tracking — Parsed, Unsupported, Unrecognized — against your item-1 inventory. Anything the tool drops or flags should already be on your rewrites-owed list; if it is not, the audit missed it. Our translation-table post shows what that triage looks like on a realistic ten-annotation tenant set.
  2. Stand up the new controller alongside ingress-nginx (separate LoadBalancer, separate IP) and dual-serve, shifting traffic per hostname with the before/after route tests from item 2 gating each move.
  3. Cut over hostname by hostname, then decommission in the order that closes the attack surface first: admission webhook off, controller deleted, CRDs last. The full no-flag-day choreography is in our four-phase cutover guide.

Note what this post deliberately did not do: recommend a controller, convert a single manifest, or move a single DNS record. Those are execution decisions with good guides already written. The audit is the decision layer those guides assume — and the step whose absence turns a planned migration into a month of diffing 404s.

One month remains before the last vendor patch bridge ends. The audit above is a day's work for most fleets: thirty seconds to confirm exposure, an afternoon for the annotation inventory and host sweeps, and a morning of access-log sampling for the behaviors manifests cannot show. Run it this week, while the schedule is still yours.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Sources​

Related articles

Check your move before you migrate

Free browser tools: check a render.yaml or your Render scripts against bex, or turn a Heroku app or docker-compose.yml into a draft render.yaml. Nothing you paste leaves your browser.

Open the migration tools