Skip to content

Gateway API

Gateway API is the modern Kubernetes traffic API model for north-south and some east-west use cases.

It improves on Ingress by separating infrastructure ownership from application routing ownership.

Core resource model

  • GatewayClass: implementation and capability definition
  • Gateway: network entry point and listeners
  • HTTPRoute or other Route objects: traffic matching and backend routing
flowchart LR
  Client --> Gateway
  Gateway --> Service
  Service --> Pods
  HTTPRoute -.configures.-> Gateway

Role separation model

Gateway API is designed around three distinct roles with clear ownership boundaries:

graph TD
    INFRA[Infrastructure Provider\nwrites GatewayClass] --> GW[Cluster Operator\nwrites Gateway]
    GW --> APP[App Developer\nwrites HTTPRoute]
    APP --> SVC[Service / Pods]
  • GatewayClass: owned by the infrastructure provider (e.g. the load balancer vendor or platform team). Defines implementation capabilities.
  • Gateway: owned by the cluster operator. Defines listeners, ports, TLS config, and which namespaces may attach routes.
  • HTTPRoute / other routes: owned by app developers. Defines routing rules within the boundaries the Gateway permits.

This three-tier ownership model eliminates the problem Ingress has: annotations that encode implementation-specific behavior on objects the app team writes but the platform team must maintain.

Why teams adopt Gateway API

  • clear role boundaries between platform and app ownership
  • typed fields instead of annotation-heavy behavior
  • protocol-specific route resources (HTTP, gRPC, TCP, TLS)
  • stronger cross-namespace attachment controls via allowedRoutes

Minimal example

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: public-gateway
  namespace: networking
spec:
  gatewayClassName: nginx
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      hostname: app.example.com
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: app-example-tls
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              expose-via-gateway: "true"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route
  namespace: app
spec:
  parentRefs:
    - name: public-gateway
      namespace: networking
  hostnames:
    - app.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: app-service
          port: 80

Cross-namespace safety

allowedRoutes is a key control. It defines which namespaces may attach routes to a gateway listener.

That single control prevents accidental or unauthorized route attachment in shared clusters.

ReferenceGrant

When an HTTPRoute in one namespace needs to reference a Service in another namespace, the destination namespace must grant permission with a ReferenceGrant:

apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: allow-app-route
  namespace: backend
spec:
  from:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      namespace: app
  to:
    - group: ""
      kind: Service

Without this grant, cross-namespace backend references are rejected. This is a deliberate security boundary.

Route types

Gateway API supports protocol-specific route resources:

Route Protocol Stability
HTTPRoute HTTP and HTTPS GA (v1)
GRPCRoute gRPC GA (v1)
TLSRoute TLS passthrough GA (v1) since Gateway API v1.5.0
TCPRoute Raw TCP Experimental channel (v1alpha2)
UDPRoute Raw UDP Experimental channel (v1alpha2)

TCPRoute and UDPRoute have sat in the Experimental channel considerably longer than the HTTP-family routes, and channel/version status moves release to release. Before you build on one, check the Gateway API release notes and the project's API versioning and channel policy for the version you have installed - and confirm your controller actually implements it, since Experimental-channel CRDs are not installed by default.

Traffic splitting and request filters

Two things a reader migrating off Ingress annotations needs immediately are canary weighting and header manipulation. Both are typed fields in Gateway API rather than controller-specific annotations.

Weighted backendRefs split traffic across backends by relative weight. Weights are relative, not percentages - the share a backend receives is its weight over the sum of all weights in the rule:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-canary
  namespace: app
spec:
  parentRefs:
    - name: public-gateway
      namespace: networking
  rules:
    - backendRefs:
        - name: app-stable
          port: 80
          weight: 90
        - name: app-canary
          port: 80
          weight: 10

A backend with weight: 0 receives no traffic but stays a valid reference, which is how progressive delivery controllers (Flagger, Argo Rollouts) park a backend without deleting the route. Note that weighting applies per rule - a request that matches a different rule is unaffected.

Filters modify a request or response in the data path. RequestHeaderModifier and ResponseHeaderModifier cover the common annotation replacements, and RequestRedirect, URLRewrite, and RequestMirror cover the rest:

  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      filters:
        - type: RequestHeaderModifier
          requestHeaderModifier:
            set:
              - name: X-Environment
                value: production
            add:
              - name: X-Forwarded-Prefix
                value: /api
            remove:
              - X-Internal-Debug
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /
      backendRefs:
        - name: api-service
          port: 8080

RequestMirror deserves a warning: a mirrored request is a real request that really executes against the mirror backend, and its response is discarded. Mirroring a write path double-writes.

Policy attachment

