Part of the series Kubernetes networking

Ingress to Gateway API: A safe, step-by-step migration

Your Ingress objects still work, but the most popular controller behind them no longer gets security fixes. Understand the mapping from Ingress to Gateway API first. Then convert, test side by side, and switch traffic without downtime.

, 15 min read

If your cluster uses Ingress NGINX, it is running software that no longer gets security fixes.

In November 2025, Kubernetes SIG Network announced that Ingress NGINX would get only best-effort maintenance until March 2026, and then nothing: no releases, no bug fixes, no security patches (Kubernetes blog). In January 2026, the Kubernetes Steering and Security Response Committees described it as critical infrastructure in about half of cloud native environments, and warned that staying on it leaves users exposed (statement).

Nothing breaks on the day support ends. Your Ingress objects keep routing traffic. That is exactly why this migration is easy to postpone and risky to ignore.

This guide explains the ideas first: what was retired, how Ingress maps to Gateway API, and which old behaviours quietly change. Then it walks through a migration you can practise: inventory, convert, test side by side, switch traffic, and clean up.

If Ingress or Gateway API is new to you, read how Ingress routes traffic and Gateway API explained first.

Part 1: Understand the concepts

1. What exactly was retired?

Three things have similar names. Only one of them was retired.

Thing What it is Status
Ingress API The Ingress resource type, networking.k8s.io/v1 Still part of Kubernetes. Frozen: no new features, but not removed (Google Open Source blog).
Ingress NGINX One popular controller that implements Ingress, from kubernetes/ingress-nginx Retired after March 2026.
Gateway API The newer routing API: GatewayClass, Gateway, HTTPRoute The recommended successor (Kubernetes blog).

A real-life comparison: the government still accepts the old paper form (Ingress API), but the office that processed most of those forms has closed (Ingress NGINX). You can find another office that accepts the old form, or switch to the new online system (Gateway API).

So there are two valid paths:

  1. Keep Ingress, change the controller. Move to another maintained Ingress controller. This is the smaller change, but you stay on a frozen API and must still translate NGINX-specific annotations.
  2. Move to Gateway API. This is the bigger change, but it is where new Kubernetes networking features are being added.

This guide covers path 2. The committees’ statement warns that none of the alternatives is a drop-in replacement, whichever path you choose (statement).

The NGINX Ingress Controller made by F5/NGINX (nginxinc/kubernetes-ingress) is a different project and was not part of this retirement. Check which one you run before deciding anything.

2. Why Gateway API is the successor

Ingress was designed for simple host-and-path routing. Anything beyond that, such as rewrites, header matching, or canary traffic, was added through controller-specific annotations. The same feature was written differently for every controller, so moving between controllers meant rewriting annotations.

Gateway API fixes this in three ways:

Features are part of the API. Redirects, rewrites, header matching, and weighted traffic splitting are normal fields, not annotations. The same YAML works on any conformant controller.

Responsibilities are split by role. One Ingress object mixed infrastructure and app settings. Gateway API separates them:

Resource Who usually owns it Restaurant comparison
GatewayClass Platform or cloud provider The company that builds restaurant entrances
Gateway Cluster operator One entrance: its address, ports, and TLS certificate
HTTPRoute App team The sign inside that says which table each guest goes to

Sharing is explicit. A Gateway decides which namespaces may attach routes to it. App teams manage their own routes without being able to edit the shared entrance.

3. How Ingress maps to Gateway API

Every part of an Ingress has a home in Gateway API:

Ingress Gateway API
Ingress controller Gateway controller (an implementation of Gateway API)
ingressClassName / IngressClass gatewayClassName / GatewayClass
The controller’s address The Gateway and its listeners
spec.tls (hosts and Secret) Gateway listeners[].tls.certificateRefs
rules[].host HTTPRoute hostnames
paths[].path with pathType: Prefix matches[].path with type: PathPrefix
pathType: Exact type: Exact
pathType: ImplementationSpecific No direct equivalent. Decide what you meant: prefix, exact, or regex.
backend.service.name and port backendRefs[].name and port
Redirect and rewrite annotations RequestRedirect and URLRewrite filters
Canary annotations Several backendRefs with weight
defaultBackend A catch-all HTTPRoute rule for /

