Skip to main content

No More Annotations: Gateway API v1.5 and v1.6 Make PaaS Ingress 100% Portable YAML

8 min readDora NodaDora Noda
Share
On this page

Every annotation on your Ingress objects is a lock-in contract with one specific controller. nginx.ingress.kubernetes.io/enable-cors: "true" means nothing to Traefik; Traefik's half-dozen CORS middlewares mean nothing to Envoy Gateway; Envoy Gateway's policies mean nothing to Cilium. A git-push PaaS that provisions tenant routes with annotations cannot swap its ingress layer without rewriting every tenant's routing config — which is why most platforms never swap, and slowly fossilize around whatever controller they picked first.

That excuse expired this summer. Gateway API v1.5 (April 2026) promoted six features to the Standard channel — including a native HTTPRoute CORS filter — and v1.6 (June 2026) graduated TCPRoute and UDPRoute to v1, closing the last L4 gap. As of v1.6, a tenant's entire ingress layer — HTTP routes, CORS policy, gRPC services, TLS passthrough, raw TCP — can be expressed in portable, Standard-channel YAML with zero experimental CRDs and zero controller-specific annotations. The listing below is the whole argument; the rest of this post is the evidence and the one catch.

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: tenant-web
spec:
  parentRefs:
    - name: tenant-gateway
  hostnames:
    - app.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      filters:
        - type: CORS
          cors:
            allowOrigins:
              - https://app.example.com
            allowMethods: [GET, POST]
            allowCredentials: true
      backendRefs:
        - name: tenant-web
          port: 8000

And the rest of that tenant's ingress — gRPC plus TLS passthrough — in the same portable style. Notice what is missing throughout: no annotations, no middleware CRDs, no vendor-specific objects.

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
  name: tenant-rpc
spec:
  parentRefs:
    - name: tenant-gateway
  hostnames:
    - rpc.example.com
  rules:
    - matches:
        - method:
            service: billing.BillingService
      backendRefs:
        - name: tenant-rpc
          port: 9000
apiVersion: gateway.networking.k8s.io/v1
kind: TLSRoute
metadata:
  name: tenant-db-tunnel
spec:
  parentRefs:
    - name: tenant-gateway
  hostnames:
    - db.example.com
  rules:
    - backendRefs:
        - name: tenant-db-proxy
          port: 5432

Three route kinds, one Gateway, one API group, all v1, all Standard channel. That is the deliverable v1.5 plus v1.6 hand you. Now the details.

What each release actually contributed

The version history matters because the TODO-item version of this story ("v1.5 moves GRPCRoute out of experimental") is wrong on one point, and getting it wrong leads teams to wait for something that already shipped. Here is the corrected timeline:

ReleaseWhat went StandardWhy a PaaS cares
v1.1 (2024)GRPCRoute graduates to v1gRPC routing needed no experimental CRDs for ~2 years; v1alpha2 removed in v1.2
v1.4 (2025)CORS filter, experimentalFirst native CORS, but with a breaking fix to allowCredentials (aligned to a strict boolean per the CORS spec) — do not build on the experimental shape
v1.5 (Apr 2026)HTTPRoute CORS filter (GEP-1767), TLSRoute v1, ListenerSet, Gateway client-cert validation, cert selection for TLS origination, ReferenceGrant v1CORS, TLS passthrough, cross-namespace refs, and multi-team Gateway sharing all become GA at once
v1.6 (Jun 2026)TCPRoute + UDPRoute graduate to v1Raw TCP/UDP (databases, game servers, custom protocols) no longer needs Services-with-hacks or vendor CRDs; v1alpha2 deprecated with a removal clock running

The honest headline is therefore slightly longer than the catchy one: v1.1 standardized gRPC, v1.5 standardized everything around it, and v1.6 standardized what sits below it. The combination is what makes the "100% portable" claim true — v1.5 alone still left L4 on the experimental channel.

Two of the v1.5 promotions deserve a closer look because they solve specifically platform-shaped problems. ListenerSet lets multiple teams share one Gateway: the platform team owns the Gateway and its listeners, and each tenant team attaches routes to a slice of it, instead of every tenant needing a dedicated Gateway (with its own load balancer, IP, and cert lifecycle) or the platform inventing vendor-specific sharing glue. For a PaaS that provisions per-tenant routing on shared infrastructure, that is the difference between one load balancer per tenant and one load balancer per fleet.

ReferenceGrant going v1 matters for the same reason at the cross-namespace boundary: a tenant's route in namespace A referencing a shared gateway or backend in namespace B is now a stable, GA trust primitive rather than an alpha API you apologize for in the docs. And Gateway client-certificate validation plus certificate selection for TLS origination close the mTLS story at the edge — the Gateway can now verify who is calling and pick which identity to present upstream, both as spec-defined behavior instead of controller-specific policy CRDs.

Before and after: what the annotations were doing

