In March 2026, Kubernetes retired the ingress controller running in front of roughly half of all cloud-native environments — and the most dangerous thing it left behind was not an unpatched binary. It was five silent routing behaviors baked into millions of Ingress objects, any one of which turns a careful-looking Gateway API migration into a 3 a.m. outage. If you operate a self-hosted PaaS, your routing layer inherited every one of them for free, and now you have to own each explicitly.
The timeline is worth stating plainly because it sets the stakes. The retirement was announced in November 2025: no releases, no bug fixes, no security patches after March 2026. In January 2026 the Steering Committee and the Security Response Committee issued a joint statement calling the situation urgent. The repo was archived in late March.
The proximate cause was IngressNightmare (CVE-2025-1974), a CVSS 9.8 unauthenticated remote-code-execution flaw disclosed in March 2025 that touched an estimated 43% of cloud environments and more than 6,500 clusters — landing on a project maintained by one or two developers in their nights and weekends. The community did the math and chose hospice over resuscitation.
This post is the audit that retirement demands. In February 2026, just before the end, Microsoft's Steven Jin published "Before You Migrate: Five Surprising Ingress-NGINX Behaviors You Need to Know" on the Kubernetes blog: five defaults and side effects that are, quote, "probably present in your cluster today." Each section below maps one of those behaviors to the concrete thing a git-push PaaS — TLS termination, per-tenant subdomains, custom-domain assignment — must now replicate deliberately. Here is the whole audit up front; the rest of the post works it end to end.
| # | Silent behavior | What breaks on a naive Gateway API translation | What your routing layer must own explicitly |
|---|---|---|---|
| 1 | Regex matches are prefix-based and case-insensitive | Routes that matched too much suddenly 404 (/uuid no longer matches /[A-Z]{3}) | Documented match semantics per route; full-match regex by default |
| 2 | use-regex on one Ingress reinterprets every path on that host, across all Ingresses | Typo'd Exact paths that "worked" start 404ing | Per-route match isolation — no host-wide annotation side effects |
| 3 | rewrite-target silently implies use-regex | Rewrites plus silently-regexed paths both change meaning at once | Rewrite as an explicit per-route filter, decoupled from matching |
| 4 | Missing trailing slash gets an automatic 301 to the slashed path | Clients following the free redirect start getting 404s | An explicit redirect policy per route, not inherited magic |
| 5 | URLs are normalized (., .., //) before matching | Backends that relied on canonicalization see raw paths (or don't — depends on the new implementation) | A stated normalization policy, verified against your Gateway implementation |
The regex trap: one annotation poisons a whole hostname
Behaviors one through three are really one trap with three triggers, and it is the nastiest of the five because it violates the isolation boundary a multi-tenant platform depends on.
Start with behavior one. In Ingress-NGINX, a regex path is a case-insensitive prefix match. The Kubernetes blog's example is deliberately absurd: an Ingress with use-regex: "true" and path /[A-Z]{3} — intended to route only three-uppercase-letter paths — happily routes /uuid, /uuid/some/other/path, and anything else starting with three letters.
Gateway API's RegularExpression match type, by contrast, is a case-sensitive full match. A literal translation of that Ingress into an HTTPRoute silently narrows what matches, and traffic the old stack served starts 404ing. To preserve the old behavior you must rewrite the pattern yourself (/[a-zA-Z]{3}.*) — the point being that "preserve" is now a conscious decision, not an inheritance.
Behavior two is where it becomes a platform problem. The use-regex annotation does not apply to the Ingress that carries it. It applies to every path on that hostname across every Ingress-NGINX Ingress in the cluster.
The blog's example: one Ingress sets use-regex: "true" for host regex-match.example.com, and a second, unrelated Ingress on the same host declares an Exact path of /Header (a typo for /headers). You would expect /headers to 404. It returns 200, because the first Ingress's annotation silently converted the second Ingress's Exact match into a case-insensitive regex prefix match.
Now translate that to a PaaS. If tenants share a hostname space — wildcard subdomains, shared preview domains, or a custom-domain setup where multiple Ingresses attach to one host — then one tenant's annotation changes the routing semantics of another tenant's routes. That is not a quirk; that is a routing-isolation violation hiding behind an annotation.
On Gateway API, Exact and Prefix matches are never silently reinterpreted, which restores isolation — but only if your migration audits for every route that was secretly depending on the contamination. Any route whose "working" state was actually a typo rescued by someone else's annotation will break, and it will break in the tenant that never touched regex at all.
Behavior three extends the blast radius further: rewrite-target silently implies use-regex, with all the host-wide side effects above. A team that added a rewrite annotation to strip a path prefix also — without any signal — flipped every path on that host into case-insensitive regex matching.
In Gateway API these are separate concerns: matching is matching, and rewrites are explicit URLRewrite filters attached to specific rules. That separation is the correct design. But the migration has to decompose what the old stack fused: for every rewrite-target in your fleet, you owe two audits, not one — the rewrite itself, and every path on the same host whose match semantics the annotation was secretly altering.
What to own explicitly: per-route match semantics with no cross-route side channels. If your platform generates routes from tenant config (a bex.yml, a dashboard toggle, an API call), the generator must emit the match type and the rewrite as independent, visible fields — and your docs must state that regex means full-match unless the tenant asks otherwise. The host-wide annotation era is over; make sure your route generator never reintroduces it as a convenience.
Redirects and normalization your backends never asked for
Behaviors four and five share a theme: Ingress-NGINX did helpful things nobody configured, and your tenants' clients and backends may depend on them without knowing it.
Behavior four: request /my-path when only /my-path/ exists (as Exact or Prefix), and Ingress-NGINX responds with a 301 Moved Permanently to the slashed path. No annotation, no flag — it just does that (except for regex paths). Conformant Gateway API implementations configure no silent redirects.
So a migration that faithfully translates every route and forgets the redirect changes a 301 into a 404 for every client that requests the un-slashed URL — which includes every bookmark, every hardcoded webhook, and every mobile app build you cannot force-upgrade. The fix is the RequestRedirect filter, applied route by route, which means the migration input is not just "the Ingress objects" but "the Ingress objects plus every redirect the controller was emitting that appears in no object at all." Grep your access logs for 301s before cutover; each one is a redirect you must now declare.
Behavior five is subtler: Ingress-NGINX normalizes URLs before matching — resolving . and .. segments, collapsing duplicate slashes — so /ip/abc/../../uuid and ////uuid both match an Exact path of /uuid. The risk here runs in both directions. Backends that assumed they would only ever receive canonical paths may now receive raw ones, depending on the new implementation; and security-sensitive path checks that sat behind the controller's normalization need re-examination.
Most Gateway API implementations (Istio, Envoy Gateway, Kgateway) normalize . and .. out of the box, but "most" and "your chosen implementation's exact default" are different statements. Normalization is now a property of whichever data plane you picked, not of the Ingress API — so it belongs in your platform's stated routing contract, verified against your implementation's docs, not assumed.
What to own explicitly: a written redirect policy and a written normalization policy, both per-route configurable and both visible to tenants. "Requests to the un-slashed path 301 to the slashed path" and "paths are normalized per RFC 3986 section 6.2 before matching" are platform behaviors your tenants build on whether you document them or not. Document them.
What your routing layer must own explicitly now
Step back and the pattern is clear: Ingress-NGINX was not just a controller, it was an undocumented half of every platform's routing contract. Retirement deletes that half. Here is the ownership checklist for a git-push PaaS's routing layer, each item something the old stack provided silently:
- TLS termination and certificate lifecycle. Gateway API 1.5 — released the same day as the "Before You Migrate" post — graduated ListenerSet, which lets app developers manage TLS certificates without owning the whole Gateway. If your platform provisions per-tenant certs (Let's Encrypt for custom domains, wildcard for subdomains), decide now whether that lifecycle lives in your controller, in cert-manager, or in delegated ListenerSets — and who rotates what.
- Per-tenant subdomain and custom-domain assignment. Hostname-to-tenant binding, wildcard provisioning, custom-domain verification (the TXT-record dance), and conflict handling when two tenants claim overlapping hosts. Under Ingress-NGINX, overlapping-host weirdness was papered over by merge behavior; under Gateway API, hostname conflicts are explicit and must be resolved by your policy, not the controller's accident.
- Redirect policy. Trailing-slash 301s, HTTP-to-HTTPS, apex-to-www, deprecated-path redirects — every redirect your access logs show, declared as filters on specific routes.
- Path-normalization policy. What your data plane canonicalizes before matching, stated in your docs and covered by conformance probes, so a data-plane upgrade cannot silently change it.
- Match semantics per route. Full-match regex by default,
Exact/Prefixmeaning exactly what they say, rewrites as separate filters — with no annotation that can change a route it is not attached to. - An annotation inventory as a migration input. SIG Network's ingress2gateway 1.0 (March 2026) translates 30-plus common annotations into Gateway API, which scopes the mechanical work — but no tool translates behavior. Every
use-regex,rewrite-target, and merge-order-dependent Ingress in the inventory needs the behavioral audit from the table above, not just a syntax conversion.
Item six deserves emphasis because it is where migrations die. A tool that converts 100% of your YAML while preserving 0% of your undocumented behavior reports success and ships an outage. Run ingress2gateway to scope the work, then audit the behaviors it cannot see.
The cutover playbook
With the audit in hand, the migration itself is unglamorous on purpose:
- Inventory first. List every Ingress, every annotation, and — from access logs — every 301 and every request path containing
.,.., or//that currently returns 200. That log-derived list is the set of behaviors no YAML file contains. - Pick the successor deliberately. The ecosystem consensus for most teams is Envoy Gateway as the default Gateway API implementation; kgateway for annotation-heavy configs that want the richest translation surface; Traefik v3, which reads NGINX annotations directly, for the fastest drop-in; and NGINX Gateway Fabric for teams staying in the NGINX family on a Gateway-API-native codebase. There is no wrong answer here except "keep running the archived controller."
- Run both controllers in parallel. Deploy the Gateway stack alongside Ingress-NGINX on a separate address, shift a canary hostname, and diff behavior — status codes, redirect targets, matched backends — not just "does the homepage load."
- Cut over per service or per hostname, starting with the lowest-traffic host that exercises regex, rewrites, and redirects. Verify with probes that assert the old observable behavior (this 301, that match) until you have consciously decided which behaviors to keep and which to drop.
- Decommission loudly. Delete the Ingress-NGINX deployment, remove the IngressClass, and revoke the RBAC — a retired controller left running "just in case" is an unpatched RCE surface with a cluster-admin-adjacent service account. IngressNightmare was the warning; there will be no patch for the next one.
The reference controller is gone; the contract is yours
Ingress-NGINX's retirement carries a lesson beyond routing: the reference implementation was the spec. Nothing in the Ingress API says regex is prefix-based, that one annotation rewrites another object's semantics, or that missing slashes 301. Teams built on those behaviors for a decade anyway, because "what the controller does" is the only contract that ever mattered at 3 a.m. Gateway API is a better contract — explicit matches, explicit filters, conformance profiles you can test against — but only if your platform treats it as a contract to implement deliberately rather than a new set of accidents to inherit.
So run the audit. List the behaviors your routes depend on that appear in no manifest, decide which ones you are keeping, and write them down where your tenants can read them. The next upstream deprecation will come — controllers always churn — and a routing layer that owns its contract explicitly will survive it with a translation pass instead of an incident.
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.