The key shift: one Ingress becomes at least one Gateway plus one HTTPRoute. The Gateway holds the entrance details, such as ports and TLS. The HTTPRoute holds the routing rules.

4. Hidden Ingress NGINX behaviours that change

This is the part most migrations get wrong. Ingress NGINX did several things you never configured. Conformant Gateway API controllers do not copy these defaults, so routes that worked can start returning 404.

The Kubernetes project documented five of these behaviours (before you migrate):

Ingress NGINX behaviour What happens after migration
Regex paths match as case-insensitive prefixes. /[A-Z]{3} also matches /uuid. Many Gateway controllers match regex fully and case-sensitively, so /uuid returns 404.
use-regex on one Ingress affects every path on that host, even in other Ingresses. Exact and PathPrefix matches are never silently treated as regex.
rewrite-target silently turns on regex for the host. A URLRewrite filter does not change how paths match.
A missing trailing slash is redirected. /my-path gets a 301 to /my-path/. No automatic redirect. /my-path returns 404 unless you add a redirect rule.
URLs are normalised before matching, so /a/../uuid becomes /uuid. Normalisation differs between controllers, and Gateway API has no setting for it.

Before migrating, ask: does any client depend on routing that works only because of these defaults? Test each important URL on the new setup, including odd ones such as missing trailing slashes or mixed case.

5. Migrate side by side, not all at once

Never delete the old Ingress first and hope the new setup works. Run both at the same time, then move traffic gradually:

1. Inventory     What Ingresses, annotations, and controller do you have?
2. Prepare       Install Gateway API CRDs and a Gateway controller
3. Convert       Write Gateway + HTTPRoute (by hand or with ingress2gateway)
4. Test          Gateway gets its OWN address; test it directly
5. Shift traffic Move DNS or load balancer weight step by step
6. Clean up      Delete Ingresses, uninstall Ingress NGINX

During steps 4 and 5, both entrances work. Customers still use the old one until you move them, and if something breaks, rollback is just pointing DNS back.

This is the approach recommended with the official migration tool: deploy Gateway API next to the existing Ingress, shift traffic with weighted DNS or your load balancer, and only then remove the Ingress (Ingress2Gateway 1.0).

6. What ingress2gateway does, and what it does not

ingress2gateway is the official SIG Network tool that reads Ingress resources and prints the equivalent Gateway API YAML. Version 1.0, released in March 2026, supports more than 30 common Ingress NGINX annotations, each tested for behaviour against the real controller (Ingress2Gateway 1.0).

It is a migration assistant, not a one-click replacement. From the project’s own notes and from testing it:

It does It does not
Reads Ingresses from files, one namespace, or the whole cluster Install a Gateway controller for you
Creates a Gateway, listeners, TLS references, and HTTPRoutes Choose the right gatewayClassName. It copies your old Ingress class name.
Translates common annotations, such as SSL redirect, CORS, and rewrites Translate everything. configuration-snippet and proxy-body-size are not translated.
Prints warnings for things it cannot match exactly Guarantee identical behaviour for regex, timeouts, or URL normalisation
Supports providers including ingress-nginx, kong, traefik, istio, cilium, apisix, and gce Copy annotations across unchanged (README)

Always read its warnings and review the output line by line.

7. Doing it by hand matters too

In the CKA exam and in small clusters, you may not have ingress2gateway. You need to write the Gateway and HTTPRoute yourself from an existing Ingress. The mapping table in section 3 is the whole skill. Part 2 shows both the tool and the manual way.

Part 2: Commands and practice

The example is an online shop. The shop namespace has two Services, web on port 80 and api on port 8080, and a TLS Secret named shop-tls.

