The three-layer model

  • GatewayClass — cluster admin defines which implementation provides Gateways (e.g. "use Envoy Gateway", "use nginx Gateway Fabric"). Set once per cluster, per implementation.
  • Gateway — platform team creates one or more. Each Gateway provisions an actual load balancer + listeners (port + protocol). Multiple namespaces / app teams attach Routes to it.
  • HTTPRoute / TCPRoute / TLSRoute / GRPCRoute — app team writes these in their own namespace. Each says "for traffic arriving at gateway X on hostname Y, route to my Service."

The role separation is the killer feature. With Ingress, the platform team and app teams fought over the same Ingress resource. Gateway API gives each role its own resource type with appropriate RBAC.

Install (Envoy Gateway as an example)

kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml

# Install an implementation (Envoy Gateway)
helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.2.0 \
    -n envoy-gateway-system --create-namespace

# The implementation auto-creates a GatewayClass
kubectl get gatewayclass

Other implementations: nginx Gateway Fabric, Traefik 3.x, Cilium (when its gateway support is enabled), Istio's own Gateway API conformance, HAProxy. Most major ingress controllers now support Gateway API alongside Ingress.

The minimum-viable example

# 1. The Gateway (platform team)
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: external
  namespace: ingress
spec:
  gatewayClassName: eg
  listeners:
    - name: http
      port: 80
      protocol: HTTP
      allowedRoutes:
        namespaces:
          from: All        # allow HTTPRoutes from any namespace to attach
    - name: https
      port: 443
      protocol: HTTPS
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: example-com-tls
      allowedRoutes:
        namespaces:
          from: All

---
# 2. An HTTPRoute (app team, in their own namespace)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-app-routes
  namespace: my-app
spec:
  parentRefs:
    - name: external
      namespace: ingress
      sectionName: https
  hostnames: [ "app.example.com" ]
  rules:
    - matches:
        - path: { type: PathPrefix, value: "/api" }
      backendRefs:
        - name: api-service
          port: 8080
    - matches:
        - path: { type: PathPrefix, value: "/" }
      backendRefs:
        - name: web-service
          port: 3000

Apply both. The Gateway provisions a LoadBalancer (or whatever the implementation uses); the HTTPRoute attaches its routing rules; app.example.com resolves through the gateway with path-based routing.

What HTTPRoute can express

spec:
  hostnames: [ "api.example.com" ]
  rules:
    # Match by path + header + method
    - matches:
        - path: { type: PathPrefix, value: "/v1/" }
          method: GET
          headers:
            - name: X-API-Version
              value: "1.0"
      backendRefs:
        - name: api-v1
          port: 8080

    # Header-based routing for canary / experiments
    - matches:
        - path: { type: PathPrefix, value: "/api/" }
          headers:
            - name: X-Beta-User
              value: "true"
      backendRefs:
        - name: api-beta
          port: 8080

    # Weighted backend routing for A/B / canary
    - matches:
        - path: { type: PathPrefix, value: "/api/" }
      backendRefs:
        - name: api-stable
          port: 8080
          weight: 95
        - name: api-canary
          port: 8080
          weight: 5

    # Request mutation
    - matches:
        - path: { type: PathPrefix, value: "/legacy/" }
      filters:
        - type: RequestHeaderModifier
          requestHeaderModifier:
            set:
              - { name: X-Legacy-Route, value: "true" }
        - type: URLRewrite
          urlRewrite:
            path: { type: ReplacePrefixMatch, replacePrefixMatch: "/api/v1/" }
      backendRefs:
        - name: api-v1
          port: 8080

TLS termination + cert-manager

Gateway API works directly with cert-manager (see step-ca tutorial's cert-manager pattern):

# cert-manager creates a Certificate resource that maps to the Gateway's Secret
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com-tls
  namespace: ingress
spec:
  secretName: example-com-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames: [ "app.example.com", "api.example.com" ]

The cert renews automatically; the Gateway picks up changes; no annotations needed.

Cross-namespace ReferenceGrants

For security: by default, an HTTPRoute in namespace A can only reference Services / Secrets in namespace A. To allow cross-namespace references (e.g. central TLS secret), the target namespace must explicitly grant permission:

apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: ingress-to-tls-secret
  namespace: ingress
spec:
  from:
    - group: gateway.networking.k8s.io
      kind: Gateway
      namespace: ingress
  to:
    - group: ""
      kind: Secret

Explicit grant model prevents Ingress's "anyone can claim any hostname" cross-namespace footgun.

TCP / TLS / gRPC routes

# Raw TCP (forward port-to-service, no protocol parsing)
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TCPRoute
metadata:
  name: postgres
spec:
  parentRefs:
    - name: tcp-gateway
      sectionName: postgres-listener
  rules:
    - backendRefs:
        - name: postgres
          port: 5432

# TLS pass-through (SNI-routed; cert lives at the backend)
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
  name: prometheus-tls
spec:
  parentRefs:
    - name: tls-gateway
  hostnames: [ "prom.example.com" ]
  rules:
    - backendRefs:
        - name: prometheus
          port: 443

# gRPC (HTTP/2 with gRPC-specific matchers)
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
  name: my-service-grpc
spec:
  parentRefs: [ { name: grpc-gateway } ]
  hostnames: [ "grpc.example.com" ]
  rules:
    - matches:
        - method: { service: my.package.UserService, method: GetUser }
      backendRefs:
        - { name: user-service-grpc, port: 50051 }

Migration from Ingress

The ingress2gateway CLI translates Ingress + annotations to Gateway API resources:

go install github.com/kubernetes-sigs/ingress2gateway@latest
ingress2gateway print --providers ingress-nginx,kong --input-file ingress.yaml > gateway.yaml

Translation is mechanical for simple cases; provider-specific annotations may need manual review.

Why this matters

  • Portability. One config works across nginx Gateway Fabric, Envoy Gateway, Traefik, Cilium, Istio. With Ingress, switching controllers meant rewriting annotations.
  • Role separation. Cluster admin owns GatewayClass; platform owns Gateway; app teams own Routes. Each has its own RBAC.
  • Real-protocol support. HTTP / gRPC / TCP / TLS-passthrough are first-class, not annotation hacks.
  • Standards body. SIG-Network-managed; conformance tests; predictable evolution.

When Ingress is still fine

  • Existing setups that work, with stable team-knowledge of the specific Ingress controller. Don't rewrite for the sake of it.
  • Very simple use cases (one hostname, one service) where Gateway API adds CRD overhead for little gain.
  • Controllers that don't yet support Gateway API at the conformance level you need.

For new clusters or significant rewrites in 2026, start with Gateway API. The Ingress resource is deprecated in spirit (still works, no new features); Gateway API is what Kubernetes networking is evolving into.