TL;DR: The Kubernetes community's Ingress NGINX controller stopped receiving releases, bug fixes and security updates after March 2026. Existing deployments keep running, but every new CVE is now yours to mitigate. The Ingress API itself is not being removed, so you have a choice: move to another Ingress controller, or move to Gateway API. This guide gives platform teams a decision framework, a migration sequence that avoids a big-bang cutover, working manifests, and the mistakes that tend to hurt most.
If you run Kubernetes in production, there is a reasonable chance your front door is still ingress-nginx. It is the default in countless Helm charts, tutorials and landing-zone templates. It is also, as of March 2026, a controller with no upstream maintainer pipeline behind it.
That is a different situation from a normal dependency upgrade. Nothing breaks on the day support ends. Pods keep serving traffic, and the container images stay available. The risk is quiet: the next vulnerability in the controller, or in the NGINX configuration it generates, will not get an upstream patch.
This article is written for platform engineers, SREs and engineering leads in the US, UK and Singapore who need to decide what to move to and how to move without an outage. We stay close to the primary sources (the Kubernetes project, the Gateway API project, and the tools' own repositories) and we say plainly where those sources are silent.
Table of Contents
- What actually changed, and what did not
- Step zero: find out what you are really running
- The decision: another Ingress controller or Gateway API
- How Gateway API differs from Ingress
- Target architecture and the migration path
- Working manifests
- Using ingress2gateway, and what it will not do
- Annotation translation: where the real work hides
- Trade-offs at a glance
- Common mistakes
- Migration checklist
- FAQ
- How Syslabs can help
What actually changed, and what did not
The Kubernetes project announced that Ingress NGINX would receive best-effort maintenance until March 2026, after which there would be "no further releases, no bugfixes, and no updates to resolve any security vulnerabilities that may be discovered." The same announcement is explicit that existing deployments continue to function and installation artifacts remain available (Kubernetes blog, 11 Nov 2025).
Three clarifications prevent most of the confusion we see in planning meetings:
- Only the controller is retiring. The Ingress API (
networking.k8s.io/v1, GA since Kubernetes 1.19) is described as feature-frozen, not removed (Google Open Source Blog). - It is a different product from F5's NGINX Ingress Controller. The F5/NGINX Inc. controller is a separate project that continues to be maintained, in open source and commercial forms (same Google post). Mixing up the two is the most common error in internal tickets.
- The stated causes are structural. The Google post cites the "snippet" annotations, which allow raw NGINX configuration injection and are considered a major security risk, and a maintainership gap, with the project sustained by one or two people in their spare time.
That last point matters for your risk assessment. If your clusters let tenants set configuration-snippet-style annotations, you have been carrying a sharp edge regardless of the retirement date.
Step zero: find out what you are really running
Do not start by choosing a target. Start by inventorying. The Kubernetes and Google posts both suggest auditing for controller pods first:
# Find ingress-nginx controller pods (label is common but may vary by install)
kubectl get pods --all-namespaces -l app.kubernetes.io/name=ingress-nginx
# List IngressClasses and which controller each points to
kubectl get ingressclass -o custom-columns=NAME:.metadata.name,CONTROLLER:.spec.controllerThe controller name reported for Ingress NGINX is k8s.io/ingress-nginx. Then inventory what your Ingress objects actually depend on:
# Every Ingress, with the namespaces and classes in play
kubectl get ingress --all-namespaces \
-o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,CLASS:.spec.ingressClassName
# Count distinct nginx annotations in use
kubectl get ingress --all-namespaces -o json \
| jq -r '.items[].metadata.annotations // {} | keys[] | select(startswith("nginx.ingress.kubernetes.io"))' \
| sort | uniq -c | sort -rnThat second command is the most valuable ten seconds in the whole project. The annotation histogram tells you how much of your routing behaviour is portable and how much is NGINX-specific. A cluster whose Ingresses use only host, path and TLS is a weekend job. A cluster with fifty distinct annotations, some of them snippets, is a project.
Also record who owns each Ingress. Migration stalls on ownership far more often than on YAML.
The decision: another Ingress controller or Gateway API
You have two honest options.
Option A: swap the controller, keep the Ingress API. You pick another Ingress controller and keep your Ingress objects. The upside is a smaller diff. The downside, as the Google post notes, is that you may simply move to a different set of implementation-specific annotations, and you are still building on an API that is feature-frozen.
Option B: move to Gateway API. The Kubernetes project recommends this path ("Consider migrating to Gateway API, the modern replacement for Ingress"). The Google post calls it "Ingress 2.0", and says its standard channel has had no breaking changes or API version deprecations for over two years.
Our default recommendation for a platform that will live for several more years is Option B, with one important caveat: you do not have to do it everywhere at once. Option A is defensible when you have a small number of simple Ingresses, a freeze on platform change, or a managed controller from your cloud provider that already covers your needs. Pick it deliberately, not by inertia.
A quick decision aid:
| If this is true... | Lean towards |
|---|---|
| Many teams share clusters and you want self-service routing with guardrails | Gateway API |
| You rely on header/weight-based traffic splitting today via annotations | Gateway API (it is in the spec) |
| You need TCP, UDP or gRPC routing under the same model | Gateway API |
| You have under a dozen simple Ingresses and no appetite for new CRDs | Another Ingress controller |
| Your cloud provider's managed controller already meets your needs | Evaluate the managed option on both APIs |
How Gateway API differs from Ingress
The Gateway API project's own migration guide lists the reasons Ingress feels limiting: a small feature set (TLS termination and simple host/path routing), reliance on non-portable annotations, and a permission model that does not suit shared multi-team clusters (Migrating from Ingress). The differences that matter in practice:
- Explicit entry points. Ingress entry points are implicit. In Gateway API, a
Gatewaydeclares explicit listeners (port, protocol, hostname). - TLS belongs to the listener. Certificates are referenced from the Gateway listener, stored in a Secret, rather than being declared on every Ingress.
- Routes are separate objects.
HTTPRouteis the closest thing to an Ingress. Redirects, header manipulation, traffic splitting and matching on headers, query parameters or methods are expressed in the route, not in annotations. - No default backend. There is no direct equivalent, so you define an explicit catch-all rule such as a
/prefix. - Defined merge and conflict rules. The spec says how routes merge and how conflicts resolve, with more specific matches winning.
- Policy resources instead of annotations. Extension happens through external references and Policy resources; annotations on Gateway API resources are discouraged.
The role model is the real feature
The security model documents who should write what (Gateway API security model). In the simple three-tier version:
| Role | GatewayClass | Gateway | Route |
|---|---|---|---|
| Infrastructure provider | write | write | write |
| Cluster operator | no | write | write |
| Application developer | no | no | write |
Platform teams own the Gateway (listeners, TLS, which namespaces may attach). Application teams own their HTTPRoutes. With Ingress, a developer who can create an Ingress can effectively reach into shared entry-point behaviour. With Gateway API, RBAC can enforce the separation you probably wanted all along.
Target architecture and the migration path
The safest migration runs both data planes side by side and shifts traffic gradually at a layer above the cluster (DNS weights, a global load balancer, or CDN origin weights).
flowchart LR
U[Clients] --> D[DNS / CDN / external LB<br/>weighted origins]
D -->|most traffic initially| L[ingress-nginx<br/>legacy data plane]
D -->|small slice, then growing| G[Gateway API implementation<br/>new data plane]
L --> S[(Services / Pods)]
G --> S
subgraph Platform team
GW[Gateway + listeners + TLS]
end
subgraph App teams
R[HTTPRoute per service]
end
GW --- G
R --> GWThe key property is that both controllers point at the same Services. You are not migrating applications, only the way requests reach them. That makes rollback a DNS or load balancer change rather than a redeploy.
A sequence that works well:
- Inventory and classify every Ingress: portable, needs translation, or needs redesign.
- Choose a conformant implementation and run a proof of concept. The Google post names GKE Gateway, Cilium and Envoy Gateway as examples; the Gateway API implementations page lists Envoy Gateway, Cilium, Istio, NGINX Gateway Fabric and Traefik Proxy as Conformant at the time we checked (implementations). Check the conformance report for the exact Gateway API version and the features you need before you commit.
- Install Gateway API CRDs and a GatewayClass in a non-production cluster first. Pin the CRD version.
- Create the shared Gateway (platform-owned), with listeners and TLS.
- Convert routes in waves, starting with low-risk internal services, using the conversion tool for a first draft and humans for the review.
- Shadow and compare. Send synthetic and replayed traffic to the new data plane and compare status codes, headers and latency.
- Shift real traffic gradually at DNS or the edge. Hold each step long enough to see a full traffic cycle.
- Decommission the old controller only after a quiet period, and delete the Ingress objects in a tracked change.
Working manifests
The following examples follow the Gateway API and cert-manager documentation. Adjust names, class and hostnames to your environment.
A platform-owned Gateway with scoped attachment
Use allowedRoutes so only intended namespaces can attach routes. The security model page recommends selecting on the kubernetes.io/metadata.name label because it is always set to the namespace name; custom labels can be changed by anyone who can label namespaces.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: shared-gateway
namespace: gateway-system
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod # example issuer name
spec:
gatewayClassName: example-class # set to your implementation's class
listeners:
- name: https
hostname: "shop.example.com"
port: 443
protocol: HTTPS
tls:
mode: Terminate
certificateRefs:
- name: shop-example-com-tls
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
kubernetes.io/metadata.name: shopFor cert-manager to issue the certificate, the listener must have a non-empty hostname, tls.mode: Terminate (Passthrough is not supported), and a certificateRefs name. The Secret must live in the Gateway's namespace, because cross-namespace Secret references are not supported by cert-manager's Gateway integration. Gateway API support is enabled with config.gatewayAPI.enabled: true in the Helm values, and the Gateway API CRDs should be installed before cert-manager starts (otherwise restart the deployment) (cert-manager docs).
An application-owned HTTPRoute
This is the route from the Gateway API migration guide, attached to a specific listener with sectionName:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-route
namespace: shop
spec:
parentRefs:
- name: shared-gateway
namespace: gateway-system
sectionName: https
hostnames:
- shop.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /cart
backendRefs:
- name: cart-app
port: 80The route attaches across namespaces because the Gateway's listener allows the shop namespace. Remember there is no implicit default backend: add an explicit rule matching / if the Ingress relied on one.
Weighted canary in the route
Traffic splitting is part of the spec. The guide's example uses proportional weights, not percentages: the sum of weights in a rule is the denominator, weight defaults to 1, a weight of 0 sends no traffic, and a single-backend rule gets everything.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: simple-split
spec:
rules:
- backendRefs:
- name: foo-v1
port: 8080
weight: 90
- name: foo-v2
port: 8080
weight: 10Matching rules take precedence over the weighted split, so you can route a header such as traffic=test to the new version first and only then open it up by weight (traffic splitting guide).
Cross-namespace backends need a ReferenceGrant
If a route in namespace foo points at a Service in namespace bar, the owner of bar must opt in. "Without a ReferenceGrant, a cross namespace reference is invalid."
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-foo-routes
namespace: bar
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: foo
to:
- group: ""
kind: ServiceGrants are additive and cannot conflict, and the to section has no namespace because a grant only covers its own namespace (ReferenceGrant). Your existing Ingress-based setup probably never needed this, so expect it to be the first "why is my route not working" surprise.
Using ingress2gateway, and what it will not do
ingress2gateway is a kubernetes-sigs project that translates Ingress resources into Gateway API resources. At the time we checked, the repository showed release v1.0 supporting Gateway API v1.5.0, with ingress-nginx among the supported providers (repository).
A typical dry run reads from the cluster and prints YAML for review:
# Convert ingress-nginx Ingresses in the current namespace
ingress2gateway print --providers=ingress-nginx
# All namespaces, or from a file, with a specific IngressClass
ingress2gateway print --providers=ingress-nginx --all-namespaces
ingress2gateway print --providers=ingress-nginx --input-file ingresses.yaml
ingress2gateway print --providers=ingress-nginx --ingress-nginx-ingress-class nginxTreat the output as a first draft. The project documents several limits worth reading before you rely on it:
- Annotations are not copied over; they are translated only where they map directly to Gateway API.
- Widely used provider-specific annotations may not be supported.
- Experimental fields (such as
URLRewrite) are excluded unless you pass--allow-experimental-gw-api. - It warns about untranslated fields and unsupported features, and conflicting routes are reported as errors for the one that sorts later (by creation time, then namespace/name).
The Gateway API migration guide says the same thing in different words: test and verify all converted output. Run the generated manifests through a server-side dry run and then through your integration tests. Do not apply tool output straight to production.
Annotation translation: where the real work hides
The ingress-nginx provider README documents what the tool maps. A selection (see the provider README for the full, current list):
| NGINX annotation family | Gateway API target |
|---|---|
canary, canary-by-header(-value), canary-weight(-total) | Header match and backend weights |
rewrite-target | URLRewrite filter (ReplaceFullPath); capture-group references like $1 are flagged |
app-root, permanent-redirect, temporal-redirect | HTTPRequestRedirect |
proxy-*-timeout | Gateway API timeouts |
backend-protocol GRPC/GRPCS | GRPCRoute |
proxy-ssl-* (with verification on) | BackendTLSPolicy |
ssl-passthrough: true | TLSRoute plus a passthrough listener |
use-regex: true | PathMatchRegularExpression |
And what the same page lists as recognised but not converted: canary-by-header-pattern, canary-by-cookie, proxy-redirect-from/to, custom-headers (all with warnings), and proxy-ssl-verify-depth and proxy-ssl-protocols (not supported by Gateway API).
Two honest caveats. First, the README has no complete list of unsupported annotations and says nothing about snippet or auth annotations, so their status cannot be read from that page. Second, the Google post warns that specialised NGINX modules may have no equivalent, so feature parity must be verified against your chosen implementation.
For anything the tool cannot translate, the question is not "what is the equivalent annotation?" but "what is this behaviour for?" A configuration snippet that injects a security header might become a ResponseHeaderModifier filter or an edge/WAF rule. An auth annotation might belong in a policy resource of your chosen implementation, or in the application itself. Each of these is a design decision, and it is the reason migration estimates based on the number of Ingress objects are usually wrong.
Trade-offs at a glance
| Dimension | Stay on ingress-nginx (unsupported) | Another Ingress controller | Gateway API |
|---|---|---|---|
| Upstream security fixes | None after March 2026 | Depends on the project | Depends on the implementation, active spec |
| Manifest change | None | Moderate (annotations differ) | Largest (new kinds, new ownership model) |
| Multi-team guardrails | Weak | Weak to moderate | Strong (role-oriented, RBAC-friendly) |
| Traffic splitting, header matching | Annotations | Controller-specific | In the spec |
| TCP/UDP/gRPC | Limited | Controller-specific | Covered by the API family |
| Portability across vendors | Annotation lock-in | Annotation lock-in | Higher, but policy resources are still implementation-specific |
| Learning curve | None | Low | Higher (more objects to learn) |
The last row of the Gateway API column deserves repeating: portability improves, but it is not absolute. Policy attachment, authentication and rate limiting still differ between implementations. Choose an implementation for the features you need, and read its conformance report instead of assuming parity.
Common mistakes
1. Treating "still works" as "still safe". The controller keeps running after support ends. That is exactly why it gets deprioritised. Put an owner and a date on the migration.
2. Confusing the retiring controller with F5's NGINX Ingress Controller. They are different projects. A procurement decision made on the wrong assumption is expensive to unwind.
3. Counting Ingress objects instead of annotations. The histogram from step zero predicts effort far better than the object count.
4. Applying converter output directly. The tool does not copy annotations and warns about untranslated fields. A route that applies cleanly can still behave differently, for example by dropping a rewrite or a timeout.
5. Forgetting the implicit default backend. Gateway API has no equivalent. Requests that used to hit a default page may now return an error unless you add an explicit catch-all.
6. Leaving allowedRoutes wide open. Allowing routes from all namespaces recreates the shared-entry-point problem. Use Selector on kubernetes.io/metadata.name.
7. Installing CRDs after cert-manager. cert-manager checks for the Gateway API CRDs at startup. If you install them later, restart it, or certificates will silently not be requested.
8. Cutting over in one step. Running both data planes behind weighted DNS or an edge layer costs a little extra capacity and buys a one-line rollback.
9. Ignoring conformance versions. An implementation's conformance report is tied to a Gateway API version and feature set. Match them to what you are using.
10. Migrating the objects but not the operating model. If application teams can still write Gateways, or platform teams hand-edit every route, you have changed the API and kept the bottleneck.
Migration checklist
- [ ] Confirm whether ingress-nginx controller pods exist in each cluster, using the label query above
- [ ] Record each IngressClass and its controller string
- [ ] Produce the annotation histogram and mark each annotation portable / translate / redesign
- [ ] Identify and review any snippet-style annotations and who is allowed to set them
- [ ] Name an owner for every Ingress and every hostname
- [ ] Shortlist two Gateway API implementations and read their conformance reports for your required features
- [ ] Pin the Gateway API CRD version; install CRDs before cert-manager
- [ ] Enable cert-manager Gateway API support and test issuance on a non-production Gateway
- [ ] Define the Gateway ownership model: who writes Gateways, who writes routes, which namespaces may attach
- [ ] Generate draft routes with the converter; review every warning
- [ ] Add explicit catch-all rules where a default backend existed
- [ ] Add ReferenceGrants for every cross-namespace backend
- [ ] Shadow traffic to the new data plane and compare responses
- [ ] Shift traffic in weighted steps at DNS or the edge; define rollback criteria beforehand
- [ ] Keep the old controller idle through one full business cycle, then remove it
- [ ] Delete Ingress objects and old IngressClasses in a tracked change; update runbooks and dashboards
How Syslabs can help
Syslabs works with engineering teams on exactly this kind of platform change: inventorying what is really running, choosing a target that fits the team's operating model, and sequencing the cutover so it is boring. If you want a second pair of eyes on your annotation audit or your Gateway ownership model, see our cloud infrastructure services and DevOps and CI/CD pipeline work, or book a conversation with an engineer.
References
- Kubernetes Blog, "Ingress NGINX Retirement: What You Need to Know" (11 Nov 2025): https://kubernetes.io/blog/2025/11/11/ingress-Nginx-retirement/
- Google Open Source Blog, "The End of an Era: Transitioning Away from Ingress NGINX" (Feb 2026): https://opensource.googleblog.com/2026/02/the-end-of-an-era-transitioning-away-from-ingress-nginx.html
- Gateway API, "Migrating from Ingress": https://gateway-api.sigs.k8s.io/guides/migrating-from-ingress/
- Gateway API, security model: https://gateway-api.sigs.k8s.io/concepts/security-model/
- Gateway API, ReferenceGrant: https://gateway-api.sigs.k8s.io/api-types/referencegrant/
- Gateway API, traffic splitting: https://gateway-api.sigs.k8s.io/guides/traffic-splitting/
- Gateway API, implementations: https://gateway-api.sigs.k8s.io/implementations/
- cert-manager, Gateway API usage: https://cert-manager.io/docs/usage/gateway/
- kubernetes-sigs/ingress2gateway: https://github.com/kubernetes-sigs/ingress2gateway
Ready to plan your own migration? Book a meeting with Syslabs.



