Skip to main content

Five Surprising Ingress-NGINX Behaviors to Audit Before Migrating to Gateway API

9 min readDora NodaDora Noda
Share
On this page

Ingress-NGINX is dead, and your migration tool's exit code is lying to you. The repository went read-only in March 2026, no CVE will ever be patched again, and Datadog telemetry cited in the retirement statement put the controller in roughly half of all cloud-native clusters. If you are one of those clusters, you have probably already run ingress2gateway 1.0, watched it emit clean Gateway API YAML, and felt done. You are not done.

The Kubernetes blog's "Before You Migrate" post, published February 27, 2026 as the retirement clock ran down, documents five Ingress-NGINX behaviors that silently change what your traffic does when they are mechanically converted to Gateway API routes. A regex that matched becomes a regex that 404s. An Exact path that served traffic starts refusing it. A redirect your clients depend on vanishes. The converter exits 0 through all of it, because it translates syntax, not semantics.

Here is the full audit table up front. The rest of this post works each row with the curl commands that prove the divergence and the Gateway API equivalent that fixes it.

#Ingress-NGINX behaviorSilent failure after naive conversionGateway API fix
1Regex matches are prefix-based and case-insensitiveRequests that used to match now 404Expand the pattern (/[a-zA-Z]{3}.*)
2use-regex applies host-wide across ALL IngressesTypo'd Exact paths in unrelated Ingresses start 404ingFix the typo; scope regex per-route
3rewrite-target silently implies regexSame host-wide regex surprise, with rewritesURLRewrite filter + corrected matches
4Missing trailing slash gets an automatic 301Clients lose redirects they depend onExplicit RequestRedirect rule
5URLs are normalized before matchingDepends on your Gateway implementationVerify ./../// handling per controller

The regex trilogy: behaviors 1 through 3​

The first three behaviors are one story in three acts, and they account for most of the silent 404s in a migration. They all flow from a single design decision: in Ingress-NGINX, regular expressions are case-insensitive prefix matches, while Gateway API's RegularExpression match type is a full, case-sensitive match.

Behavior 1 is the base surprise. Suppose you route all three-uppercase-letter paths to a service with the pattern /[A-Z]{3} and use-regex: "true". Under Ingress-NGINX, a request to /uuid — four lowercase letters — matches, because matching is prefix-based (/[A-Z]{3} matches the first three characters and ignores the rest) and case-insensitive. Convert that pattern verbatim into an HTTPRoute RegularExpression match and /uuid 404s. To preserve the old behavior you must rewrite the pattern as /[a-zA-Z]{3}.*, spelling out both the case-insensitivity and the prefix semantics the old controller gave you for free.

Behavior 2 is where it gets genuinely dangerous. The use-regex: "true" annotation does not apply to the Ingress that carries it. It applies to every path on that host across every Ingress-NGINX Ingress in the cluster. The blog post demonstrates this with a typo: one Ingress sets use-regex for regex-match.example.com, while a completely separate Ingress — no annotations at all — declares an Exact path of /Header (a typo for /headers). Because a sibling Ingress flipped the host into regex mode, /Header is treated as a case-insensitive prefix pattern, /headers matches, and traffic flows with a 200. After migration, Gateway API honors Exact literally, the typo'd route 404s, and the outage is in a manifest nobody touched, owned by a team that never used regex.

Behavior 3 extends the blast radius further: rewrite-target silently implies use-regex, with all of behavior 2's host-wide side effects. An Ingress that only wanted to rewrite /ip to /uuid flips every path on its host into case-insensitive-prefix regex mode — including the Exact paths of other teams' Ingresses. Gateway API's URLRewrite filter does no such thing, which is correct behavior and precisely why the converted routes diverge.

For a self-hosted platform, the audit here is mechanical and should come before any conversion. Find every Ingress carrying either annotation:

bash
kubectl get ingress -A -o json | jq -r '.items[] | select(.metadata.annotations | has("nginx.ingress.kubernetes.io/use-regex") or has("nginx.ingress.kubernetes.io/rewrite-target")) | "\(.metadata.namespace)/\(.metadata.name): \(.spec.rules[].host)"'

Then, for each host on that list, audit every Ingress on the same host — not just the annotated one — because behaviors 2 and 3 mean the annotated Ingress changes what its neighbors match. Any Exact or Prefix path on those hosts that only ever worked due to case-insensitive prefix matching is a 404 waiting for cutover.

The vanishing redirect: behavior 4​

Behavior 4 is the one that breaks clients rather than servers. Given an Exact path of /my-path/, a request to /my-path does not 404 under Ingress-NGINX. The controller responds with 301 Moved Permanently and a Location pointing at the trailing-slash URL. The same holds for Prefix paths (regex paths are exempt). Years of clients, bookmarks, health checks, and third-party webhooks may depend on that redirect without anyone remembering it was never configured — it is controller behavior, not configuration, so no converter can carry it over.