1. Inventory what you have

Find every Ingress and which class it uses:

kubectl get ingress -A
kubectl get ingressclass

Find which controller is running, and whether it is the retired one:

kubectl get pods -A -l app.kubernetes.io/name=ingress-nginx -o wide
kubectl get deploy -A -o wide | grep -i ingress

An image such as registry.k8s.io/ingress-nginx/controller means you run the retired Kubernetes project.

List every NGINX annotation in use, with counts. Each one is a feature you must translate or replace:

kubectl get ingress -A -o yaml | grep -o 'nginx.ingress.kubernetes.io/[a-z0-9-]*' | sort | uniq -c | sort -rn

Pay extra attention to use-regex, rewrite-target, configuration-snippet, server-snippet, proxy-body-size, and the timeout annotations. Those are the ones most likely to behave differently.

2. Install the Gateway API CRDs

Gateway API resources are CRDs, not built into Kubernetes. Check whether they exist:

kubectl get crd | grep gateway.networking.k8s.io

If not, install the standard channel from the Gateway API releases page. At the time of writing, the latest release line is v1.6 (v1.6 release):

kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.1/standard-install.yaml

Many Gateway controllers install these CRDs for you. Check your controller’s documentation, so you do not install two different versions.

3. Install a Gateway controller

CRDs only teach Kubernetes the new object types. Something must implement them, just as an Ingress needed an Ingress controller.

Pick a conformant implementation from the official implementations list. Common choices include Envoy Gateway, Istio, Cilium, NGINX Gateway Fabric, Kong, Traefik, and kgateway. Follow that project’s install guide, then check that its GatewayClass is accepted:

kubectl get gatewayclass
NAME   CONTROLLER                                      ACCEPTED   AGE
eg     gateway.envoyproxy.io/gatewayclass-controller   True       1m

Note the NAME. You will need it for gatewayClassName. The output above is an example; yours depends on the controller.

4. The Ingress you are migrating

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop
  namespace: shop
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts: [shop.example.com]
      secretName: shop-tls
  rules:
    - host: shop.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api
                port:
                  number: 8080

In plain words: HTTPS on shop.example.com, with /api going to api and everything else to web. HTTP requests are redirected to HTTPS.

5a. Convert with ingress2gateway

Install it (README):

brew install ingress2gateway
# or
go install github.com/kubernetes-sigs/ingress2gateway@v1.0.0

Convert from a file, a namespace, or the whole cluster:

ingress2gateway print --providers=ingress-nginx --input-file ingress.yaml > gwapi.yaml
ingress2gateway print --providers=ingress-nginx --namespace shop > gwapi.yaml
ingress2gateway print --providers=ingress-nginx --all-namespaces > gwapi.yaml

Running the first command on the Ingress above with v1.0.0 printed this warning:

WARN  Gateway API does not support configuring URL normalization (RFC 3986, Section 6).
      Please check if this matters for your use case and consult implementation-specific details.

And this output (shortened):

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: nginx
  namespace: shop
spec:
  gatewayClassName: nginx              # ← copied from ingressClassName. Change it!
  listeners:
  - name: shop-example-com-http        # ← port 80 listener, added to match NGINX
    hostname: shop.example.com
    port: 80
    protocol: HTTP
  - name: shop-example-com-https
    hostname: shop.example.com
    port: 443
    protocol: HTTPS
    tls:
      certificateRefs:
      - kind: Secret
        group: ""
        name: shop-tls
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: shop-shop-example-com
  namespace: shop
spec:
  parentRefs:
  - name: nginx
    port: 443
  hostnames: [shop.example.com]
  rules:
  - matches:
    - path: { type: PathPrefix, value: / }
    backendRefs:
    - { name: web, port: 80 }
  - matches:
    - path: { type: PathPrefix, value: /api }
    backendRefs:
    - { name: api, port: 8080 }
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: shop-shop-example-com-http
  namespace: shop
