LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Gateway API: The Future of Kubernetes Ingress

kubernetesnetworkinggateway-apiingresstraefikenvoydevops

If you’ve worked with Kubernetes Ingress resources for any length of time, you’ve hit the walls. You want to split traffic between two backend versions — no standard way to do it. You want to route based on a request header — vendor annotation required. You want your platform team to control which TLS certificates are allowed while letting application teams manage their own routes — impossible without hacking the RBAC model. You end up with dozens of nginx.ingress.kubernetes.io/ annotations that tie you to a specific implementation and break the moment you switch controllers.

The Kubernetes Gateway API was designed to fix all of this. It graduated to GA (v1) in late 2023 and is now the recommended path for new Kubernetes networking. It’s expressive enough to replace Ingress, service meshes, and many custom networking controllers — all with portable, implementation-agnostic resources.

What’s Wrong with Ingress

The Kubernetes Ingress API was designed for a narrow use case: HTTP/HTTPS routing from outside the cluster to services inside it. It handles host-based and path-based routing and nothing else. Every real-world requirement beyond that gets addressed through implementation-specific annotations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# This works only with nginx-ingress — tie yourself to one implementation
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "20"
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "10"
    nginx.ingress.kubernetes.io/rate-limit: "100"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/use-regex: "true"
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts: [app.example.com]
      secretName: app-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 8080

There’s no RBAC separation between infrastructure owners and application teams, no standard traffic-splitting mechanism, no gRPC or TCP routing, and no way to express cross-cutting policies. The annotation soup is unportable and undiscoverable.

The Gateway API Model

Gateway API introduces four main resources with a deliberate role-based separation:

GatewayClass  ←  infrastructure provider (cluster admin)
    ↓
Gateway       ←  cluster operator / network admin
    ↓
HTTPRoute     ←  application developer
TCPRoute      ←  application developer
GRPCRoute     ←  application developer

This role separation is built into the API design, not bolted on with RBAC hacks.

GatewayClass — defined by the infrastructure team or installed by the controller. Describes what implementation handles Gateways (Envoy, Traefik, nginx, Cilium, etc.).

Gateway — requests a load balancer / listener. Defines ports, protocols, and TLS. Owned by the network/platform team. Can restrict which namespaces and Routes are allowed to attach.

HTTPRoute — defines routing rules for HTTP traffic. Owned by application teams. Attaches to a Gateway in another namespace if allowed.

GRPCRoute, TCPRoute, TLSRoute, UDPRoute — same model for other protocols.

Installing Gateway API CRDs

Gateway API ships as CRDs separate from Kubernetes itself. Install the standard channel (stable features):

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

For experimental features (TCPRoute, UDPRoute, BackendLBPolicy):

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

Then install a Gateway controller. We’ll use Envoy Gateway (the reference implementation backed by the Envoy proxy project):

1
2
3
4
5
6
7
8
9
helm install eg oci://docker.io/envoyproxy/gateway-helm \
  --version v1.2.0 \
  --namespace envoy-gateway-system \
  --create-namespace

kubectl wait --timeout=5m \
  -n envoy-gateway-system \
  deployment/envoy-gateway \
  --for=condition=Available

Other well-supported implementations: Traefik v3, Cilium, nginx Gateway Fabric, Istio, Linkerd.

Basic HTTP Routing

A minimal setup that replicates what Ingress does:

1
2
3
4
5
6
7
# 1. GatewayClass — installed by the controller, you usually don't write this
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: eg
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 2. Gateway — owned by network/platform team
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: production
  namespace: infra
spec:
  gatewayClassName: eg
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      # Allow HTTPRoutes from any namespace to attach
      allowedRoutes:
        namespaces:
          from: All

    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: wildcard-tls
            namespace: infra
      allowedRoutes:
        namespaces:
          from: All
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 3. HTTPRoute — owned by the application team, lives in the app namespace
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-app
  namespace: production
spec:
  # Attach to the Gateway in the infra namespace
  parentRefs:
    - name: production
      namespace: infra
      sectionName: https   # Attach to the HTTPS listener specifically

  hostnames:
    - "app.example.com"

  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: api-service
          port: 8080

    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: frontend-service
          port: 3000

