Table of contents
- Learning goal
- Prerequisites
- Why Gateway API replaces Ingress
- The resource model: GatewayClass, Gateway, HTTPRoute
- Setting up Gateway API with Envoy Gateway
- Configuring HTTPRoute: matching, filters, and traffic splitting
- TLS termination and cert-manager integration
- Role separation and RBAC
- What you learned
Learning goal
By the end of this tutorial you will have a working Gateway API setup on a Kubernetes cluster, with Envoy Gateway as the data plane, HTTPRoutes handling path-based and header-based routing, TLS certificates managed by cert-manager, and RBAC roles that let platform engineers own the Gateway while application developers own their routes independently.
Prerequisites
- A Kubernetes cluster running v1.29 or later. Gateway API graduated to GA in v1.0 alongside Kubernetes 1.29. Earlier clusters lack the required CRD support.
kubectlconfigured with cluster-admin access (needed for CRD installation and initial Gateway setup)- Helm 3.x installed locally
- At least one application deployed behind a ClusterIP Service. Gateway API routes traffic to Services, not directly to pods. The linked article covers Service types and when to use each.
- DNS records for your domain(s) that you can point to the Gateway's external IP after setup
- Familiarity with the Kubernetes Ingress model is helpful but not required. The sections below explain where Gateway API diverges.
Why Gateway API replaces Ingress
The Ingress API was designed for one thing: simple HTTP host-and-path routing. Everything beyond that (rewrites, rate limiting, timeouts, canary deployments) was pushed into controller-specific annotations. ingress-nginx alone accumulated over 100 annotations. Those annotations are unvalidated strings. A typo fails silently. Switching from ingress-nginx to Traefik means rewriting every annotation from scratch.
Three problems make this worse over time:
- No protocol support beyond HTTP/HTTPS. gRPC, TCP passthrough, and WebSocket upgrades all require annotation hacks or entirely separate tooling. There is no first-class API for non-HTTP protocols.
- No role separation. Ingress merges infrastructure concerns (which load balancer, which ports, which TLS certificates) and application concerns (which paths route where) into one resource. In multi-tenant clusters this means either overly broad RBAC or tickets back and forth between platform and app teams.
- The ingress-nginx retirement. On March 24, 2026, the
kubernetes/ingress-nginxrepository was archived as read-only. No security patches, no bug fixes. Google's Open Source Blog confirmed the transition away from ingress-nginx as the de facto standard.
Gateway API solves all three. Routing features like header matching, traffic splitting, and request mirroring are part of the spec, not annotations. Typed resources with schemas mean kubectl apply --dry-run=server actually validates your config. And the resource model is designed around organizational roles from the start.
One clarification: the Ingress API itself is not deprecated. Only the nginx-based reference implementation is retired. Maintained controllers like Traefik and HAProxy still support Ingress resources. But the direction is clear.
The resource model: GatewayClass, Gateway, HTTPRoute
Gateway API splits routing into three layers. Each layer maps to a different team or role:
GatewayClass (cluster-scoped, owned by infrastructure provider)
└── Gateway (namespaced, owned by platform team)
└── HTTPRoute / GRPCRoute (namespaced, owned by app developers)
└── Service (backend)
GatewayClass
A cluster-scoped resource, similar to StorageClass. It represents a category of gateway implementations, not a specific instance. Each implementation registers a unique controllerName:
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: envoy-gateway
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
Common controller names as of April 2026:
| Implementation | controllerName |
|---|---|
| Envoy Gateway | gateway.envoyproxy.io/gatewayclass-controller |
| Istio | istio.io/gateway-controller |
| Cilium | io.cilium/gateway-controller |
| NGINX Gateway Fabric | gateway.nginx.org/nginx-gateway-controller |
You typically do not create GatewayClass yourself. The Helm chart for your chosen implementation creates it during installation. Check for it with kubectl get gatewayclass after installing the controller.
Gateway
A namespaced resource representing an actual load balancer or proxy instance. It references a GatewayClass and defines one or more listeners with protocol, port, optional hostname, and TLS configuration:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: prod-gateway
namespace: infra
spec:
gatewayClassName: envoy-gateway
listeners:
- name: http
protocol: HTTP
port: 80
- name: https
protocol: HTTPS
port: 443
hostname: "*.staging.infra.example.com"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: wildcard-tls
By default, a Gateway only accepts Routes from its own namespace. To allow Routes from other namespaces, configure allowedRoutes:
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: "true" # label on allowed namespaces
HTTPRoute
The resource application developers create most often. It attaches to a Gateway via parentRefs and defines routing rules:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-route
namespace: app-team
spec:
parentRefs:
- name: prod-gateway
namespace: infra
hostnames:
- "api.staging.infra.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /v1
backendRefs:
- name: api-svc
port: 8080
Route-Gateway binding is bidirectional: the Route must reference the Gateway via parentRefs, AND the Gateway must permit the Route via allowedRoutes. Both sides must consent. This is a security property, not a convenience feature.
Checkpoint. You should now understand why three resources exist instead of one. GatewayClass defines the implementation, Gateway defines the infrastructure (listeners, ports, TLS), HTTPRoute defines the application routing. Each maps to a different RBAC scope.
Setting up Gateway API with Envoy Gateway
This section uses Envoy Gateway as the implementation. Envoy Gateway is a CNCF sub-project of the Envoy Proxy project, reached GA in March 2024, and passes full v1.4.0 conformance. If you prefer a different implementation (Cilium, Istio, NGINX Gateway Fabric), the Gateway and HTTPRoute manifests remain the same; only the installation step changes.
Step 1: install the Gateway API CRDs
Gateway API CRDs are not bundled with Kubernetes. Install the Standard Channel CRDs (the production-stable set):
kubectl apply --server-side \
-f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
The --server-side flag is required due to CRD size. Without it, annotations exceed the client-side apply limit.
Verify:
kubectl get crd | grep gateway.networking.k8s.io
Expected output (four CRDs at minimum):
gatewayclasses.gateway.networking.k8s.io 2026-04-09T10:00:00Z
gateways.gateway.networking.k8s.io 2026-04-09T10:00:00Z
httproutes.gateway.networking.k8s.io 2026-04-09T10:00:00Z
referencegrants.gateway.networking.k8s.io 2026-04-09T10:00:00Z
Step 2: install Envoy Gateway
helm install eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.7.1 \
-n envoy-gateway-system \
--create-namespace
Wait for the deployment:
kubectl wait --timeout=5m \
-n envoy-gateway-system \
deployment/envoy-gateway \
--for=condition=Available
Expected output:
deployment.apps/envoy-gateway condition met
Verify the GatewayClass was created:
kubectl get gatewayclass
NAME CONTROLLER ACCEPTED
envoy-gateway gateway.envoyproxy.io/gatewayclass-controller True
The ACCEPTED: True status means Envoy Gateway has validated the GatewayClass and is ready to provision Gateways.
Step 3: create a Gateway
# gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: prod-gateway
namespace: infra
spec:
gatewayClassName: envoy-gateway
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All # allow Routes from any namespace (tighten this in production)
kubectl create namespace infra
kubectl apply -f gateway.yaml
Check the Gateway status:
kubectl get gateway -n infra
NAME CLASS ADDRESS PROGRAMMED AGE
prod-gateway envoy-gateway 203.0.113.42 True 30s
PROGRAMMED: True and an ADDRESS value mean the data plane is ready to receive traffic. Note the external IP; you will point DNS records here.
Step 4: create an HTTPRoute
Assuming you have a Service named frontend in namespace default running on port 80:
# frontend-route.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: frontend-route
namespace: default
spec:
parentRefs:
- name: prod-gateway
namespace: infra
hostnames:
- "app.staging.infra.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: frontend
port: 80
kubectl apply -f frontend-route.yaml
Step 5: verify end-to-end
export GATEWAY_IP=$(kubectl get gateway prod-gateway -n infra \
-o jsonpath='{.status.addresses[0].value}')
curl -H "Host: app.staging.infra.example.com" http://$GATEWAY_IP/
You should see the response from your frontend Service. If you get a 404, check that the HTTPRoute's parentRefs namespace matches where the Gateway lives, and that the Gateway's allowedRoutes permits the HTTPRoute's namespace.
Checkpoint. You now have a working Gateway API setup: CRDs installed, Envoy Gateway running, a Gateway with a listener on port 80, and an HTTPRoute forwarding traffic to a backend Service.
Configuring HTTPRoute: matching, filters, and traffic splitting
HTTPRoute supports four match types, combinable with AND logic within a single match block:
Path and header matching
rules:
- matches:
- path:
type: PathPrefix
value: /api/v2
headers:
- type: Exact
name: x-env
value: canary
backendRefs:
- name: api-v2-canary
port: 8080
This rule only matches requests where the path starts with /api/v2 AND the header x-env equals canary. Both conditions must be true.
Multiple rules within an HTTPRoute use OR logic: if any rule matches, it applies.
Request filters
Filters modify requests or responses inline. No annotations needed.
HTTP-to-HTTPS redirect:
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
URL rewrite (strip a path prefix before forwarding):
rules:
- matches:
- path:
type: PathPrefix
value: /legacy-app
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: legacy-svc
port: 8080
Requests to /legacy-app/dashboard arrive at the backend as /dashboard.
Header manipulation:
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
add:
- name: x-request-source
value: "gateway"
remove:
- x-internal-only
One constraint: URLRewrite and RequestRedirect cannot coexist in the same rule. Use separate rules if you need both.
Traffic splitting
The weight field on backendRefs is a proportional ratio, not a percentage. Weights default to 1 when omitted.
A 90/10 canary split:
rules:
- backendRefs:
- name: app-v1
port: 8080
weight: 90
- name: app-v2
port: 8080
weight: 10
To complete the rollout, set v1's weight to 0:
rules:
- backendRefs:
- name: app-v1
port: 8080
weight: 0
- name: app-v2
port: 8080
weight: 1
Compare this with Ingress, where canary deployments required controller-specific annotations like nginx.ingress.kubernetes.io/canary-weight. Here it is part of the spec.
Timeouts (GA since v1.2)
rules:
- matches:
- path:
type: PathPrefix
value: /reports
timeouts:
request: 60s # total client-to-client timeout
backendRequest: 30s # gateway-to-backend timeout
backendRefs:
- name: reports-svc
port: 8080
backendRequest must be less than or equal to request when both are set. These use Go duration strings (300ms, 30s, 5m). The timeout feature graduated to GA in Gateway API v1.2 (November 2024).
Checkpoint. You can now configure path matching, header matching, request filters, traffic splitting, and timeouts, all in typed YAML instead of annotation strings.
TLS termination and cert-manager integration
Adding an HTTPS listener
TLS is configured at the Gateway level, not on the HTTPRoute. Add an HTTPS listener alongside the existing HTTP listener:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: prod-gateway
namespace: infra
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
gatewayClassName: envoy-gateway
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
- name: https
protocol: HTTPS
port: 443
hostname: "app.staging.infra.example.com"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: app-staging-tls # cert-manager creates this
allowedRoutes:
namespaces:
from: All
The cert-manager.io/cluster-issuer annotation tells cert-manager to automatically issue certificates for every HTTPS listener that has a non-empty hostname and tls.mode: Terminate.
cert-manager prerequisites
cert-manager must be installed with Gateway API support enabled:
helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager \
--namespace cert-manager \
--create-namespace \
--set config.enableGatewayAPI=true
Gateway API CRDs must be installed before cert-manager starts. If you installed them in Step 1 of this tutorial, you are set. If cert-manager was installed first, restart its pods after installing the CRDs.
Verifying the certificate
After applying the annotated Gateway, cert-manager detects the HTTPS listener, creates a Certificate resource, and issues the certificate via your ClusterIssuer:
kubectl get certificate -n infra
NAME READY SECRET AGE
app-staging-tls True app-staging-tls 45s
READY: True means the certificate was issued and the Secret exists. If it stays False, check:
kubectl describe certificate app-staging-tls -n infra
kubectl get challenges -n infra
kubectl logs -n cert-manager deploy/cert-manager
HTTP-to-HTTPS redirect
Create an HTTPRoute that attaches only to the HTTP listener and redirects everything to HTTPS:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: http-redirect
namespace: infra
spec:
parentRefs:
- name: prod-gateway
sectionName: http # attach to the HTTP listener only
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
The sectionName field targets a specific listener by name. Without it, the route would attach to all listeners, creating a redirect loop on the HTTPS listener.
Verify the full chain:
curl -v http://app.staging.infra.example.com/
# Expected: 301 redirect to https://...
curl -v https://app.staging.infra.example.com/
# Expected: TLS handshake with valid certificate, response from frontend
Checkpoint. TLS is now handled at the Gateway level with automatic cert-manager integration. Your HTTPRoutes do not need any TLS configuration; they just route traffic that the Gateway has already decrypted.
Role separation and RBAC
Gateway API's three-layer model maps directly to Kubernetes RBAC. This is where the real organizational value lives, especially in multi-tenant clusters.
Platform admin: owns GatewayClass and Gateway
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: gateway-admin
rules:
- apiGroups: ["gateway.networking.k8s.io"]
resources: ["gatewayclasses", "gateways"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
The platform team decides which implementation runs (GatewayClass), what ports are open (Gateway listeners), and which namespaces may attach Routes (allowedRoutes). Application teams never touch these resources.
Application developer: owns HTTPRoute
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: route-developer
namespace: app-team
rules:
- apiGroups: ["gateway.networking.k8s.io"]
resources: ["httproutes", "grpcroutes"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
A developer can ship a new canary route, add a path match, or adjust traffic weights without needing cluster-admin access. They cannot change the Gateway's listeners, ports, or TLS configuration.
One thing to know: as of Kubernetes 1.33, the built-in admin ClusterRole does not include permissions for Gateway API resources. You must create these Roles explicitly. That issue tracks the gap.
Cross-namespace trust with ReferenceGrant
When an HTTPRoute in namespace app-team needs to reference a Service or Secret in namespace infra, a ReferenceGrant in the target namespace must explicitly permit it:
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-routes-to-infra
namespace: infra
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: app-team
to:
- group: ""
kind: Service
Removing the ReferenceGrant immediately revokes access. Keep grants narrow: specify both the source namespace and the target resource type.
For a broader look at Kubernetes RBAC patterns, the RBAC guide covers role design, ClusterRole aggregation, and common mistakes.
What you learned
This tutorial covered:
- Why Gateway API exists: Ingress's annotation sprawl, protocol limitations, and missing role separation drove the design of a typed, multi-layer routing API
- The resource model: GatewayClass (implementation), Gateway (infrastructure), HTTPRoute (application routing), each at a different RBAC scope
- Hands-on setup: installing CRDs, deploying Envoy Gateway, creating a Gateway with listeners, and attaching an HTTPRoute
- Advanced routing: path matching, header matching, request filters, URL rewrites, traffic splitting with weighted backends, and per-route timeouts
- TLS: HTTPS listeners with cert-manager automatic certificate issuance and HTTP-to-HTTPS redirect via sectionName targeting
- RBAC: separate ClusterRoles for platform admins and namespace Roles for developers, with ReferenceGrant for cross-namespace trust
If you are migrating an existing ingress-nginx setup, the ingress-nginx to Gateway API migration guide covers the full conversion workflow with ingress2gateway, parallel operation, and zero-downtime DNS cutover. For a deeper look at Ingress concepts themselves, the Ingress configuration guide covers host-based routing, pathType, and controller selection.