On March 19, 2026, kubernetes/ingress-nginx shipped version 1.15.1 — its final release. No more patches. No more CVE fixes. After March, every cluster still running it carries an unpatched ingress controller on its most exposed surface: the thing that terminates TLS and routes every inbound request.
Three weeks earlier, on February 27, the Gateway API project shipped v1.5 — its biggest release yet — promoting six widely requested features from the experimental channel to the Standard (GA) channel. The timing is not coincidental. SIG Network has been steering new adoption away from Ingress for two years, and the retirement notice that landed in November 2025 made the destination explicit: Gateway API is the replacement, and the migration tooling is ready.
If you run a single app behind one Ingress, the migration is a weekend task. If you run a self-hosted PaaS that provisions a domain, a certificate, and a routing rule per tenant — hundreds of objects, all flowing through the same controller — the migration is a fleet-wide cutover where a missed annotation silently drops traffic for every tenant at once. This post is the inventory you need before you start.
The inventory: what translates cleanly, what needs hand-work, and what has no equivalent
This is the core deliverable. Every row below maps a common ingress-nginx annotation or Ingress field to its Gateway API equivalent, and flags whether ingress2gateway handles it automatically or you write the replacement by hand.
| Ingress / annotation | Gateway API equivalent | ingress2gateway? | Notes |
|---|---|---|---|
spec.rules[].host + spec.rules[].http.paths | HTTPRoute.spec.hostnames + HTTPRoute.spec.rules[].matches | ✅ Automatic | Direct structural mapping. Path types (Prefix, Exact) map to path.type in HTTPRoute. |
spec.tls[].hosts + spec.tls[].secretName | Gateway.spec.listeners[].tls.certificateRefs + HTTPRoute attachment | ✅ Automatic | TLS termination moves from Ingress to the Gateway listener. One Gateway with multiple listeners replaces N Ingresses sharing a controller. |
nginx.ingress.kubernetes.io/rewrite-target | HTTPRoute.spec.rules[].filters[].type: URLRewrite | ✅ Automatic | The most common annotation. ingress2gateway converts rewrite patterns to URLRewrite filters correctly for simple cases. |
nginx.ingress.kubernetes.io/ssl-redirect | Gateway.spec.listeners with TLS mode + HTTPRoute redirect filter | ⚠️ Partial | Simple ssl-redirect: "true" maps to an HTTPS listener with redirect. Conditional redirects need a hand-authored RequestRedirect filter. |
nginx.ingress.kubernetes.io/cors-allow-origin and related cors-* | HTTPRoute.spec.rules[].filters[].type: CORS (new in Gateway API 1.5, GEP-1767) | ⚠️ Partial | Before 1.5, CORS required implementation-specific extensions. Now it is a Standard-channel filter — but ingress2gateway coverage for the full cors-* annotation family is incomplete. Expect to hand-verify allow-methods, allow-headers, and max-age. |
nginx.ingress.kubernetes.io/auth-type: basic + auth-secret | HTTPRoute + implementation-specific authentication policy | ❌ Manual | No Standard-channel auth resource yet. Gateway API 1.5 has Gateway/HTTPRoute-level authentication in the experimental channel (GEP-1494). Until it graduates, auth stays vendor-specific: Envoy Gateway uses SecurityPolicy, Cilium uses CiliumNetworkPolicy, etc. |
nginx.ingress.kubernetes.io/rate-limit-* | BackendTrafficPolicy or vendor extension | ❌ Manual | Rate limiting has no Standard-channel API. Each implementation exposes it differently. |
nginx.ingress.kubernetes.io/canary (weighted canary) | HTTPRoute.spec.rules[].backendRefs[].weight | ⚠️ Partial | Weight-based splitting maps cleanly to backendRefs weights. Header/cookie-based canary conditions need hand-authoring. |
nginx.ingress.kubernetes.io/affinity: cookie (session affinity) | BackendTLSPolicy / Service session affinity or BackendTrafficPolicy | ❌ Manual | Sticky sessions via NGINX's consistent-hash annotation have no direct Gateway API equivalent. Some implementations offer it as an extended feature; none is Standard. |
nginx.ingress.kubernetes.io/server-snippet / configuration-snippet | Nothing | ❌ No equivalent | Raw NGINX config injection is the single hardest migration category. Any server-snippet or configuration-snippet is arbitrary NGINX directives with no Gateway API translation. Each one needs a manual rewrite against the target implementation's extension model — or a redesign that removes the need. |
nginx.ingress.kubernetes.io/proxy-body-size / proxy-read-timeout | BackendTrafficPolicy or HTTPRoute.spec.rules[].timeouts | ❌ Manual | Timeouts graduated toward Standard in 1.5-era discussions but body-size limits remain implementation-specific. |
spec.ingressClassName | GatewayClass + Gateway | ✅ Automatic | ingress2gateway generates the GatewayClass and Gateway resources from the ingress class. Review the generated Gateway.spec.listeners — especially if you had custom NGINX ConfigMap tuning. |
nginx.ingress.kubernetes.io/whitelist-source-range | BackendTLSPolicy / implementation NetworkPolicy | ❌ Manual | IP allowlisting has no Standard API. |
The pattern is clear: routing, TLS, and rewrites translate automatically; policy (auth, rate limiting, affinity, raw config) does not. Count your server-snippet and configuration-snippet annotations before you estimate the migration — they are the long tail that determines whether this is a one-day ingress2gateway run or a multi-week rewrite.
What Gateway API 1.5 actually promoted — and why each one matters for migration
Gateway API v1.5 (released February 27, 2026, with a v1.5.1 patch following shortly after) is described by SIG Network as its biggest release yet. The headline is not new resources but graduation: six features that previously required the experimental install channel now ship in the standard channel, meaning they are covered by the GA compatibility guarantee and available without opting into alpha CRDs.
Here is what moved, in the order that matters for a migration:
1. ListenerSet (GEP-1713) — Before 1.5, a Gateway owned all its listeners. Adding a listener for a new tenant domain meant editing the shared Gateway object — a write contention point and a blast-radius risk when one bad listener spec could invalidate the whole Gateway. ListenerSet lets listeners be defined as separate resources that attach to a Gateway. For a PaaS that provisions domains per tenant, this is the primitive that makes "one Gateway per fleet, many tenant-owned listeners" a safe ownership boundary instead of a single object every tenant's controller writes to.
2. HTTPRoute CORS filter (GEP-1767) — CORS handling on Ingress was annotation soup: cors-allow-origin, cors-allow-methods, cors-allow-headers, cors-max-age, each as a separate annotation string. Gateway API 1.5 promotes a typed CORS filter on HTTPRoute rules to Standard. If your tenants set CORS via Ingress annotations, this is the direct replacement — but as the inventory table notes, ingress2gateway does not yet translate the full family automatically.
3. TLSRoute v1 (GEP-2643) — TLSRoute graduates from v1alpha2 to v1 in the Standard channel. In 1.5 it still exists as v1alpha2 in the experimental channel for backward compatibility, but it will be removed there in 1.6. If you terminate TLS passthrough (as opposed to TLS termination at the Gateway), TLSRoute is the resource. The CEL validation it ships with requires Kubernetes 1.31 or higher.
4. Gateway client certificate validation (GEP-91, GEP-3567) — Mutual TLS at the Gateway listener — spec.listeners[].tls.certificateRefs plus client cert validation — is now Standard. Previously this required implementation-specific extensions. Relevant if your PaaS offers mTLS between tenants or between the platform and tenant workloads.
5. Certificate selection for Gateway TLS origination (GEP-3155) — Controls which certificate the Gateway presents when originating TLS to a backend. Matters when your backends expect a specific client identity from the Gateway, not just any valid cert.
6. ReferenceGrant v1 — The cross-namespace reference grant (letting an HTTPRoute in namespace A attach to a Gateway in namespace B) graduates to v1. For a PaaS where each tenant lives in its own namespace but the Gateway is platform-owned in a shared namespace, ReferenceGrant is the permission primitive that makes the attachment model work without giving tenants write access to the Gateway namespace.
Two additional changes ship alongside the promotions: a new ValidatingAdmissionPolicy called safe-upgrades.gateway.networking.k8s.io that prevents accidentally installing experimental CRDs over standard ones (or downgrading past 1.5), and the deprecation of TLSRoute v1alpha2 from the experimental channel ahead of its removal in 1.6.
Why "no more patches" hits a PaaS harder than a single-app cluster
A single-app cluster with one or two Ingress objects can tolerate a stale controller for a while. The Ingress API resource itself is not deprecated — it remains GA and feature-frozen. What is retired is the controller (kubernetes/ingress-nginx) that reconciles those objects into actual NGINX config and reloads.
For a PaaS, the calculus is different in three ways:
Every tenant domain flows through the same controller. A self-hosted PaaS on Cluster API typically runs one ingress controller (or one Gateway implementation) per workload cluster, and every tenant's Ingress or HTTPRoute is an attachment to it. A CVE in an unpatched ingress-nginx — and the November 2025 retirement notice explicitly cited architectural limitations that make long-term security maintenance impossible — is not one app's risk. It is every tenant's risk through one shared process.
An annotation miss is fleet-wide breakage, not one-app breakage. When a single team migrates one app and misses a rewrite-target or cors-allow-origin, that app breaks. When a PaaS migrates its fleet-wide routing layer and misses an annotation pattern that 40 tenants share (say, a common server-snippet that sets a header every tenant's auth depends on), 40 tenants break simultaneously. The inventory table above is not a reference — it is a pre-flight checklist that needs to be run against every Ingress in the fleet before the cutover, not after.
There is no managed control plane to paper over the gap. On a hyperscaler-managed Kubernetes, the provider might quietly translate old Ingress behavior or keep a fork patched. On a Cluster API fleet on Hetzner bare metal, the operator owns the entire routing layer. The migration is not optional, and there is no vendor absorbing the delay.
ingress2gateway 1.0: the automated path and where it stops
The ingress2gateway tool (now at 1.0, announced alongside the broader Gateway API migration push) converts Ingress resources into Gateway API resources. The basic workflow:
# Install Gateway API 1.5 Standard-channel CRDs
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.5.0/standard-install.yaml
# Deploy a Gateway API implementation (e.g., Envoy Gateway)
helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.5.0 -n envoy-gateway-system --create-namespace
# Convert — dry run first
ingress2gateway print --input-file ingress.yaml --output-file gateway-output.yaml
# Or convert live Ingresses directly
kubectl get ingress --all-namespaces -o yaml | ingress2gateway print -o gateway-output.yamlWhat it handles well: host/path rules, TLS config, rewrite-target, ingressClassName to GatewayClass/Gateway generation.
Where it stops — and this is the part that determines your timeline:
- Any annotation with no Standard equivalent (auth, rate limiting, affinity, body-size limits) produces a warning, not a resource. You must author the replacement against your chosen implementation's extension CRDs.
server-snippet/configuration-snippet— raw NGINX directives — are opaque to the tool. It flags them; it cannot translate them. Each one is a manual investigation: what did this snippet actually do, and what is the equivalent in Envoy Gateway / Cilium / Traefik?- NGINX ConfigMap tuning (worker processes, keepalive, custom error pages) lives outside any Ingress object.
ingress2gatewaydoes not see it. Your Gateway implementation's Helm values orGatewayClassparameters are the new home for global tuning.
Run ingress2gateway in --report mode first to get a count of warnings per annotation type. That count is your real migration estimate — not the number of Ingress objects, but the number of untranslatable annotations.
Picking the replacement: four implementations, one decision
Gateway API is a spec, not an implementation. After you convert the resources, you need a controller that reconciles them. Four options cover most self-hosted fleet needs:
| Implementation | Model | Gateway API 1.5 Standard coverage | Best fit when |
|---|---|---|---|
| Envoy Gateway | Envoy proxy, Kubernetes-native control plane | Full Standard channel including new 1.5 features | You want the reference implementation with the broadest conformance. The default for most new fleets. |
| Cilium | eBPF dataplane, Gateway API as L7 on top of Cilium networking | Strong Standard + L4 routes, Cilium-specific policies for the rest | You already run Cilium as your CNI and want one dataplane for networking + ingress. |
| Traefik | Go-based proxy, Hub/Gateway API provider | Standard channel via Traefik Hub provider (v1.5.1 supported) | You already run Traefik or need its middleware model. Note: benchmarks show Traefik as the slowest Gateway API implementation on update propagation. |
| NGINX Gateway Fabric (F5) | NGINX dataplane, Gateway API-native (distinct from retired ingress-nginx) | Standard channel | You want to stay on NGINX but on the supported, Gateway API-native controller — not the retired annotation-based one. F5's actively maintained replacement. |
For a Cluster API fleet on Hetzner where the routing layer must be self-hosted and the Gateway implementation runs as pods in the workload cluster, Envoy Gateway is the most common default. Cilium is compelling if you already pay the eBPF tax for networking. Avoid choosing based on familiarity with ingress-nginx annotations — the annotation model is exactly what is being retired.
One additional note from Gateway API v1.5's release: the project now ships a ValidatingAdmissionPolicy that blocks installing experimental CRDs over standard ones. If you previously installed the experimental channel to try GRPCRoute or BackendTLSPolicy, you need to delete the safe-upgrades VAP before you can reconcile the channel mismatch. Plan this into your upgrade order: CRDs first, then controllers, then converted resources.
A phased migration checklist that does not take every domain down at once
Fleet-wide routing migrations fail in one of two ways: a bad Gateway spec that rejects all routes, or a slow drift where old Ingresses and new HTTPRoutes coexist and conflict. A phased approach avoids both:
Phase 1 — Inventory (before any YAML changes)
- Run
kubectl get ingress --all-namespaces -o yaml | ingress2gateway print --report report.mdand categorize every warning by annotation type. - Count
server-snippet/configuration-snippetoccurrences. Each one is a manual task — estimate accordingly. - Identify which tenants use auth, rate limiting, or session affinity annotations. These need implementation-specific replacements authored before cutover.
Phase 2 — Install side-by-side
- Install Gateway API 1.5 Standard CRDs and your chosen implementation alongside the existing ingress-nginx controller. Do not remove ingress-nginx yet. Both controllers can coexist — they watch different resource types (
IngressvsGateway/HTTPRoute). - Create a
GatewayClassand a sharedGatewaywith anHTTPRoutefor a single canary tenant. Verify routing, TLS, and any custom filters end-to-end before touching the next tenant.
Phase 3 — Convert and validate per tenant
- Convert each tenant's Ingress to
HTTPRoute+ReferenceGrant(if cross-namespace). Apply and test with a synthetic request before switching DNS or deactivating the old Ingress. - For tenants with untranslatable annotations, author the implementation-specific policy resources (e.g., Envoy Gateway
SecurityPolicyfor auth,BackendTrafficPolicyfor timeouts) and test them in staging.
Phase 4 — Cut over and decommission
- Once every tenant has a validated
HTTPRoute, remove the oldIngressobjects. Only then scale down or remove the ingress-nginx controller. Keep it running until the last Ingress is gone — it costs nothing idle and is your instant rollback. - Delete the
safe-upgradesVAP only if you need to reconcile a channel mismatch from a prior experimental install.
Phase 5 — Harden
- Enable the Gateway API conformance tests for your implementation to catch regressions on upgrade.
- If you used
ListenerSet(new in 1.5), verify that tenant-owned listeners attach correctly and that a malformed listener in one tenant's namespace cannot invalidate the shared Gateway's status.
What this means for a self-hosted PaaS
The ingress-nginx retirement and Gateway API 1.5's GA promotions are two halves of the same message: the annotation-based Ingress model is end-of-life, and the typed, role-oriented Gateway API is ready to replace it — not as an experimental alternative but as the Standard channel.
For a team running a PaaS on owned hardware, the migration is not a Kubernetes hygiene task. It is the routing layer that every tenant's domain depends on. The cost of getting it wrong is not one 404 — it is every tenant's 404 at the same time. The inventory table and phased checklist in this post are the minimum pre-flight before you touch a fleet-wide Gateway spec.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. The routing layer described in this post is part of what bex provisions per tenant behind a single Gateway. Star the repo on GitHub or deploy your first app today.