Software Engineering Wiki

Kubernetes

Gateway API

GatewayClass, Gateway and Route objects, who owns each one, and how to find why a route is receiving no traffic.

Cheatsheet #

TaskCommand
Is the Gateway programmedkubectl get gateway -A
Why notkubectl describe gateway web -n infra
Is the route attachedkubectl get httproute -A -o wide
Attachment statuskubectl get httproute api -o jsonpath='{.status.parents[*].conditions[*]}' | jq
Listener addresskubectl get gateway web -o jsonpath='{.status.addresses[0].value}'
Backends behind a routekubectl get endpointslice -l kubernetes.io/service-name=api
Test a host without DNScurl -H 'Host: api.example.com' http://<gw-ip>/
Which classes existkubectl get gatewayclass
Controller logskubectl logs -n envoy-gateway-system deploy/envoy-gateway
Cross-namespace permissionkubectl get referencegrant -A

A route that is not receiving traffic #

Traffic needs three things to line up: the Gateway has an address and is Programmed, the Route reports Accepted and ResolvedRefs on the parent it asked for, and the backend Service has ready endpoints. Every failure is one of those three.

kubectl get gateway -A
kubectl get httproute -A -o wide
kubectl describe httproute api -n apps | sed -n '/Status:/,$p'
kubectl get endpointslice -n apps -l kubernetes.io/service-name=api
ConditionMeaning
Gateway Accepted=FalseThe GatewayClass controller rejected the spec, usually TLS or listener conflict
Gateway Programmed=FalseAccepted but the dataplane is not ready; no load balancer address yet
Route Accepted=FalseThe listener refused attachment: hostname mismatch, allowedRoutes namespace selector, or wrong sectionName
Route ResolvedRefs=FalseBackend Service missing, wrong port, or a cross-namespace reference without a ReferenceGrant
All green, still 503No ready endpoints behind the Service

The resource model #

ResourceOwned byPurpose
GatewayClassInfrastructureNames the controller implementation, cluster-scoped
GatewayPlatform teamListeners, ports, TLS certificates, which routes may attach
HTTPRoute, GRPCRoute, TCPRouteApplication teamMatching and forwarding rules, in the application’s namespace
ReferenceGrantThe namespace being referencedPermission for a cross-namespace reference
BackendTLSPolicyEitherTLS from the gateway to the backend

The split is the point: the platform team owns certificates and public addresses, application teams own their own routing, and neither needs edit access to the other’s namespace.

A Gateway #

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: web
  namespace: infra
spec:
  gatewayClassName: envoy
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      hostname: "*.example.com"
      tls:
        mode: Terminate
        certificateRefs:
          - { kind: Secret, name: wildcard-example-com }
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels: { gateway-access: "true" }
    - name: http
      protocol: HTTP
      port: 80
      hostname: "*.example.com"

allowedRoutes.namespaces.from is Same by default, so routes in other namespaces are silently ignored until it is set to Selector or All. Each listener reports its own attachedRoutes count, which is the fastest confirmation that attachment worked.

An HTTPRoute #

Matching is most-specific-first and deterministic: exact path beats prefix, longer prefix beats shorter, then header and query matches break ties. Rule order in the file does not decide precedence.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api
  namespace: apps
spec:
  parentRefs:
    - { name: web, namespace: infra, sectionName: https }
  hostnames: ["api.example.com"]
  rules:
    - matches:
        - path: { type: PathPrefix, value: /v2 }
      backendRefs:
        - { name: api-v2, port: 80, weight: 90 }
        - { name: api-canary, port: 80, weight: 10 }
    - matches:
        - path: { type: Exact, value: /healthz }
      backendRefs: [{ name: api, port: 80 }]
      timeouts: { request: 2s }
    - matches:
        - headers: [{ name: x-beta, value: "true" }]
      backendRefs: [{ name: api-beta, port: 80 }]

Weights split traffic proportionally across backends in the same rule, which is the whole canary mechanism — no extra CRD, no sidecar. A weight of 0 drains a backend without removing it.

Cross-namespace references #

A Route in one namespace may only reference a Service or Secret in another if that namespace publishes a ReferenceGrant. The grant lives with the resource being referenced, so the owner of the data decides.

apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: allow-apps-routes
  namespace: shared
spec:
  from:
    - { group: gateway.networking.k8s.io, kind: HTTPRoute, namespace: apps }
  to:
    - { group: "", kind: Service, name: shared-api }

Missing grants surface as ResolvedRefs=False with RefNotPermitted, not as a 404.

Filters #

Filters run in the order listed, before the backend is selected.

      filters:
        - type: RequestHeaderModifier
          requestHeaderModifier:
            set: [{ name: x-env, value: prod }]
            remove: ["x-internal"]
        - type: RequestRedirect
          requestRedirect: { scheme: https, statusCode: 301 }
        - type: URLRewrite
          urlRewrite:
            path: { type: ReplacePrefixMatch, replacePrefixMatch: / }
        - type: RequestMirror
          requestMirror: { backendRef: { name: api-shadow, port: 80 } }

RequestMirror copies traffic and discards the response, which makes it safe for shadow-testing a new version under real load.

Migrating from Ingress #

IngressGateway API
ingressClassNamegatewayClassName on the Gateway
spec.tlslisteners[].tls.certificateRefs
spec.rules[].hosthostnames on the Route
Path type Prefixpath.type: PathPrefix
nginx.ingress.kubernetes.io/rewrite-targetURLRewrite filter
Controller-specific canary annotationsbackendRefs[].weight

Both can serve the same hostname during a migration as long as they hold different addresses; move DNS last.

Oneliners #

# Every route and the parent it claims
kubectl get httproute -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\t"}{.spec.parentRefs[*].name}{"\n"}{end}'

# Routes that failed to attach
kubectl get httproute -A -o json | jq -r '.items[] | select([.status.parents[].conditions[] | select(.type=="Accepted" and .status!="True")] | length > 0) | "\(.metadata.namespace)/\(.metadata.name)"'

# Attached route count per listener
kubectl get gateway -A -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{range .status.listeners[*]}{.name}={.attachedRoutes}{" "}{end}{"\n"}{end}'

# Gateway addresses
kubectl get gateway -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\t"}{.status.addresses[*].value}{"\n"}{end}'

# Hit a host through the gateway IP, ignoring DNS
curl -sv --resolve api.example.com:443:"$(kubectl get gateway web -n infra -o jsonpath='{.status.addresses[0].value}')" https://api.example.com/healthz

# Certificates referenced by every listener
kubectl get gateway -A -o json | jq -r '.items[].spec.listeners[]?.tls?.certificateRefs[]?.name' | sort -u

# Backends with no endpoints
kubectl get httproute -A -o json | jq -r '.items[].spec.rules[].backendRefs[].name' | sort -u | xargs -I{} sh -c 'kubectl get endpointslice -l kubernetes.io/service-name={} -A --no-headers | grep -q . || echo "{} has no endpoints"'

# Controller errors, whichever implementation is installed
kubectl logs -n envoy-gateway-system deploy/envoy-gateway --tail 200 | grep -iE 'error|reject'

Last updated 15 September 2026 · Edit this page