Lesson 32 of 66 · 15 min read · Updated
Gateway API Resource Model — Roles, Listeners, allowedRoutes and Status Conditions
On this page
In the previous article you installed Envoy Gateway and sent traffic to kiosk-api through a GatewayClass, a Gateway, and an HTTPRoute. Everything worked on the first try, which is nice, but it also means you never saw the model push back.
In real clusters, routes get rejected. A team attaches a route to a listener that does not want it, a hostname does not line up, a Service name has a typo. Ingress would quietly ignore all of that. Gateway API refuses, and tells you why, through status conditions. To read those messages you need to understand the rules the controller is applying.
This article takes the resource model apart piece by piece, then breaks it on purpose so you can see every common failure and fix it.
We will cover the following topics:
- The roles behind GatewayClass, Gateway, and routes, and how RBAC enforces them
- GatewayClass in detail:
controllerNameandparametersRef - Listeners: name, port, protocol, hostname, and what makes two listeners conflict
- allowedRoutes: which namespaces and which route kinds may attach to a listener
- parentRefs and
sectionName: how a route chooses where to attach - Hostname intersection: how listener and route hostnames combine
- Status conditions on Gateways, listeners, and routes, and the reasons you will see most often
Technical Requirements
- The minikube cluster from the previous article, with Envoy Gateway v1.9.2 and the
egGatewayClass installed. - The kiosk-api Deployment and Service in the
storefrontnamespace. kubectlandcurl.
Core Concepts
Roles and Ownership
Gateway API was designed around three personas. Each one edits a different kind, which is what makes the split enforceable.
| Role | Edits | Typical RBAC |
|---|---|---|
| Infrastructure provider | GatewayClass, implementation config such as EnvoyProxy | Cluster-admin only |
| Cluster operator | Gateway, TLS Secrets, namespace labels | Platform team, in an infrastructure namespace |
| Application developer | HTTPRoute, GRPCRoute, their own Services | Each team in its own namespace |
The key idea is that a route asks to attach to a Gateway, and the Gateway decides whether to accept it. An application team can write any route it likes, but it cannot make the shared Gateway serve a hostname or a namespace the platform team did not allow. That decision is encoded in the Gateway's listeners.
GatewayClass: controllerName and parametersRef
A GatewayClass has two important fields.
controllerName is a string in domain/path form. Each implementation watches for its own value. Envoy Gateway watches for gateway.envoyproxy.io/gatewayclass-controller. A GatewayClass with a value no controller recognises is simply never accepted. The field is immutable after creation.
parametersRef points to implementation-specific configuration. With Envoy Gateway it points to an EnvoyProxy resource, which controls how the data plane is deployed: replica count, resources, the Service type, and much more.
Every Gateway of class eg-ha now gets two Envoy replicas. This is how a platform offers several "flavours" of Gateway: one class per flavour, each with its own parameters. A single Gateway can also override the class with spec.infrastructure.parametersRef, which is useful when only one Gateway needs different settings.
Listeners
A Gateway is a list of listeners. Each listener is a logical endpoint defined by four fields:
| Field | Required | Meaning |
|---|---|---|
name | Yes | Unique within the Gateway. Routes use it as sectionName. |
port | Yes | The port the proxy listens on. |
protocol | Yes | HTTP, HTTPS, TLS, TCP, or UDP. It decides which route kinds make sense. |
hostname | No | Restricts the listener to one hostname or a wildcard such as *.storefront.local. Empty means any hostname. |
Several listeners may share a port. That is how one Gateway serves many hostnames on port 80. The rule is that listeners on the same port and protocol must be distinguishable by hostname. Two HTTP listeners on port 80 with the same hostname, or both without a hostname, are in conflict. The controller marks them with a Conflicted condition and does not program them.
With Envoy Gateway, every listener port also appears on the Envoy Service. A Gateway with listeners on ports 80 and 8080 gets a Service exposing both.
allowedRoutes
Each listener carries an allowedRoutes block that answers two questions: from which namespaces may routes attach, and which kinds of route.
namespaces.from takes one of three values:
| Value | Who may attach |
|---|---|
Same | Only routes in the Gateway's own namespace. This is the default. |
All | Routes in any namespace. |
Selector | Routes in namespaces whose labels match selector. |
The default of Same is deliberately conservative. A shared Gateway in an infrastructure namespace will reject every application route until the platform team opens it up. Selector is usually the right production choice, because the platform team controls namespace labels and can grant access one namespace at a time.
kinds restricts the route types. If you omit it, the listener accepts the kinds that fit its protocol, such as HTTPRoute (and GRPCRoute where supported) for an HTTP listener.
parentRefs and sectionName
A route names the Gateways it wants to attach to in spec.parentRefs. Each entry has these fields:
| Field | Default | Meaning |
|---|---|---|
name | Required | The Gateway's name. |
namespace | The route's namespace | Needed when the Gateway lives elsewhere. |
sectionName | Empty | A listener name. Attach to that listener only. |
port | Empty | Attach to the listeners on that port. |
Without sectionName the route tries to attach to every listener of the Gateway, and succeeds on the ones that allow it. With sectionName it targets exactly one listener, which makes intent explicit and failures easier to read. A route can list several parents, and its status reports a separate result for each one.
Hostname Intersection
Both the listener and the route can carry hostnames. The route is served for the intersection of the two. If the intersection is empty, the route does not attach to that listener.
Listener hostname | Route hostnames | Result |
|---|---|---|
| empty | storefront.local | Attaches. Serves storefront.local. |
storefront.local | empty | Attaches. Inherits storefront.local. |
storefront.local | storefront.local | Attaches. |
*.storefront.local | kiosk.storefront.local | Attaches. Serves kiosk.storefront.local. |
*.storefront.local | storefront.local | Does not attach. A wildcard needs at least one extra label. |
*.storefront.local | kiosk.storefront.local, kiosk.example.com | Attaches. Serves only kiosk.storefront.local. |
storefront.local | kiosk.example.com | Does not attach. Reason: NoMatchingListenerHostname. |
A wildcard such as *.storefront.local matches any number of labels in front, so a.b.storefront.local matches too. It never matches the bare storefront.local.
Status Conditions
Status lives in three places. Learn where to look and most problems become one command away.
Gateway conditions in status.conditions:
| Condition | True means | Common False reasons |
|---|---|---|
Accepted | The spec is valid and a controller owns it | Invalid, UnsupportedAddress, or Pending while no controller has claimed it |
Programmed | The data plane is ready | Invalid, Pending, AddressNotAssigned |
Listener status in status.listeners[], one entry per listener, each with an attachedRoutes count:
| Condition | Meaning |
|---|---|
Accepted | The listener's configuration is supported. |
Programmed | The listener is live in the proxy. |
ResolvedRefs | Referenced objects such as TLS certificates exist and are allowed. |
Conflicted | True is bad here. The listener clashes with another one, for example HostnameConflict. |
Route status in status.parents[], one entry per parentRef:
| Condition | Reason | Cause |
|---|---|---|
Accepted=False | NotAllowedByListeners | The listener's allowedRoutes excludes this namespace or kind. |
Accepted=False | NoMatchingListenerHostname | No listener hostname intersects the route hostnames. |
Accepted=False | NoMatchingParent | The sectionName or port does not exist on that Gateway. |
ResolvedRefs=False | BackendNotFound | A backendRef points to a Service that does not exist. |
ResolvedRefs=False | RefNotPermitted | A backendRef points into another namespace without a ReferenceGrant. |
ResolvedRefs=False | InvalidKind | A backendRef points to a kind the implementation does not support. |
Notice the difference between the two route conditions. Accepted=False means the route is not attached at all, so its hostnames return 404. ResolvedRefs=False means the route is attached but some backend is broken. Requests matching a rule with an invalid backend receive an HTTP 500 from the Gateway.
Hands-On: Kubernetes Commands
Show every condition of a Gateway
Type, status, and reason on one line each.
Show how many routes each listener has
attachedRoutes is the fastest check that a route really landed.
Show a route's result for each parent
Prints the Gateway, listener, and every condition with its reason.
Describe a route to read status and events together
Slower to read but includes the human-readable messages.
Label a namespace to let it attach
Used with allowedRoutes.namespaces.from: Selector.
List all routes in the cluster with their hostnames
A quick overview of who is attached where.
Read the explanation of any field from the API
The CRDs carry their documentation.
Step-by-Step Example
The Scenario
The storefront platform team wants one Gateway with two entry points. Port 80 serves the public storefront.local site, and only the storefront namespace may publish there. Port 8080 serves internal tools under *.internal.storefront.local, and any namespace the platform team labels may publish there. A new team runs a reports-api in the reporting namespace and wants to use the internal entry point.
Along the way you will make the most common mistakes on purpose and fix each one from its status message.
Step 1 — A Gateway with Two Listeners
Save as 1-storefront-gateway.yaml:
Both listeners exist and nothing is attached yet. Note the quotes around the wildcard hostname. YAML would otherwise treat * as the start of an alias.
Step 2 — Break It: A Hostname That Does Not Intersect
Write the kiosk-api route with a hostname the Gateway does not serve. Save as 2-kiosk-api-route.yaml:
The rule has no matches, which means it matches every path.
The backend is fine, but the route is not attached. The public listener only serves storefront.local, and kiosk.example.com does not intersect with it. With Ingress, the equivalent mistake produced no error at all.
Step 3 — Fix It
Change the hostname to storefront.local in the file, or simply remove the hostnames field so the route inherits the listener's hostname. Then apply again:
Start a port-forward that covers both listener ports, and test:
On Docker Desktop's built-in kind cluster the port-forward is optional: the Envoy LoadBalancer Service is published on localhost for every listener port, so http://localhost/ and http://localhost:8080/ reach the two listeners directly. Keep using the port-forward if those ports are already taken on your machine.
Step 4 — Break It: A Backend That Does Not Exist
Edit the route and misspell the Service name as kiosk-apii, then apply and check:
This time the route is attached. Only the backend is broken. Look at what a client gets:
A 500 instead of a 404 is a strong hint. It means a route matched, but its backend could not be resolved. Fix the name back to kiosk-api, apply, and the request succeeds again.
Step 5 — Deploy the reports-api in Another Namespace
The reporting team runs the same image under a different name. Save as 3-reports-api.yaml:
Step 6 — Break It: Attaching to a Listener That Does Not Allow You
The reporting team first tries the public listener. Save as 4-reports-api-route.yaml:
The public listener only allows routes from its own namespace. The reporting team can write the route, but it cannot force its way onto the public site. This is the role split working as intended.
Step 7 — Break It Again: Right Listener, Unlabelled Namespace
Change sectionName to internal and add a hostname under the internal wildcard:
Same reason, different cause. The internal listener uses a namespace selector, and reporting does not carry the label yet.
Step 8 — Fix It: The Platform Team Grants Access
Labelling the namespace is the platform team's decision, not the application team's. In a real cluster only the platform team would have RBAC permission to do it.
The controller re-evaluated the route as soon as the label appeared. No change to the route was needed. Test it through port 8080:
The podIp and node fields say unknown only because this Deployment passes POD_NAME but not POD_IP or NODE_NAME. If the status still shows NotAllowedByListeners right after labelling, wait a second or two and run the command again; the controller reconciles asynchronously.
Step 9 — Prove the Listeners Are Isolated
The public port must not serve the internal hostname, and the internal port must not serve the public one:
Each listener only knows the routes attached to it.
Step 10 — Read the Whole Picture from the Gateway
One route on each listener, exactly as designed. When something does not work, start here, then move to the route's status.parents.
Step 11 — Break the Gateway Itself
Finally, see what happens when nobody owns a Gateway. Create one that references a GatewayClass that does not exist:
Unknown with reason Pending is the default status the API server writes. No controller has ever looked at this Gateway, so nothing was provisioned. If you see this on a Gateway you expected to work, check the class name and whether its GatewayClass is Accepted.
Step 12 — Clean Up
Stop the port-forward and remove what this article created. Keep the eg GatewayClass, Envoy Gateway, and kiosk-api for the rest of the series.
Summary
- Gateway API splits ownership: GatewayClass for the infrastructure provider, Gateway for the cluster operator, routes for application teams. A route asks to attach, and the Gateway decides.
- A GatewayClass is claimed by the controller whose
controllerNamematches.parametersRefpoints to implementation config such as Envoy Gateway'sEnvoyProxy. - A Gateway is a list of listeners, each with a name, port, protocol, and optional hostname. Listeners on the same port must differ by hostname, or they become
Conflicted. - allowedRoutes limits which namespaces (
Sameby default,All, orSelector) and which route kinds may attach. - Routes target Gateways with parentRefs, and a specific listener with sectionName.
- A route serves the intersection of its hostnames and the listener's. No intersection means
NoMatchingListenerHostname. Accepted=Falsemeans the route is not attached and its traffic gets 404.ResolvedRefs=Falsemeans it is attached but a backend is broken, and matching requests get 500.- Check
status.listeners[].attachedRouteson the Gateway first, thenstatus.parents[]on the route.
In the next articles of this series you will build on this model: serving HTTPS from the Gateway, writing richer route rules, and splitting traffic between versions of a .NET API.