Migrating off Ingress NGINX
The routing that worked for years and then returns 404 after the move: five Ingress NGINX behaviours, how to find them first, and how to keep them in Gateway API.
From the Kubernetes project’s own posts · 21 October 2026
On this page · 11 sections
The short answer
Ingress NGINX, the community Ingress controller, was retired in March 2026. There are no further releases, bug fixes or fixes for security vulnerabilities. Existing installations keep running, and its images and Helm charts stay available, which is why it is easy to leave in place.
Moving the same rules to Gateway API or another controller can still cause an outage, because Ingress NGINX did things your manifests never said: it matched regex paths as case-insensitive prefixes, turned regex on for a whole host, redirected missing trailing slashes and normalised URLs. A literal translation drops all of that, and the first sign is a 404.
Ingress NGINX (kubernetes/ingress-nginx, annotations nginx.ingress.kubernetes.io/*) is not F5’s NGINX Ingress Controller (annotations nginx.org/*). Both use NGINX; only the first is retired. Every example here is from the Kubernetes blog and was not re-run for this page.
Do you run it?
With cluster administrator permissions, the Kubernetes announcement suggests:
kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx
Symptom to cause
| After migrating, you see | Ingress NGINX was |
|---|---|
| Requests that used to work return 404 at the gateway, typically ones whose path is longer than the pattern or in a different case. | Regex paths match as case-insensitive prefixes |
| Paths defined in a different Ingress, which never had a regex annotation, start returning 404 after the move. | use-regex changes every path on the host, in every Ingress |
| Rewritten routes and unrelated routes on the same host both return 404 after the move. | rewrite-target turns regex on, without use-regex |
| Clients that requested /my-path get 404 instead of following a 301 to /my-path/. | A missing trailing slash was redirected |
| Requests with //, /./ or /../ in the path are routed differently, or not at all. | URLs were normalised before matching |
1. Regex paths match as case-insensitive prefixes
With use-regex, Ingress NGINX matches a path pattern such as /[A-Z]{3} as a case-insensitive prefix, so /uuid and /uuid/anything were routed by it. Gateway API implementations based on Envoy, such as Istio, Envoy Gateway and Kgateway, match RegularExpression paths in full and case-sensitively.
Check before you move
curl -sS -H "Host: regex-match.example.com" http://INGRESS_IP/uuid
Keep it, or drop it on purpose
Write the pattern to match what Ingress NGINX actually matched, or use a case-insensitive flag if your implementation supports one.
rules:
- matches:
- path:
type: RegularExpression
value: "/[a-zA-Z]{3}.*" # or "(?i)/[a-z]{3}.*" where (?i) is supported2. use-regex changes every path on the host, in every Ingress
Once any Ingress for a host carries use-regex, Ingress NGINX treats all paths for that host, across all of its Ingresses, as case-insensitive prefix regexes. An Exact path of /Header in another Ingress quietly served /headers too. Gateway API does not reinterpret Exact or PathPrefix matches.
Check before you move
curl -sS -H "Host: regex-match.example.com" http://INGRESS_IP/headers
Keep it, or drop it on purpose
Fix the path you actually meant, or make the match a case-insensitive regex if clients depend on it.
rules:
- matches:
- path:
type: Exact
value: "/headers" # the path you meant, case-sensitive3. rewrite-target turns regex on, without use-regex
The rewrite-target annotation silently adds use-regex, with all the side effects above, for every Ingress on that host. In Gateway API a URLRewrite filter rewrites the path and changes nothing about matching.
Check before you move
curl -sS -H "Host: rewrite-target.example.com" http://INGRESS_IP/ip
Keep it, or drop it on purpose
Rewrite with a URLRewrite filter, and choose each path's match type deliberately.
rules:
- matches:
- path:
type: Exact
value: "/ip"
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplaceFullPath
replaceFullPath: /uuid4. A missing trailing slash was redirected
For an Exact or Prefix path ending in a slash, Ingress NGINX answers the same path without the slash with 301 Moved Permanently to the slashed one. It does not do this for regex paths. Conformant Gateway API implementations configure no redirects you did not ask for.
Check before you move
curl -isS -H "Host: trailing-slash.example.com" http://INGRESS_IP/my-path
Keep it, or drop it on purpose
Add the redirect explicitly with a RequestRedirect filter.
rules:
- matches:
- path:
type: Exact
value: "/my-path"
filters:
- type: RequestRedirect
requestRedirect:
statusCode: 301
path:
type: ReplaceFullPath
replaceFullPath: /my-path/5. URLs were normalised before matching
Ingress NGINX normalises paths before matching (RFC 3986, section 6.2): my/./path becomes my/path, my/../path becomes /path, my//path becomes my/path. Backends may rely on that. Most Gateway API implementations normalise . and .. by default, but the details differ.
Check before you move
curl -sSi -H "Host: path-normalization.example.com" http://INGRESS_IP////uuid
Keep it, or drop it on purpose
Read your Gateway API implementation's documentation on path normalisation, and test these requests against it before switching traffic.
Audit before you move
Behaviours 2 and 3 follow the host, not the Ingress, so start by finding every host that has an Ingress with use-regex or rewrite-target, and then every Ingress on those hosts. Those are the ones whose paths Ingress NGINX was treating as regexes, whatever their own pathType says.
# Hosts where Ingress NGINX treats every path as a regex:
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"))
| .spec.rules[]?.host] | unique | .[]'# Every Ingress on those hosts, including ones with no annotation of their own:
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"))
| .spec.rules[]?.host] | unique) as $h
| .items[] | select([.spec.rules[]?.host] | any(. as $x | $h | index($x)))
| "\(.metadata.namespace)/\(.metadata.name)"'Both commands were tested with jq 1.7.1 on sample Ingress JSON, including an Ingress with no annotation that shares a host with one that has use-regex: the second command lists it, as it should. Then run each behaviour’s check request against Ingress NGINX and again against the new controller before moving traffic.
The Kubernetes manifest analyzer flags Ingress NGINX annotations in a pasted manifest, and when the paste holds several Ingresses for one host, it names the ones that a regex annotation elsewhere changes.
Do not leave a retired controller as the edge of your cluster indefinitely. No security fixes means the next vulnerability stays open.
How Gateway API matches paths
Exactmatches the whole path, case-sensitively:/abcmatches only/abc, not/abc/,/Abcor/abcd.PathPrefix(not Ingress’sPrefix) matches whole path elements, case-sensitively.RegularExpressionis case-sensitive, and its dialect (POSIX, PCRE, RE2 or another) is up to the implementation: read its documentation before porting a pattern.- Rewrites and redirects are explicit filters,
URLRewriteandRequestRedirect; nothing happens unless you configure it, and the two may not be combined in one rule.
Where to go next
- Other changes between Kubernetes releases: Kubernetes 1.34 to 1.37: what breaks.
- When the gateway answers 502 or 504 after the move: 502 Bad Gateway and 504 Gateway Timeout.
- A Service the new route points at has nothing behind it: Service has no endpoints.
Sources
Behaviours and examples are the Kubernetes blog's; Gateway API field names are checked against its source. Read on 21 October 2026.
- Kubernetes blog: Ingress NGINX Retirement: What You Need to Know (11 Nov 2025)
- Kubernetes blog: Before You Migrate: Five Surprising Ingress-NGINX Behaviors (27 Feb 2026)
- Gateway API reference: HTTPRoute
- Gateway API source: HTTPRoute types (apis/v1/httproute_types.go)
- Kubernetes documentation: Ingress controllers
- Kubernetes documentation: Gateway API
- RFC 3986, section 6.2: Comparison ladder (normalisation)
Did this get you to an answer?
No text box on purpose — please do not paste production logs anywhere
Something wrong or out of date? Report it