Kubernetes Gateway API vs Ingress: Differences & Migration Guide (2026)
In November 2025 the Kubernetes project announced the retirement of Ingress NGINX, the community ingress controller running in a large share of clusters. Best-effort maintenance ended in March 2026, the repository was archived, and there are no further releases or security fixes. Existing deployments keep working, but nobody will patch the next vulnerability.
That makes this the right time to understand Gateway API, the upstream-backed successor to the Ingress resource. This guide explains what changed, compares the two APIs with real YAML, helps you pick a controller, and gives a migration plan using the official ingress2gateway tool.
Table of Contents
- What happened to Ingress NGINX
- Ingress vs Gateway API at a glance
- The Gateway API model
- Side-by-side YAML
- Choosing a Gateway API implementation
- Migration plan
- Using ingress2gateway
- Annotation mapping
- Bare-metal and homelab notes
- Troubleshooting
- FAQ
1. What happened to Ingress NGINX
| Date | Event |
|---|---|
| 11 Nov 2025 | Kubernetes SIG Network and the Security Response Committee announce the retirement of Ingress NGINX |
| March 2026 | Best-effort maintenance ends; no more releases, bug fixes, or security patches |
| 20 Mar 2026 | ingress2gateway 1.0 released as the official migration tool |
| 24 Mar 2026 | The kubernetes/ingress-nginx repository is archived (read-only) |
Nothing breaks overnight: running controllers continue to work and the installation artifacts remain available. The risk is cumulative, because any new vulnerability stays unpatched. Plan a migration rather than waiting for an incident to force one.
Check which NGINX controller you actually run. There are two different projects:
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.spec.containers[*].image}{"\n"}{end}' | grep -i nginx
| Image contains | Project | Status |
|---|---|---|
registry.k8s.io/ingress-nginx/controller | Community ingress-nginx (Kubernetes project) | Retired |
nginx/nginx-ingress or nginxinc | F5 NGINX Ingress Controller | Separate project, not covered by this retirement |
2. Ingress vs Gateway API at a glance
| Ingress | Gateway API | |
|---|---|---|
| API group | networking.k8s.io/v1 | gateway.networking.k8s.io/v1 |
| Core resources | Ingress, IngressClass | GatewayClass, Gateway, HTTPRoute, GRPCRoute, and more |
| Protocols in the standard channel | HTTP/HTTPS | HTTP, HTTPS, gRPC, TLS; TCP and UDP routes are available in the experimental channel |
| Traffic splitting, header matching, redirects, rewrites | Controller-specific annotations | Built into the API |
| Role separation | One object, mixed concerns | Infra team owns Gateway; app teams own HTTPRoute |
| Cross-namespace routing | Awkward or unsupported | First-class, controlled by ReferenceGrant |
| Portability | Annotations differ per controller | Conformance tests across implementations |
| Status | Frozen (stable, no new features) | Actively developed |
The Ingress API is not being removed, but it is feature-frozen. New networking capabilities land in Gateway API.
3. The Gateway API model
Infrastructure provider Cluster operator Application developer
----------------------- ---------------- ---------------------
+-------------------+ +----------------+ +--------------------+
| GatewayClass | <------ | Gateway | <-------- | HTTPRoute |
| (which controller)| | (listeners: | | (hostnames, paths, |
| | | ports, TLS) | | headers, weights) |
+-------------------+ +--------+-------+ +---------+----------+
| |
data plane (Envoy, NGINX, ...) v
gets a LoadBalancer / NodePort +-----------+
| | Service |
Client ---> cloud LB / MetalLB ---------+---------------------> | + Pods |
+-----------+
| Resource | Owned by | Purpose |
|---|---|---|
GatewayClass | Infrastructure provider | Names the controller implementation |
Gateway | Cluster operator | Defines listeners (ports, protocols, TLS certificates) and who may attach routes |
HTTPRoute / GRPCRoute | Application team | Maps hosts, paths, and headers to backend Services |
ReferenceGrant | Target namespace owner | Permits cross-namespace references (for example, a route using a Service or Secret in another namespace) |
4. Side-by-side YAML
Today, with Ingress and annotations:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
namespace: shop
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/canary: "true" # needs a second Ingress object
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
ingressClassName: nginx
tls:
- hosts: [shop.example.com]
secretName: shop-tls
rules:
- host: shop.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: shop-api
port:
number: 80
With Gateway API:
# 1. Owned by the platform team
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: envoy
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: public-gw
namespace: infra
spec:
gatewayClassName: envoy
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
- name: https
protocol: HTTPS
port: 443
hostname: "shop.example.com"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: shop-tls
allowedRoutes:
namespaces:
from: All
# 2. Owned by the application team: redirect HTTP to HTTPS
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-redirect
namespace: shop
spec:
parentRefs:
- name: public-gw
namespace: infra
sectionName: http
hostnames: ["shop.example.com"]
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
---
# 3. Routing with a built-in 90/10 canary split
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-api
namespace: shop
spec:
parentRefs:
- name: public-gw
namespace: infra
sectionName: https
hostnames: ["shop.example.com"]
rules:
- matches:
- path:
type: PathPrefix
value: /api
backendRefs:
- name: shop-api-v1
port: 80
weight: 90
- name: shop-api-v2
port: 80
weight: 10
Notice what disappeared: no vendor annotations for the redirect, and the canary is two weighted backendRefs on one route rather than a second Ingress object with magic annotations.
The TLS Secret shop-tls in this example lives in the same namespace as the Gateway (infra). If it lives elsewhere, the Secret’s namespace must allow it with a ReferenceGrant:
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-gateway-to-read-certs
namespace: shop # namespace that owns the Secret
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: infra
to:
- group: ""
kind: Secret
5. Choosing a Gateway API implementation
Gateway API itself is only the specification and CRDs. You still need a controller that implements it.
| Implementation | Data plane | Good fit | Notes |
|---|---|---|---|
| Envoy Gateway | Envoy | Direct replacement for a shared cluster edge; rich policy extensions | CNCF-hosted, popular for teams leaving Ingress NGINX |
| NGINX Gateway Fabric | NGINX | Teams that want to stay on NGINX semantics | From F5/NGINX, implements Gateway API |
| Traefik | Traefik | Existing Traefik users; can also read NGINX Ingress annotations as a stop-gap | Useful for phased migrations |
| Cilium | eBPF + Envoy | Clusters already using Cilium CNI | Integrated with the network layer |
| Istio | Envoy | Service mesh users | Gateway API is Istio’s recommended API |
| Cloud load balancer controllers | Managed LB | EKS, GKE, AKS | Check provider support for your features |
Check each project’s conformance page for the Gateway API version and features it supports. Vendor-specific extras (rate limiting, external auth, timeouts) are exposed through policy resources that are not portable between implementations.
6. Migration plan
1. Inventory ---> 2. Install CRDs ---> 3. Deploy new ---> 4. Convert
kubectl get + chosen controller Gateway beside with ingress2gateway
ingress -A old Ingress |
v
7. Retire <--- 6. Cut over DNS / <--- 5. Test with Host header, review output
ingress-nginx LB gradually staging hostnames by hand
- Inventory. List every Ingress and its annotations.
kubectl get ingress -A kubectl get ingress -A -o yaml | grep 'nginx.ingress.kubernetes.io' | sort | uniq -c | sort -rnThe annotation counts show how much custom behavior you must re-create. - Install the Gateway API CRDs and a controller. Use the standard channel unless you need experimental routes.
# Check the latest release at github.com/kubernetes-sigs/gateway-api/releases GATEWAY_API_VERSION=v1.6.0 kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/${GATEWAY_API_VERSION}/standard-install.yaml kubectl get crd | grep gateway.networking.k8s.ioThen install your chosen controller, typically with Helm. See Managing Kubernetes Applications with Helm Charts. - Run in parallel. Deploy the new Gateway with its own load balancer address next to the old controller. Do not touch production DNS yet.
- Convert with
ingress2gatewayand review every generated manifest. - Test using a Host header against the new address:
curl -s -H "Host: shop.example.com" http://<new-gateway-ip>/api curl -sk --resolve shop.example.com:443:<new-gateway-ip> https://shop.example.com/api -I - Cut over gradually. Lower DNS TTLs in advance, move a small share of traffic first (weighted DNS or load balancer rules), watch error rates, then shift the rest.
- Retire the old controller only after a full business cycle without incident. Keep manifests in Git so you can roll back. A GitOps workflow helps; see Argo CD on Kubernetes.
7. Using ingress2gateway
ingress2gateway reads existing Ingress resources and prints Gateway API equivalents. Version 1.0 arrived on 20 March 2026 and widened support for Ingress NGINX annotations considerably compared with the early 0.x releases.
# Install: download the binary from the project's GitHub releases page,
# or use your package manager (for example: brew install ingress2gateway)
chmod +x ingress2gateway
sudo mv ingress2gateway /usr/local/bin/
# Convert everything in the cluster (dry run: output only)
ingress2gateway print --providers=ingress-nginx --all-namespaces
# One namespace
ingress2gateway print --providers=ingress-nginx --namespace shop
# From a file, saved for review
ingress2gateway print --providers=ingress-nginx --input-file=legacy-ingress.yaml -o yaml > converted.yaml
Use
--providers=ingress-nginxfor the retired community controller. The tool also has a provider for F5’s separate NGINX controller; picking the wrong one produces misleading output.
The tool is a starting point, not an autopilot. It cannot translate everything, and it flags or skips annotations that have no Gateway API equivalent. Always read the generated routes, check parentRefs, hostnames, and filters, and add the policy resources your chosen controller needs.
8. Annotation mapping
| Ingress NGINX annotation (concept) | Gateway API equivalent |
|---|---|
ssl-redirect / force-ssl-redirect | RequestRedirect filter (scheme https) on an HTTP listener route |
rewrite-target | URLRewrite filter |
Canary weight (canary, canary-weight) | Weighted backendRefs in one HTTPRoute |
| Canary by header or cookie | Header matches on separate rules |
proxy-set-headers, response header changes | RequestHeaderModifier / ResponseHeaderModifier filters |
enable-cors | CORS filter where the implementation supports it, otherwise a vendor policy |
| Timeouts, body size, buffering | Vendor-specific policy resources (not portable) |
Rate limiting (limit-rps, limit-connections) | Vendor-specific policy; check your controller |
External auth (auth-url, auth-signin) | Vendor-specific policy or a different mechanism |
| Basic auth | Vendor-specific policy |
configuration-snippet, server-snippet | No equivalent. Re-express the behavior with standard filters or policies, or rethink the requirement |
| TCP/UDP services ConfigMap | TCPRoute / UDPRoute (experimental channel), if your controller supports them |
Roughly speaking, routing logic maps well; operational tuning (timeouts, limits, auth) lands in implementation-specific policies, so budget time for it.
9. Bare-metal and homelab notes
On a kubeadm cluster such as the one in our Kubernetes cluster on Rocky Linux 10 guide, the Gateway’s data-plane Service of type LoadBalancer stays in <pending> unless something assigns an address. Use MetalLB (the same fix described in our NGINX Ingress guide for Rocky Linux 10), or expose the proxy with hostNetwork/NodePort and put your own load balancer in front.
On Rocky Linux nodes, open the listener ports in firewalld on the nodes that receive traffic, and keep SELinux enforcing. Review the hardening advice in Kubernetes Security Best Practices (2026 Edition) when exposing a new edge proxy, including NetworkPolicies and TLS Secret handling. For managing certificates and Secrets at scale, see Kubernetes Secrets Management.
10. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
no matches for kind "HTTPRoute" | Gateway API CRDs not installed | Apply the standard-install manifest |
Gateway PROGRAMMED: False or no address | Controller not running, wrong gatewayClassName, or no LoadBalancer provider | kubectl describe gateway; check the controller pods; install MetalLB on bare metal |
Route status Accepted: False | parentRefs name/namespace/sectionName wrong, or allowedRoutes blocks the namespace | kubectl describe httproute; fix parentRefs or listener allowedRoutes |
Route status ResolvedRefs: False | Backend Service missing, wrong port, or cross-namespace without ReferenceGrant | Check the Service name and port; add a ReferenceGrant |
| TLS handshake fails or default certificate served | Secret missing, wrong namespace, or hostname mismatch on the listener | Verify the Secret type is kubernetes.io/tls and the listener hostname |
| 404 on every request | No route matches host or path | Compare hostnames and matches; test with the correct Host header |
| Converted route behaves differently from the old Ingress | Annotation had no equivalent | Review the tool’s warnings; add a policy resource or change the app |
| Works from inside the cluster, not outside | Firewall, NodePort, or load balancer path | Check firewalld on nodes, MetalLB pool, and security groups |
| HTTP to HTTPS redirect loops | Redirect route attached to the HTTPS listener | Attach the redirect route to the HTTP listener only (sectionName: http) |
| Wrong provider flag output | Used the F5 provider for community ingress-nginx | Use --providers=ingress-nginx |
Useful commands:
kubectl get gatewayclass,gateway,httproute -A
kubectl describe gateway -n infra public-gw
kubectl describe httproute -n shop shop-api
11. FAQ
Is the Ingress API deprecated?
It is not being removed, but it is frozen. New capabilities go to Gateway API, and the retirement of the most popular controller pushes most teams toward the new API.
Do I have to migrate to Gateway API, or can I just switch to another Ingress controller?
You can swap to another Ingress controller and keep your manifests, which buys time. Gateway API is the longer-term direction, and moving once is cheaper than moving twice.
Which controller is the easiest replacement for Ingress NGINX?
Envoy Gateway is a common choice for a shared cluster edge. NGINX Gateway Fabric suits teams that prefer NGINX, and Traefik can ease the transition by reading NGINX annotations. Evaluate feature coverage (auth, rate limiting, timeouts) before deciding.
Does ingress2gateway migrate everything automatically?
No. It converts routing and many common annotations, but you must review the output and re-create unsupported behavior, such as snippets, with standard filters or vendor policies.
What is the difference between a Gateway and an HTTPRoute?
A Gateway defines listeners (ports, protocols, TLS) and is owned by the platform team. An HTTPRoute defines how requests for specific hosts and paths reach Services and is owned by application teams.
Can Gateway API handle TCP and gRPC?
gRPC has its own GRPCRoute. TCP and UDP routes exist in the experimental channel; confirm that your controller supports them before relying on them.
How do I do canary releases?
Use weighted backendRefs in an HTTPRoute, as in the example above, and shift the weights gradually.