The Route types above describe routing. Everything else a gateway does - upstream TLS, timeouts, retries, health checking, authentication - is expressed through policy attachment: a separate CRD (a "metaresource") that names the object it applies to via targetRefs, rather than adding fields to Gateway or HTTPRoute.

The reason for the indirection is the role separation described above. A cluster operator wants to set a default connection timeout for every route on their Gateway without editing routes they don't own, and sometimes wants to set a value an app team cannot override. Policy attachment encodes both:

  • Defaults flow down the hierarchy. A policy attached to a Gateway sets a default for every Route attached to it; a policy attached to a Route overrides that default for that Route.
  • Overrides flow down too, but win. An override set on the Gateway cannot be relaxed by a Route-level policy - this is the guardrail case.

BackendTLSPolicy is the concrete example that has moved furthest through the process. It describes how the gateway connects to a backend over TLS (the leg after the gateway terminates client TLS), which has no home in HTTPRoute because the backend, not the route, owns that property:

apiVersion: gateway.networking.k8s.io/v1alpha3
kind: BackendTLSPolicy
metadata:
  name: backend-tls
  namespace: app
spec:
  targetRefs:
    - group: ""
      kind: Service
      name: app-service
  validation:
    caCertificateRefs:
      - kind: ConfigMap
        group: ""
        name: backend-ca
    hostname: app-service.app.svc.cluster.local

The practical caution: policy attachment is the least settled part of Gateway API. Which policy CRDs exist, their API versions, and whether a given implementation supports defaults, overrides, or only direct attachment all vary - Istio, Cilium, Envoy Gateway, and NGINX Gateway Fabric each ship their own policy types alongside the upstream ones. Treat the upstream policy attachment documentation as the model and your implementation's docs as the authority on what you can actually apply.

East-west routing (GAMMA)

Gateway API started as a north-south (ingress) API, but the GAMMA initiative (Gateway API for Mesh Management and Administration) extended the same Route types to service-to-service traffic inside the cluster. The mechanism is deliberately small: instead of a Route naming a Gateway as its parent, it names a Service.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: checkout-internal
  namespace: shop
spec:
  parentRefs:
    - group: ""
      kind: Service
      name: checkout          # the Service consumers already call
      port: 8080
  rules:
    - matches:
        - headers:
            - name: x-canary
              value: "true"
      backendRefs:
        - name: checkout-next
          port: 8080
    - backendRefs:
        - name: checkout-stable
          port: 8080

Consumers keep calling checkout exactly as before; the mesh intercepts and applies the route. This is what lets Linkerd retire its ServiceProfile/TrafficSplit CRDs and Istio offer an alternative to VirtualService - the same HTTPRoute shape covers both directions. GAMMA requires a mesh; a plain ingress controller will ignore a Service parentRef.

Migration notes from Ingress

Gateway API and Ingress can coexist.

Recommended migration approach:

  1. deploy controller support for Gateway API
  2. migrate one hostname or route domain at a time
  3. validate policy parity, TLS behavior, and observability
  4. retire equivalent Ingress rules after cutover

Operational checks

kubectl get gatewayclass
kubectl get gateways -A
kubectl get httproutes -A
kubectl describe gateway public-gateway -n networking

Always inspect resource status conditions. Conditions are the fastest way to spot route attachment or listener errors, and the shape is worth knowing because it is where almost every "my route isn't working" answer lives.

A Route's status is reported per parent, since the same Route can attach to several Gateways and succeed on one while failing on another:

kubectl get httproute app-route -n app -o jsonpath='{.status.parents}' | jq
[
  {
    "parentRef": {"name": "public-gateway", "namespace": "networking"},
    "controllerName": "gateway.networking.k8s.io/nginx",
    "conditions": [
      {"type": "Accepted", "status": "True", "reason": "Accepted"},
      {"type": "ResolvedRefs", "status": "False", "reason": "BackendNotFound",
       "message": "Service \"app-service\" not found"}
    ]
  }
]

Read the two conditions separately. Accepted: False means the Gateway rejected the attachment - almost always NotAllowedByListeners (the listener's allowedRoutes doesn't select your namespace) or NoMatchingListenerHostname. ResolvedRefs: False means the attachment worked but something the rule points at doesn't resolve - a missing Service, a wrong port, or RefNotPermitted for a cross-namespace backend with no ReferenceGrant. An empty status.parents entirely usually means no controller is watching that GatewayClass at all.

On the Gateway side, Programmed: True is the condition that says the underlying load balancer actually exists and is configured; Accepted: True with Programmed: False is the signature of a Gateway the controller understood but could not provision.

Summary

Gateway API is a better long-term model for multi-team Kubernetes traffic management. It keeps responsibilities explicit and scales more cleanly than annotation-heavy Ingress patterns.

Further Reading