Skip to main content
DevOps Insightsguides & answers

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 seeIngress 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 supported

2. 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-sensitive

3. 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: /uuid

4. 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

  • Exact matches the whole path, case-sensitively: /abc matches only /abc, not /abc/, /Abc or /abcd.
  • PathPrefix (not Ingress’s Prefix) matches whole path elements, case-sensitively.
  • RegularExpression is 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, URLRewrite and RequestRedirect; nothing happens unless you configure it, and the two may not be combined in one rule.

Where to go next

Sources

Behaviours and examples are the Kubernetes blog's; Gateway API field names are checked against its source. Read on 21 October 2026.

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