Skip to main content

Gateway API v1.5 Is Stable: What a Self-Hosted Fleet Gains by Leaving Ingress Behind, and What the Move Costs

10 min readDora NodaDora Noda
Share
On this page

The most popular Ingress controller in Kubernetes history stopped shipping security patches in March 2026, and the Ingress API it served is frozen — no new features, ever. Meanwhile the replacement spent this year doing the least glamorous and most important thing a successor API can do: graduating features instead of inventing them. Gateway API v1.5, released February 27, 2026, promoted six long-Experimental capabilities to the Standard channel, and v1.6 followed in June by graduating raw TCP and UDP routing. For a self-hosted fleet still routing tenant traffic through Ingress objects and controller-specific annotations, the question is no longer whether the new API is ready. It is what the move gains you, what it costs, and when to schedule it.

Here is the verdict up front; the rest of this post is the evidence.

Your fleetVerdictFirst step
Fresh cluster, no Ingress yetStart on Gateway API nowInstall the Standard-channel CRDs and pick a conformant controller
Small fleet on vanilla Ingress (host/path/TLS, few annotations)Migrate this quarterRun ingress2gateway, hand-fix the gaps, cut over per tenant
Fleet deep in nginx.ingress.kubernetes.io/* annotationsPlan a staged migrationInventory annotations first — snippets and auth rules need rewrites, not conversion
Control plane that generates Ingress objects from codeBudget engineering time, not just ops timeRewrite the route builder against typed HTTPRoute structs alongside the migration

What "stable" now covers that your Ingress annotations were faking​

The Ingress resource was always a thin envelope: hostname, path, backend, TLS secret — with everything interesting bolted on as stringly-typed annotations only one controller understood. Gateway API v1.5's whole thesis is moving those bolt-ons into the typed API contract. The April 2026 release announcement lists six promotions to Standard, and each one retires a category of annotation folklore:

  • ListenerSet lets listeners live outside the Gateway object and merge onto it, so platform and application teams stop editing the same resource — and lifts the practical ceiling past 64 listeners on one shared Gateway.
  • TLSRoute graduates SNI-based routing to v1, with Passthrough mode (encrypted bytes proxied to the backend, gateway never sees keys) and Terminate mode (centralized cert management at the gateway).
  • The HTTPRoute CORS filter makes cross-origin policy a typed filter — origins, methods, headers, credentials, max age — instead of an enable-cors annotation whose semantics varied by controller.
  • Client certificate validation brings frontend mTLS into the Gateway object itself, with AllowValidOnly and AllowInsecureFallback modes.
  • Backend TLS origination lets the gateway present its own client certificate upstream via tls.backend.clientCertificateRef, completing mTLS in both directions.
  • ReferenceGrant hits v1, formalizing the cross-namespace permission slip a route needs to point at a backend in another namespace.

Then v1.6, released June 30, 2026, graduated TCPRoute and UDPRoute to Standard v1 — raw L4 routing by protocol and port, no L7 awareness required — and moved all remaining experimental resources into a separate gateway.networking.x-k8s.io API group with an X prefix, so the experimental/standard boundary is visible at the group level rather than hidden in version strings. Notably, you do not need a cutting-edge cluster to get any of this: anything running Kubernetes 1.30 or later can install the current Gateway API CRDs.

Not every promotion matters equally to a small fleet. Here is the annotation-to-API map that counts, with an honest column for who actually feels each gain:

Ingress annotation habitStable Gateway API equivalentMatters at your scale?
rewrite-target + use-regexHTTPRoute URLRewrite filter, typedYes — day one, every fleet
enable-cors + cors-* familyHTTPRoute CORS filter (v1.5 Standard)Yes — day one if tenants serve browsers
force-ssl-redirectHTTPRoute RequestRedirect filterYes — day one, and portable
proxy-body-size, timeoutsBackend policy / implementation settingsPartly — check your controller's policy object
auth-url / auth-signin (external auth)No portable equivalent in StandardOnly if you use it — this is a rewrite, see below
limit-rps / rate limitingNo portable equivalent in StandardOnly if you use it — controller-specific policy
affinity: cookie (session stickiness)No portable equivalent in StandardOnly if you use it — same story
ssl-passthroughTLSRoute Passthrough mode (v1.5 Standard)Only for end-to-end-encrypted tenants
Canary canary-weightHTTPRoute weighted backendRefsYes — traffic splitting is native, no second Ingress
Cross-namespace backends (impossible)ReferenceGrant v1 (v1.5 Standard)Yes — day one for multi-namespace platforms
One giant shared Ingress controller configListenerSet (v1.5 Standard)At scale — past dozens of listeners or multiple teams
Frontend mTLS via snippetsGateway client-cert validation (v1.5 Standard)Only for agent/mTLS workloads
TCP/UDP ConfigMap hacksTCPRoute / UDPRoute (v1.6 Standard)Only for non-HTTP tenants

The pattern: everything a git-push PaaS does per deploy — host routing, TLS, redirects, CORS, weighted splits — is now typed, conformance-tested API. Everything exotic — auth subrequests, rate limits, sticky sessions, Lua snippets — was never portable and still is not. That split is the whole migration plan in miniature.

Before and after: one tenant route, rewritten by hand and by code​

Take the most ordinary object on a self-hosted PaaS: one tenant's route. Custom domain, TLS termination, a path prefix, CORS for its browser app. The Ingress version:

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tenant-acme
  annotations:
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.acme.example"
spec:
  ingressClassName: nginx
  tls:
    - hosts: [acme.example.com]
      secretName: acme-tls
  rules:
    - host: acme.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: acme-api
                port: { number: 8080 }

Every behavior line that matters — the redirect, CORS, the origin allowlist — is a string in an annotation map, validated by nothing until the controller parses it, and portable to no other controller. The Gateway API version splits the same intent across two role-separated objects:

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: fleet-gateway
spec:
  gatewayClassName: fleet-class   # platform team owns this
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      hostname: "*.example.com"
      tls:
        certificateRefs:
          - name: wildcard-example-com

And the per-tenant route attached to it:

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: tenant-acme
spec:
  parentRefs:
    - name: fleet-gateway
  hostnames: [acme.example.com]
  rules:
    - matches:
        - path: { type: PathPrefix, value: /api }
      filters:
        - type: RequestRedirect     # was force-ssl-redirect: "true"
          requestRedirect: { scheme: https, statusCode: 301 }
        - type: CORS                # was the enable-cors annotation family
          cors:
            allowOrigins: [https://app.acme.example.com]
      backendRefs:
        - name: acme-api
          port: 8080

The annotation map is gone. Redirects and CORS are typed filters the API server validates. The Gateway (ports, TLS, wildcard cert — the platform team's concern) and the HTTPRoute (hostnames, paths, filters — per-tenant, generatable) are separate objects with separate RBAC, which is exactly the seam a multi-tenant platform wants.

That seam matters twice as much when routes are generated, not handwritten. A control plane minting per-deploy Ingress objects builds annotation strings and hopes the controller agrees with its spelling:

go
// Ingress generation: behavior as strings, validated by nobody
ing.Annotations["nginx.ingress.kubernetes.io/enable-cors"] = "true"
ing.Annotations["nginx.ingress.kubernetes.io/cors-allow-origin"] = origin
ing.Annotations["nginx.ingress.kubernetes.io/proxy-body-size"] = "10m"
// typo in a key? wrong value format? you find out in production.

The HTTPRoute equivalent constructs typed structs that fail at compile time or at kubectl apply, and the same object works against any conformant controller:

go
// HTTPRoute generation: behavior as typed fields
rule.Filters = []gatewayv1.HTTPRouteFilter{
    {Type: gatewayv1.HTTPRouteFilterCORS, CORS: &gatewayv1.HTTPCORSFilter{
        AllowOrigins: []gatewayv1.AbsoluteURI{origin},
    }},
    {Type: gatewayv1.HTTPRouteFilterRequestRedirect, RequestRedirect: &gatewayv1.HTTPRequestRedirectFilter{
        Scheme: ptr.To("https"), StatusCode: ptr.To(301),
    }},
}

Portability here is not an abstraction-layer pitch — it is the difference between "switching ingress implementations means re-auditing every annotation string in the fleet" and "switching means changing gatewayClassName and re-running conformance." The project's conformance suite exists precisely so that claim is testable rather than aspirational.

What the migration actually costs​

The honest cost has four line items, and only the first is automated.

1. What ingress2gateway 1.0 does for you. The migration tool hit 1.0 in March 2026, and its ingress-nginx provider recognizes 47 distinct annotations — host/path/TLS rules, rewrites, redirects, CORS, canary weights all convert cleanly. Run it per Ingress, review the output, and the vanilla 80 percent of a fleet converts in an afternoon.

2. What it silently drops. Almost every hard case: configuration-snippet, server-snippet, and auth-snippet (arbitrary nginx config injection), auth-url/auth-signin (external auth subrequests), rate-limit annotations, session affinity, custom load-balance algorithms, and the TCP/UDP services ConfigMaps. The tool warns on stderr for annotations it recognizes but cannot translate, and silently drops ones it does not recognize at all — so the output needs a diff review, not blind trust. Each dropped annotation is a small design decision: reimplement as a controller-native policy object, move the logic into the app, or drop the behavior deliberately.

3. What no tool can convert. If your platform builds Ingress objects in Go with client-go, ingress2gateway never sees them — it converts manifests, not code. The route builder itself needs rewriting against the Gateway API types, which is real engineering time but also the moment you collect the typed-generation payoff from the previous section. Similarly, the TLSRoute promotion carries a version trap the v1.5 announcement calls out explicitly: installing v1.5 Standard over v1.4-era Experimental leaves existing v1alpha2/v1alpha3 TLSRoutes unusable, because those versions are not in the Standard install. Either stay on the Experimental channel or migrate the objects to v1 first.

4. The cutover pattern. There is no flag day. Install the Gateway controller alongside the existing ingress controller, convert routes, shift traffic per tenant via DNS or weighted records, and decommission the old controller last. The ecosystem pieces a self-hosted fleet relies on — cert-manager for certificates, external-dns for records — both speak Gateway API now, so the automation around the routes migrates too rather than anchoring you to Ingress.

Picking a controller for a fleet you own​

The implementations registry now lists more than thirty Gateway controllers, which is a choice problem, not a maturity problem. At v1.5's publication, seven implementations already carried full v1.5 conformance reports — Agentgateway, Airlock Microgateway, GKE Gateway, HAProxy Ingress, kgateway, NGINX Gateway Fabric, and Traefik Proxy — and the v1.6 announcement added more. For a fleet on owned hardware, filter that list three ways:

  • Conformance first. A published conformance report for the version you are installing is the only portable-behavior evidence that matters. An implementation without one is a bet, not a choice.
  • Match the data plane you already operate. NGINX shops get continuity from NGINX Gateway Fabric; Envoy shops get it from Envoy Gateway or kgateway; Cilium-based fleets get a controller inside the CNI they already run. The routing API is portable precisely so the data-plane choice can be boring.
  • Check the surrounding automation. Confirm your cert-manager and external-dns versions support the Gateway and HTTPRoute resources before you commit — the controller is only half the ingress layer.

Do not overthink this step. The entire point of the migration is that the controller becomes a replaceable component under a stable API. Pick the conformant one closest to your current stack, and keep the option value.

The runbook version​

If this post had to fit on one runbook page:

  1. Inventory annotations. List every annotation key in use across the fleet; sort into converts-cleanly, needs-policy-rewrite, and deliberately-dropped.
  2. Pick a controller and install the Standard-channel CRDs. Conformance report required; Kubernetes 1.30 or later is all the cluster needs.
  3. Convert and hand-fix. Run ingress2gateway 1.0, review every stderr warning, rewrite dropped behaviors as native policies, migrate any Experimental TLSRoutes to v1.
  4. Rewrite code-generated routes. If a builder mints Ingress objects, port it to typed HTTPRoute construction and test it against a non-production Gateway.
  5. Dual-run and cut over per tenant. Both controllers live; traffic shifts by DNS; old Ingresses stay until their tenant verifies.
  6. Decommission. Remove the ingress controller, delete the Ingress objects, and close the chapter on annotation-string routing.

Ingress served Kubernetes well for a decade by being simple enough to standardize and vague enough to extend. That vagueness compounded into per-controller dialects, unretired CVEs, and route builders that manipulate strings. Gateway API v1.5 and v1.6 are the release where the replacement stopped being the experimental alternative and became the boring, typed, conformance-tested default. The migration is real work — but it is scheduled work with a tool, a cutover pattern, and an end state better than the start. Schedule it.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Check your move before you migrate

Free browser tools: check a render.yaml or your Render scripts against bex, or turn a Heroku app or docker-compose.yml into a draft render.yaml. Nothing you paste leaves your browser.

Open the migration tools