This is already cleaner than Ingress: the platform team controls the Gateway (what ports, what TLS certs, which namespaces can attach), and the application team controls the HTTPRoute (their own routing rules) without needing any special permissions on the Gateway itself.

Traffic Splitting and Canary Releases

This is where Gateway API shines. Traffic splitting is a first-class concept, not an annotation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-canary
  namespace: production
spec:
  parentRefs:
    - name: production
      namespace: infra
  hostnames:
    - "api.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        # 90% traffic to stable, 10% to canary
        - name: api-stable
          port: 8080
          weight: 90
        - name: api-canary
          port: 8080
          weight: 10

Update the weights progressively as confidence in the canary grows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Shift to 50/50
kubectl patch httproute api-canary -n production --type='json' -p='[
  {"op": "replace", "path": "/spec/rules/0/backendRefs/0/weight", "value": 50},
  {"op": "replace", "path": "/spec/rules/0/backendRefs/1/weight", "value": 50}
]'

# Full cutover
kubectl patch httproute api-canary -n production --type='json' -p='[
  {"op": "replace", "path": "/spec/rules/0/backendRefs/0/weight", "value": 100},
  {"op": "replace", "path": "/spec/rules/0/backendRefs/1/weight", "value": 0}
]'

Header-Based Routing

Route traffic based on headers without any annotations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-header-routing
  namespace: production
spec:
  parentRefs:
    - name: production
      namespace: infra
  hostnames:
    - "api.example.com"
  rules:
    # Beta users get routed to the new backend
    - matches:
        - headers:
            - name: X-User-Tier
              value: beta
      backendRefs:
        - name: api-v2
          port: 8080

    # Internal traffic (from CI, test tools) goes to the canary
    - matches:
        - headers:
            - name: X-Internal-Request
              value: "true"
      backendRefs:
        - name: api-canary
          port: 8080

    # Everyone else hits stable
    - backendRefs:
        - name: api-stable
          port: 8080

You can also match on query parameters, HTTP method, or combinations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  rules:
    # Only match POST requests to /checkout with a specific query param
    - matches:
        - path:
            type: Exact
            value: /checkout
          method: POST
          queryParams:
            - name: debug
              value: "true"
      backendRefs:
        - name: checkout-debug
          port: 8080

Request and Response Modification

Gateway API includes built-in filters for common HTTP manipulation tasks:

Request Header Manipulation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-with-filters
  namespace: production
spec:
  parentRefs:
    - name: production
      namespace: infra
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      filters:
        # Add, set, or remove request headers before forwarding
        - type: RequestHeaderModifier
          requestHeaderModifier:
            add:
              - name: X-Forwarded-By
                value: gateway
              - name: X-Request-Start
                value: "%START_TIME%"
            set:
              - name: X-Service-Version
                value: "v2"
            remove:
              - X-Internal-Debug

        # Rewrite the URL path before forwarding
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /v2

      backendRefs:
        - name: api-service
          port: 8080

Response Header Manipulation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
      filters:
        - type: ResponseHeaderModifier
          responseHeaderModifier:
            add:
              - name: X-Content-Type-Options
                value: nosniff
              - name: X-Frame-Options
                value: DENY
            set:
              - name: Cache-Control
                value: "no-store, max-age=0"

Request Mirroring

Send a copy of traffic to a second backend for testing or debugging without affecting the primary response:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      filters:
        # Mirror 10% of traffic to the shadow service
        - type: RequestMirror
          requestMirror:
            backendRef:
              name: api-shadow
              port: 8080
            percent: 10
      backendRefs:
        - name: api-service
          port: 8080

HTTP to HTTPS Redirect

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: http-redirect
  namespace: infra
spec:
  parentRefs:
    - name: production
      namespace: infra
      sectionName: http   # Attach to HTTP listener only
  rules:
    - filters:
        - type: RequestRedirect
          requestRedirect:
            scheme: https
            statusCode: 301

gRPC Routing

GRPCRoute is a first-class resource for routing gRPC traffic:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
  name: payment-grpc
  namespace: production
spec:
  parentRefs:
    - name: production
      namespace: infra
  hostnames:
    - "grpc.example.com"
  rules:
    # Route to v2 for the new ChargeV2 method
    - matches:
        - method:
            service: payments.PaymentService
            method: ChargeV2
      backendRefs:
        - name: payments-v2
          port: 9090

    # Everything else hits v1
    - matches:
        - method:
            service: payments.PaymentService
      backendRefs:
        - name: payments-v1
          port: 9090

