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 definitionGateway: network entry point and listenersHTTPRouteor 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:
- deploy controller support for Gateway API
- migrate one hostname or route domain at a time
- validate policy parity, TLS behavior, and observability
- 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:
[
{
"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.
Related Concepts¶
- Ingress
- Services
- Network Policies
- Envoy - the proxy underneath Envoy Gateway, Contour, Istio, and several other implementations
- Istio - a mesh implementation of both the north-south and GAMMA sides of this API
- Security Primer