Kubernetes Gateway API: from Ingress successor to production routing

Gateway API is the official Kubernetes successor to the Ingress API. It separates infrastructure from application routing, supports advanced traffic management out of the box, and has been GA since Kubernetes 1.29. This tutorial walks you through the resource model, setting up Envoy Gateway, configuring HTTPRoutes with path matching and traffic splitting, terminating TLS with cert-manager, and mapping the role-based model to your team's RBAC.

Table of contents

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.
  • kubectl configured 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:

  1. 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.
  2. 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.
  3. The ingress-nginx retirement. On March 24, 2026, the kubernetes/ingress-nginx repository 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.

Recurring server or deployment issues?

I help teams make production reliable with CI/CD, Kubernetes, and cloud—so fixes stick and deploys stop being stressful.

Explore DevOps consultancy

Search this site

Start typing to search, or browse the knowledge base and blog.