spec:
  parentRefs:
  - name: nginx
    port: 80
  hostnames: [shop.example.com]
  rules:
  - matches:
    - path: { type: PathPrefix, value: / }
    filters:
    - type: RequestRedirect
      requestRedirect: { scheme: https, statusCode: 308 }
  # (a second identical redirect rule for /api is also generated)

Notice how the pieces line up with the mapping table:

  • spec.tls became an HTTPS listener with a certificateRefs entry.
  • The ssl-redirect annotation became a second HTTPRoute on port 80 with a RequestRedirect filter.
  • Each Ingress path became a rule with PathPrefix and backendRefs.
  • The Gateway is named nginx and uses class nginx, both taken from the old Ingress class.

5b. Or write it by hand

For the exam, or when you want clean, minimal YAML, write the same thing yourself:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: shop-gateway
  namespace: shop
spec:
  gatewayClassName: eg                 # from: kubectl get gatewayclass
  listeners:
  - name: https
    protocol: HTTPS
    port: 443
    hostname: shop.example.com
    tls:
      mode: Terminate
      certificateRefs:
      - name: shop-tls
  - name: http
    protocol: HTTP
    port: 80
    hostname: shop.example.com
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: shop
  namespace: shop
spec:
  parentRefs:
  - name: shop-gateway
    sectionName: https                 # attach to the HTTPS listener only
  hostnames: [shop.example.com]
  rules:
  - matches:
    - path: { type: PathPrefix, value: /api }
    backendRefs:
    - { name: api, port: 8080 }
  - matches:
    - path: { type: PathPrefix, value: / }
    backendRefs:
    - { name: web, port: 80 }
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: shop-http-redirect
  namespace: shop
spec:
  parentRefs:
  - name: shop-gateway
    sectionName: http
  hostnames: [shop.example.com]
  rules:
  - filters:
    - type: RequestRedirect
      requestRedirect:
        scheme: https
        statusCode: 301

Rule order does not decide the winner. Gateway API picks the most specific match, so /api/orders goes to api even if the / rule were listed first. A rule with no matches matches every path, which is why the redirect route needs none.

If you do not want to serve plain HTTP at all, delete the http listener and the redirect route.

kubectl explain works for these resources too, which helps when you cannot remember a field:

kubectl explain gateway.spec.listeners.tls
kubectl explain httproute.spec.rules.filters.requestRedirect

6. Review before applying

Go through the generated or hand-written YAML with this checklist:

  • gatewayClassName matches a class from kubectl get gatewayclass.
  • The TLS Secret is in the same namespace as the Gateway. If it is elsewhere, a ReferenceGrant is needed.
  • If routes in other namespaces must attach, the listener’s allowedRoutes.namespaces allows them. By default, only routes in the Gateway’s own namespace can attach.
  • Regex paths: remove the (?i) prefix and .* suffix the tool adds unless you really want case-insensitive prefix matching (Ingress2Gateway 1.0).
  • Timeouts and body size limits: check your controller’s defaults (Ingress2Gateway 1.0).
  • Every warning from ingress2gateway is either fixed or deliberately accepted.

7. Apply next to the Ingress and test directly

Apply the new resources. The old Ingress stays in place:

kubectl apply -f gwapi.yaml

Check that the Gateway is ready and has its own address:

kubectl get gateway -n shop
NAME           CLASS   ADDRESS        PROGRAMMED   AGE
shop-gateway   eg      203.0.113.25   True         40s

Check that each route was accepted and its Services were found:

kubectl describe httproute shop -n shop

In the status section, look for both conditions to be True:

Type:    Accepted       Status: True
Type:    ResolvedRefs   Status: True

Now test the new address without changing DNS. --resolve tells curl to use a specific IP for the hostname:

GW=$(kubectl get gateway shop-gateway -n shop -o jsonpath='{.status.addresses[0].value}')