bash
curl -isS -H "Host: trailing-slash.example.com" http://<your-ingress-ip>/my-path
# HTTP/1.1 301 Moved Permanently
# Location: http://trailing-slash.example.com/my-path/

Gateway API has no implicit trailing-slash redirect. If your traffic relies on it, you must add an explicit rule with a RequestRedirect filter returning 301 to the slash-suffixed path, ahead of the rule serving the canonical path. The audit question is empirical, not textual: check access logs for 301s the controller emits today, because the manifests will never tell you they exist.

URL normalization: behavior 5​

Behavior 5 is Ingress-NGINX normalizing URLs before matching them against Ingress rules: . segments are dropped, .. eats the previous segment, and consecutive slashes collapse (my//path becomes my/path). Requests to /ip/abc/../../uuid and even ////uuid match an Exact path of /uuid.

This is the one behavior where the news is mostly good. Istio, Envoy Gateway, and Kgateway all normalize . and .. segments out of the box, so a typical migration inherits equivalent behavior for free. The audit item is the word typical: normalization details vary by implementation, and your backends may have quietly relied on the controller to canonicalize paths before they arrive. Verify your chosen implementation's normalization behavior against your actual traffic — especially any service that reflects or signs raw paths — rather than assuming parity.

What ingress2gateway 1.0 can't translate​

SIG Network shipped ingress2gateway 1.0 on March 20, 2026, days before the retirement deadline, and it is a genuine migration assistant: it translates Ingress resources plus 30+ common annotations, warns about untranslatable configuration, and suggests alternatives. But "warns" is doing heavy lifting. Joining the five behaviors against converter coverage gives the table that actually matters:

InputConverter handlingWhy it can't auto-convert
Behaviors 1–3 (regex semantics)Translates syntax; semantics divergeCase-insensitive-prefix vs full-case-sensitive is a judgment call per route
Behavior 4 (trailing-slash 301)Dropped (nothing in source to translate)The redirect was implicit controller behavior, not configuration
Behavior 5 (normalization)Depends on target implementationNormalization lives in the data plane you pick, not the YAML
configuration-snippet, server-snippetFlagged for manual conversionRaw NGINX directives have no Gateway API equivalent by design
auth-url / auth-snippet (external auth)DroppedEach Gateway implementation wires external auth differently (vendor extension)
Canary header/cookie matchingPartially translatableCore Gateway API matches Path, Headers, QueryParams, Method — cookie matching needs a vendor extension
Session affinity, load-balance, rate limitingDropped or partialBackend traffic policy varies per implementation

The pattern is consistent: anything that was typed configuration converts; anything that was NGINX behavior, an escape hatch, or an annotation with no Gateway API counterpart needs a human. The snippets family deserves special emphasis — it was cited in the January 29, 2026 joint statement from the Steering Committee and Security Response Committee as an unmaintainable security surface and a reason for the retirement itself. Every configuration-snippet in your fleet is not just untranslatable, it is the exact code most worth rewriting deliberately rather than transliterating.

The audit runbook for per-tenant routing​

On a self-hosted PaaS, the unit of migration pain is not a cluster, it is a tenant host. Take the representative setup: a wildcard *.app.example.com for default tenant subdomains plus per-tenant custom domains, each with its own Ingress carrying rewrite rules, external-auth chains, and canary annotations. The checklist, in order:

  1. Inventory annotations per host, not per Ingress. Behaviors 2 and 3 punish host-granular thinking. List every annotation on every Ingress, then group by host so regex-mode and rewrite Ingresses reveal which neighbors they silently affect.
  2. Hand-audit rewrite rules per tenant host. For each rewrite-target, record the intended rewrite and check every co-hosted path for typos or case assumptions that only work in regex mode. Convert to URLRewrite filters with corrected matches.
  3. Hand-audit auth snippets and auth-url chains. Map each external-auth flow to your Gateway implementation's auth extension before cutover. This is the category most likely to need staging validation, because auth failures present as 401/403 storms, not 404s.
  4. Hand-audit canary annotations per tenant. Weight-based splits map cleanly to HTTPRoute weight. Header-based canary maps to header matches. Cookie-based canary has no core equivalent — resolve it with your implementation's extension or redesign the split before migrating that tenant.
  5. Run both controllers side by side. Install the Gateway controller alongside Ingress-NGINX under a different class, translate one tenant host, and shift traffic with weighted DNS or a test hostname. Compare status-code distributions, not just success rates — a 200-to-301 or 200-to-404 class shift is the signature of behaviors 1–4.
  6. Define done as the checklist, not the converter. Decommission an Ingress only when its host's audit rows (regex, rewrite, redirect, normalization, auth, canary) are each verified against live traffic. ingress2gateway exiting 0 was step zero.

Ingress-NGINX served Kubernetes traffic for the better part of a decade, and its quirks became load-bearing in ways no manifest records. The migration that respects that history is slower than the one that trusts the converter — and it is the only one that keeps every tenant's traffic intact on the other side.

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.

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