TLS Configuration

Per-Route TLS with cert-manager

cert-manager supports Gateway API natively:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Gateway with automated certificate management
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: production
  namespace: infra
  annotations:
    # cert-manager will create and manage the certificate
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  gatewayClassName: eg
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      hostname: "*.example.com"
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: wildcard-tls   # cert-manager creates this

TLS Passthrough

For services that terminate TLS themselves (like databases or services requiring mTLS end-to-end):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
  name: postgres-tls
  namespace: production
spec:
  parentRefs:
    - name: production
      namespace: infra
      sectionName: tls-passthrough
  hostnames:
    - "db.internal.example.com"
  rules:
    - backendRefs:
        - name: postgres
          port: 5432
1
2
3
4
5
6
7
# Gateway listener for TLS passthrough
  listeners:
    - name: tls-passthrough
      protocol: TLS
      port: 5432
      tls:
        mode: Passthrough   # Forward encrypted traffic without terminating

Cross-Namespace Routing with ReferenceGrants

By default, an HTTPRoute can only reference backends (Services) in its own namespace. To reference a Service in another namespace, the target namespace must grant permission with a ReferenceGrant:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# In the 'shared-services' namespace — allows routes from 'production' to reference services here
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: allow-production-routes
  namespace: shared-services
spec:
  from:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      namespace: production
  to:
    - group: ""
      kind: Service

This makes cross-namespace service sharing explicit and auditable rather than implicit.

Policy Attachment

Policy attachment is how Gateway API handles cross-cutting concerns like timeouts, retries, rate limiting, and authentication — without annotations. Policies attach to Gateway, HTTPRoute, or Service resources and apply to matching traffic.

Timeout and Retry Policy (Envoy Gateway)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: api-timeouts
  namespace: production
spec:
  targetRef:
    group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: api-route
  timeout:
    request: 30s
  retry:
    numRetries: 3
    perRetry:
      timeout: 5s
    retryOn:
      httpStatusCodes: [503, 504]
      triggers: [connect-failure, retriable-4xx]

Rate Limiting Policy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: api-rate-limit
  namespace: production
spec:
  targetRef:
    group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: api-route
  rateLimit:
    type: Global
    global:
      rules:
        # 1000 requests per minute per distinct client IP
        - clientSelectors:
            - headers:
                - name: x-forwarded-for
                  type: Distinct
          limit:
            requests: 1000
            unit: Minute
        # 50 requests per minute for unauthenticated requests
        - clientSelectors:
            - headers:
                - name: Authorization
                  type: Absent
          limit:
            requests: 50
            unit: Minute

Authentication Policy (JWT)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
  name: jwt-auth
  namespace: production
spec:
  targetRef:
    group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: api-route
  jwt:
    providers:
      - name: keycloak
        issuer: https://auth.example.com/realms/myrealm
        audiences:
          - my-api
        remoteJWKS:
          uri: https://auth.example.com/realms/myrealm/protocol/openid-connect/certs

Migrating from Ingress

Migration can be gradual — both Ingress and Gateway API can coexist in the same cluster. Here’s a step-by-step approach:

Step 1: Install Gateway API CRDs and your chosen controller alongside the existing Ingress controller.

Step 2: Create a Gateway resource that matches your existing Ingress controller’s listener configuration.

Step 3: For each Ingress, create an equivalent HTTPRoute. Use this mapping:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# Old Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts: [app.example.com]
      secretName: app-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 8080

---
# Equivalent HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-app
  namespace: production
spec:
  parentRefs:
    - name: production
      namespace: infra
  hostnames:
    - "app.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /
      backendRefs:
        - name: api-service
          port: 8080

Step 4: Test the new routes with a curl against the new Gateway’s address.

Step 5: Update DNS to point to the new Gateway. Delete the old Ingress.

Step 6: After all Ingresses are migrated, remove the old Ingress controller.

Checking Route Status

Gateway API gives detailed status on whether routes are accepted:

1
2
3
4
5
# Check Gateway status
kubectl describe gateway production -n infra

# Check HTTPRoute status — shows whether it's attached and any errors
kubectl describe httproute my-app -n production