curl -sI --resolve shop.example.com:443:$GW https://shop.example.com/
curl -sI --resolve shop.example.com:443:$GW https://shop.example.com/api/health
curl -sI --resolve shop.example.com:80:$GW  http://shop.example.com/

The first two should return your app’s normal response. The last should return a 301 with a location: https://... header. Add -k only if you are testing with a self-signed certificate.

Repeat these tests for your important URLs, including the edge cases from Part 1: missing trailing slashes, mixed-case paths, and regex paths.

8. Shift traffic gradually

Real users still reach the old Ingress. Move them in steps:

  1. Lower the DNS TTL for shop.example.com a day ahead, so changes spread quickly.
  2. Send a small share of traffic to the Gateway address, using weighted DNS records or your cloud load balancer.
  3. Watch error rates and latency on both paths.
  4. Increase the share step by step until the Gateway handles everything.

Rollback is simple while the Ingress still exists: point DNS back to the old address.

9. Clean up

Only after all traffic has moved and stayed healthy for a while:

kubectl delete ingress shop -n shop
helm uninstall ingress-nginx -n ingress-nginx    # if it was installed with Helm
kubectl delete ingressclass nginx

Then raise the DNS TTL back to its normal value.

10. Troubleshoot

Symptom Likely cause Check
Gateway PROGRAMMED is False or has no address No controller for that class, or the class name is wrong kubectl get gatewayclass, kubectl describe gateway
Route Accepted: False Hostname does not match the listener, or the namespace is not allowed allowedRoutes, listener hostname, sectionName
ResolvedRefs: False Service name or port is wrong, or the Service is in another namespace without a ReferenceGrant kubectl get svc -n shop
TLS handshake fails Secret missing, in the wrong namespace, or wrong type kubectl get secret shop-tls -n shop
404 on a URL that worked before One of the hidden NGINX behaviours: regex, trailing slash, or normalisation Compare the old and new path rules
Request too large (413) The new controller has a different body size limit Your controller’s documentation
Works with --resolve but not for users DNS still points to the old address dig shop.example.com

11. Migration checklist

  • Listed every Ingress, IngressClass, and annotation in use
  • Confirmed which controller you run, and whether it is the retired one
  • Installed Gateway API CRDs (standard channel) and a conformant controller
  • Converted each Ingress to a Gateway and HTTPRoutes, and reviewed every warning
  • Fixed gatewayClassName, TLS Secret location, and namespace permissions
  • Checked regex paths, trailing slashes, timeouts, and body size limits
  • Tested the new address with curl --resolve before touching DNS
  • Shifted traffic gradually, with a tested rollback
  • Deleted the Ingresses and uninstalled Ingress NGINX only after the move

The roles stay small: GatewayClass picks the implementation. Gateway is the entrance: address, ports, TLS. HTTPRoute holds the routing rules. ingress2gateway drafts the YAML; you review it, test it side by side, and move traffic only when it is proven.

References

  1. Kubernetes SIG Network. (2025, November 11). Ingress NGINX Retirement: What You Need to Know. Kubernetes Blog.
  2. Kubernetes Steering and Security Response Committees. (2026, January 29). Ingress NGINX: Statement from the Kubernetes Steering and Security Response Committees. Kubernetes Blog.
  3. Google Open Source Blog. (2026). Posts on Ingress, including the note that the Ingress API is feature-frozen, not removed.
  4. Kubernetes Blog. (2026, February 27). Before You Migrate: Five Surprising Ingress-NGINX Behaviors You Need to Know.
  5. Modebadze, B., & Jin, S. (2026, March 20). Announcing Ingress2Gateway 1.0: Your Path to Gateway API. Kubernetes Blog.
  6. kubernetes-sigs. ingress2gateway README. GitHub.
  7. Kubernetes SIG Network. (2026). Gateway API v1.6: TCPRoute and UDPRoute Graduate to Standard. Kubernetes Blog.
  8. kubernetes-sigs. Gateway API releases and implementations.