OnCallReady

Lesson 34.19 · Kubernetes: Ingress, Gateway API & Service Mesh · 13 min read

Gateway API in depth: roles, listeners, and what status tells you

In plain words

Think of a shopping centre. The owner decides which security company runs the doors (the class of service), the centre manager decides which entrances exist and which shops may put up signs at each one, and each shop puts up its own sign pointing customers to its door.

Gateway API splits routing the same way. A GatewayClass names the implementation, a Gateway is an entry point with listeners (port, protocol, hostname, which namespaces may attach) owned by the platform team, and HTTPRoutes are the app teams' signs. Every object reports back in its status whether it was accepted, programmed and whether its references resolve, with a reason such as NotAllowedByListeners or RefNotPermitted. This lesson is about reading those answers.

Beyond the first Gateway

In 16.26 you created a Gateway and a canary HTTPRoute, and fixed NotAllowedByListeners and RefNotPermitted once. This lesson is the reference you will want on call: every condition and reason, HTTPS listeners, and how a Gateway decides which routes it serves.

What you need to know already: GatewayClass, Gateway, HTTPRoute, parentRefs, allowedRoutes and ReferenceGrant (16.26-16.28), TLS Secrets (16.22), CRDs (16.26), the cert-manager lesson of this chapter.

Three roles, written into the API

infrastructure provider   ships the implementation and its GatewayClass   (cloud, platform vendor)
cluster operator          creates Gateways: addresses, ports, TLS, which   (platform team)
                          namespaces may attach
application developer     creates routes in their own namespace           (app teams)

RBAC follows the split: app teams get create on httproutes in their namespace, nothing on gateways. That is the point - a team can ship routing rules on a shared entry point without being able to break it for the others.

The current release

Gateway API ships as CRDs in two channels: standard (GA, stable, v1 fields only) and experimental (new fields first). The latest is v1.6 (v1.6.2, September 2026). In the standard channel today:

GatewayClass, Gateway, HTTPRoute, GRPCRoute      v1 since 1.0 / 1.1
ReferenceGrant                                   v1 since 1.5 (v1beta1 still served and stored)
TLSRoute, ListenerSet, CORS filter               standard since 1.5
TCPRoute, UDPRoute                               v1 since 1.6
HTTPRoute timeouts (request, backendRequest)     standard
HTTPRoute retries                                experimental (GEP-1731)

Install the CRDs of the channel your implementation supports; installing experimental CRDs under an implementation that only understands standard gives you fields it silently ignores.

Listeners

A Gateway's listeners are its doors:

listeners:
- name: http
  port: 80
  protocol: HTTP
- name: shop-https
  port: 443
  protocol: HTTPS
  hostname: shop.lab              # only requests for this name (SNI + Host)
  tls:
    mode: Terminate               # (Passthrough exists for TLSRoute)
    certificateRefs:
    - kind: Secret
      name: shop-tls
      namespace: certs            # another namespace: needs a ReferenceGrant
  allowedRoutes:
    namespaces:
      from: Selector
      selector:
        matchLabels:
          gateway-access: shop
    kinds:
    - kind: HTTPRoute

Certificates in another namespace. Platform teams keep TLS Secrets in a locked-down namespace. A Gateway may only use them if that namespace says so:

apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: gateways-may-use-certs
  namespace: certs                 # lives next to the Secrets
spec:
  from:
  - group: gateway.networking.k8s.io
    kind: Gateway
    namespace: gw-edge
  to:
  - group: ""
    kind: Secret

Without it the listener reports ResolvedRefs False, RefNotPermitted and the data plane does not serve that certificate. Same object as 16.28, other direction: there an HTTPRoute pointed at a Service, here a Gateway at a Secret.

Status: the whole table

Every object has status.conditions (a route has them per parent). This is the list from the v1.6 API, with the reasons you will actually see:

GatewayClass  Accepted          True: Accepted.   False: InvalidParameters, Unsupported.  Unknown: Pending/Waiting
              SupportedVersion  False: UnsupportedVersion (CRDs newer/older than the controller)
Gateway       Accepted          False: ListenersNotValid, UnsupportedAddress, InvalidParameters
                                Unknown + Pending "Waiting for controller": no controller for this class
              Programmed        False: AddressNotAssigned (no LB address), NoResources, Invalid, Pending
Listener      Accepted          False: PortUnavailable, UnsupportedProtocol, UnsupportedValue
              Programmed        False: Invalid, Pending
              ResolvedRefs      False: InvalidCertificateRef, RefNotPermitted, InvalidRouteKinds
              Conflicted        True: HostnameConflict, ProtocolConflict
Route parent  Accepted          False: NotAllowedByListeners, NoMatchingListenerHostname,
                                NoMatchingParent, UnsupportedValue, IncompatibleFilters
              ResolvedRefs      False: BackendNotFound, RefNotPermitted, InvalidKind, UnsupportedProtocol

Reading order on call: GatewayClass Accepted -> Gateway Programmed -> listener ResolvedRefs/Conflicted -> route Accepted -> route ResolvedRefs. The first False is your problem.

# quick views
kubectl get gatewayclass                                     ACCEPTED column
kubectl get gateway -A                                       PROGRAMMED + ADDRESS
kubectl describe gateway edge -n gw-edge                     listeners, attachedRoutes, conditions
kubectl get httproute shop -n shop -o jsonpath='{range .status.parents[*].conditions[*]}{.type}={.status} {.reason}{"\n"}{end}'

attachedRoutes on a listener counts the routes that attached. A route you just applied that does not raise that number did not attach - check its status before anything else.

A Gateway nobody handles (a typo in gatewayClassName, or the implementation not installed) keeps the CRD's default status: Accepted Unknown, reason Pending, "Waiting for controller", and PROGRAMMED Unknown. Nothing is "wrong" with it - nobody is listening.

In an interview: "Gateway API splits Ingress into GatewayClass (the implementation), Gateway (the entry point, owned by the platform team) and routes (owned by app teams). When a route does not work I read its status: Accepted False with NotAllowedByListeners means the Gateway does not admit its namespace; ResolvedRefs False with RefNotPermitted means a missing ReferenceGrant."

What you can now do:

Why it helps

Ingress put everything into one object and a pile of annotations, so the platform team and app teams fought over it. Gateway API writes the roles into the API, which is why it is replacing Ingress, and why interviews increasingly ask about it.

Its biggest practical difference is that it tells you what is wrong. A route that does nothing has a condition saying why: the listener does not allow routes from its namespace, the hostname does not match, the backend Service does not exist, or a cross-namespace reference has no ReferenceGrant. Knowing the condition table turns "my route is ignored" from guesswork into reading one field.

Commands in this lesson

kubectl

FAQ

What is the difference between Accepted and Programmed?

Accepted means the controller read the object and considers it valid; Programmed means it has actually configured the data plane (for a Gateway: an address and running proxies). A Gateway can be Accepted but not Programmed yet, for example while its load balancer address is still being assigned.

Why does my route say NotAllowedByListeners?

The Gateway's listener has allowedRoutes that does not include your namespace: by default only routes from the Gateway's own namespace may attach. The platform team allows other namespaces with allowedRoutes.namespaces.from: All or a selector on namespace labels.

When do I need a ReferenceGrant?

Whenever an object refers to something in another namespace: an HTTPRoute sending traffic to a Service elsewhere, or a Gateway listener using a certificate Secret from another namespace. The ReferenceGrant lives in the target namespace and says which kinds from which namespaces may refer to it; without it the reference fails with RefNotPermitted.

What are the standard and experimental channels?

Gateway API ships two sets of CRDs. The standard channel holds stable resources and fields (Gateway, HTTPRoute, GRPCRoute, ReferenceGrant); the experimental channel adds fields and kinds still being tried out. Clusters normally install standard; a field from experimental is ignored or rejected there.

What does Conflicted mean on a listener?

Two listeners on the same Gateway clash, for example the same port and hostname with different protocols. The controller marks them Conflicted and may not program one of them. Give them different hostnames or ports, or merge them into one listener.

In an interview Mid

An app team says its HTTPRoute is ignored. How do you find out why?

I read the route's status first: kubectl describe httproute shows a condition per parent Gateway. Accepted False with NotAllowedByListeners means the listener's allowedRoutes does not admit the route's namespace; NoMatchingParent means the parentRef's name or sectionName does not exist; NoMatchingListenerHostname means the hostnames do not intersect. ResolvedRefs False with BackendNotFound means a missing Service or port, and RefNotPermitted means a cross-namespace backend without a ReferenceGrant. If the route is fine, I check the Gateway itself: Programmed True and an address. The status names the object to fix.

Also asked: What are the three roles in Gateway API, and which objects does each own? · Why does Gateway API need ReferenceGrant? · How is a Gateway listener different from an Ingress rule?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.