Successful attachment looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Status:
  Parents:
    - ParentRef:
        Group: gateway.networking.k8s.io
        Kind: Gateway
        Name: production
        Namespace: infra
        SectionName: https
      Conditions:
        - Type: Accepted
          Status: "True"
          Reason: Accepted
        - Type: ResolvedRefs
          Status: "True"
          Reason: ResolvedRefs

If a route is rejected (wrong namespace, wrong listener, invalid backend), the status shows exactly why:

1
2
3
4
5
      Conditions:
        - Type: ResolvedRefs
          Status: "False"
          Reason: RefNotPermitted
          Message: "Cross-namespace reference to Service 'other-ns/api-service' not allowed without ReferenceGrant"

Choosing a Gateway Controller

Controller Best For Notable Features
Envoy Gateway General purpose, greenfield Official reference impl, rich policy API
Traefik v3 Homelab, Docker + K8s Easiest setup, great UI
Cilium eBPF networking No sidecar, kernel-native, combined CNI+Gateway
nginx Gateway Fabric Migrating from nginx-ingress Familiar, good performance
Istio Service mesh + ingress mTLS, traffic management, observability
Linkerd Lightweight service mesh Low overhead, automatic mTLS

For a homelab or small cluster: Traefik v3 has the easiest setup and a built-in dashboard. For production on Kubernetes with an existing Cilium CNI: the Cilium gateway controller eliminates a separate proxy entirely.

Traefik v3 with Gateway API

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# values.yaml for traefik helm chart
providers:
  kubernetesGateway:
    enabled: true    # Enable Gateway API support

ingressRoute:
  dashboard:
    enabled: true

experimental:
  kubernetesGateway:
    enabled: true
1
2
3
4
helm upgrade --install traefik traefik/traefik \
  --namespace traefik \
  --create-namespace \
  -f values.yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# GatewayClass is created automatically by Traefik
# Just create your Gateway:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: traefik
  namespace: traefik
spec:
  gatewayClassName: traefik
  listeners:
    - name: web
      port: 80
      protocol: HTTP
      allowedRoutes:
        namespaces:
          from: All
    - name: websecure
      port: 443
      protocol: HTTPS
      tls:
        certificateRefs:
          - kind: Secret
            name: wildcard-tls
      allowedRoutes:
        namespaces:
          from: All

Real-World Pattern: Multi-Team Gateway

Here’s a production-grade multi-team setup where the platform team owns the Gateway and application teams manage their own routes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Platform team: gateway.yaml (namespace: infra)
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: production
  namespace: infra
spec:
  gatewayClassName: eg
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs:
          - name: wildcard-tls
      allowedRoutes:
        namespaces:
          # Only allow routes from namespaces with this label
          from: Selector
          selector:
            matchLabels:
              gateway-access: allowed
1
2
3
4
# Platform team: label production namespaces
kubectl label namespace payments gateway-access=allowed
kubectl label namespace checkout gateway-access=allowed
kubectl label namespace auth gateway-access=allowed
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Payments team: their HTTPRoute (namespace: payments)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: payments-api
  namespace: payments
spec:
  parentRefs:
    - name: production
      namespace: infra
  hostnames:
    - "payments.example.com"
  rules:
    - backendRefs:
        - name: payments-api
          port: 8080

Each team has full control over their routing rules. The platform team controls which teams can attach to the Gateway. No one needs to touch the Gateway resource to update routing — RBAC naturally enforces the separation.

Conclusion

The Kubernetes Gateway API is not just “Ingress with more features.” It’s a fundamental redesign that treats network routing as a role-based concern, separates infrastructure from application configuration, and makes complex routing patterns (traffic splitting, header matching, mirroring) first-class API concepts rather than implementation-specific annotations.

The migration path is gentle — Gateway API coexists with Ingress, and the tooling (cert-manager, external-dns, ArgoCD) already supports it. The annotation soup that accumulated on your Ingress resources can be replaced with readable, portable YAML that works across any compliant controller.

If you’re starting a new cluster today, skip the Ingress API entirely and start with Gateway. If you have an existing cluster, migrate one application at a time — the investment pays off every time you need traffic splitting, per-route timeouts, or cross-team route ownership without a kubectl escalation.

Comments