To feel the size of the change, take CORS — the single most annotation-heavy piece of ordinary tenant ingress. Before v1.5, each controller invented its own surface. Here is the same policy ("let https://app.example.com call my API, cookies included") on classic NGINX Ingress:

yaml
metadata:
  annotations:
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.example.com"
    nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST"
    nginx.ingress.kubernetes.io/cors-allow-credentials: "true"

Four annotations, one controller, zero portability — Traefik would want a Middleware CRD plus an annotation referencing it, and Envoy Gateway would want a SecurityPolicy with a cors stanza. Three controllers, three config languages, one semantic. The v1.5 CORS filter collapses all three into the filters block shown at the top of this post: allowOrigins, allowMethods, allowCredentials, expressed once, interpreted by any conformant implementation. Multiply that collapse by every tenant on the platform and every controller migration you will ever consider, and the annotation tax stops looking like background noise.

The same collapse already happened for gRPC (GRPCRoute matches on service and method instead of header-hack HTTPRoutes), for TLS passthrough (TLSRoute v1 instead of SNI annotations or TCP-with-TLS ConfigMaps), and now for L4 (TCPRoute/UDPRoute v1 instead of falling back to bare Services that bypass the Gateway entirely). Every row in the table above deletes one category of per-controller glue from a platform's routing layer — and per-controller glue is exactly what makes swapping ingress controllers a migration project instead of a config change.

The catch: Standard spec is not supported implementation

Here is the gotcha the happy path hides, and the part a platform team must get right before tenants rely on any of this: a feature graduating to the Standard channel means the API is stable, not that your chosen controller implements it. Portable YAML that no controller in your fleet honors is just a different kind of lock-in — lock-in to a CRD nobody reconciles. Before cutting over, run this checklist:

  1. Install CRDs at v1.5 or newer (v1.6 if you want TCP/UDP). The standard-install bundle is the floor; experimental-channel installs are no longer needed for any of the objects in this post.
  2. Read the implementation's conformance report, not its marketing. Cilium reports conformance including GRPCRoute and BackendTLSPolicy; Traefik tracks spec v1.5.1; NGINX Gateway Fabric has moved to Gateway API 1.6.1. "Supports Gateway API" without a versioned feature list is a vibe, not a contract.
  3. Watch for silent skipping on upgrade. Envoy Gateway's move to reconciling TCPRoute/UDPRoute via the v1 API requires the v1.6 CRDs installed — without them, TCP/UDP routes are silently skipped, and the stored version moves to v1, which means a storage-version migration is owed before v1alpha2 disappears. Test the CRD upgrade on a staging fleet first.
  4. Respect the v1alpha2 removal clock. v1.6 deprecated the v1alpha2 TCPRoute/UDPRoute shapes; they keep working for now, but manifests referencing gateway.networking.k8s.io/v1alpha2 have a numbered future. Grep the fleet for them during the same upgrade window. This is also a preview of the discipline Standard-channel adoption demands in general: every graduation so far — GRPCRoute's v1alpha2 removal in v1.2, now TCP/UDP's deprecation in v1.6 — has come with a migration window, not a flag day. The platforms that got burned were the ones running experimental-channel objects in production and treating "it applies cleanly" as "it is supported." If your fleet still has experimental GRPCRoute or TLSRoute objects from early adoption, the v1.6 upgrade is the right moment to move them to v1 alongside everything else, in one audited pass rather than three panicked ones.

None of these is hard; all of them are the kind of thing that bites at 2 a.m. precisely because the YAML applied cleanly and the controller said nothing. Budget an afternoon for the checklist now, or budget a week for the incident later.

What this removes from a git-push PaaS

For a platform that provisions one Gateway per tenant (or per fleet, shared via ListenerSet — itself newly Standard) and mints routes on every git push, the payoff compounds. Route provisioning becomes controller-agnostic template rendering: the same HTTPRoute/GRPCRoute/TLSRoute objects land identically whether the fleet runs Envoy Gateway on Hetzner today or Cilium next year. Rolling out a new controller becomes a GatewayClass change validated against the conformance suite, instead of a tenant-by-tenant annotation rewrite audited by hand. The "swap test" — could you change GatewayClass and have every tenant route keep working? — goes from a quarter-long migration to a rollout strategy. Conformance reports become the vendor-selection document, replacing annotation-compatibility spreadsheets. And tenant-facing docs can finally teach one routing model instead of "here is how CORS works on our current controller, sorry."

The direction of travel is unambiguous: Ingress still works and is not being removed, but every new routing feature lands in Gateway API, the conformance suite grows every release, and each graduation deletes another reason to keep annotation glue. The platforms that move their route provisioning onto Standard-channel objects now buy themselves the cheapest possible ingress-controller market — the ability to leave.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Portable Gateway API routes are exactly the kind of boring, standard infrastructure a self-hosted PaaS should be built on. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex