Lesson 37 of 66 · 17 min read · Updated
Cross-Namespace Routing with ReferenceGrant and Shared Gateways
On this page
In the previous article you exposed a .NET gRPC service through storefront-gateway. So far every resource in this series has lived in a single namespace, storefront. The Gateway, the routes, the Services and the certificates all belonged to one team.
Real clusters are rarely that tidy. A platform team usually runs one shared Gateway, with one public IP address and one set of certificates. Several application teams, each in their own namespace, attach routes to it. Sooner or later one of those routes needs a Service in another team's namespace, or the Gateway needs a certificate stored somewhere else.
Gateway API was designed for exactly this situation. It makes every cross-namespace reference explicit and two-sided: the owner of the target must agree before anything can point at it. This article shows the three mechanisms that make this work: allowedRoutes, ReferenceGrant, and ListenerSet.
We will cover the following topics:
- Why cross-namespace references are a security problem in the first place
- How a shared Gateway decides which namespaces may attach routes
- What happens when a route points at a Service in another namespace
- ReferenceGrant: the target namespace's written permission
- Letting a Gateway use a TLS Secret from a separate certificates namespace
- ListenerSet: letting application teams bring their own listeners
- Reading the status conditions that tell you which permission is missing
Technical Requirements
- The minikube cluster and Envoy Gateway v1.9.2 from the earlier articles. The
egGatewayClass must exist. - The
kiosk-api:1.0image built in the Services series and loaded into minikube. openssl, to create a self-signed certificate.- This article creates its own Gateway in a new
infranamespace, so it does not depend onstorefront-gateway.
Core Concepts
Why Cross-Namespace References Are Dangerous
Namespaces are the main isolation boundary in Kubernetes. RBAC usually gives each team rights inside its own namespace and nowhere else.
Now imagine a route could point at any Service in any namespace. A developer with rights only in sandbox could write an HTTPRoute that sends public traffic to the payments Service in another namespace, or tell a Gateway to serve a certificate stored in someone else's namespace. They never touched the other namespace, yet they exposed it. This is the confused deputy problem: the gateway controller has rights everywhere, and a careless reference borrows them.
The old Ingress API avoided this by simply forbidding cross-namespace backends. Gateway API allows them, but only with consent from both sides.
The Two Handshakes
There are two different directions of reference, and each has its own consent mechanism:
| Reference | Who points at whom | Who must agree | Mechanism |
|---|---|---|---|
| Route attaches to a Gateway | Route (app namespace) → Gateway (infra namespace) | The Gateway owner | allowedRoutes on the listener |
| Route sends traffic to a backend | Route (namespace A) → Service (namespace B) | The Service's namespace | ReferenceGrant in namespace B |
| Gateway uses a certificate | Gateway (namespace A) → Secret (namespace B) | The Secret's namespace | ReferenceGrant in namespace B |
| Listeners merged onto a Gateway | ListenerSet (app namespace) → Gateway | The Gateway owner | allowedListeners on the Gateway |
The pattern is always the same: the thing being referenced decides. Nobody can grant themselves access to someone else's resources.
allowedRoutes on a Shared Gateway
Each listener of a Gateway says which namespaces may attach routes to it:
allowedRoutes.namespaces.from | Meaning |
|---|---|
Same (default) | Only routes in the Gateway's own namespace |
All | Routes from any namespace |
Selector | Only namespaces whose labels match selector |
Same is the safe default, and it is why every earlier article kept the Gateway and its routes together in storefront. For a shared Gateway, Selector is the usual choice. The platform team labels the namespaces it has onboarded, and no other namespace can attach. If a route from an unlabelled namespace tries anyway, its status shows Accepted=False with the reason NotAllowedByListeners.
The listener can also restrict the route kinds, for example to HTTPRoute only, with allowedRoutes.kinds.
ReferenceGrant
A ReferenceGrant lives in the namespace that owns the target. It lists who may refer (from) and what they may refer to (to):
A few rules worth remembering:
fromnames a namespace and a kind, never an individual route. Any HTTPRoute instorefrontgets the permission.to.nameis optional. Leaving it out grants access to every Service in the namespace, which is rarely what you want.- ReferenceGrant is one-way. A grant in
ordersletsstorefrontin; it says nothing about the opposite direction. - Deleting the ReferenceGrant revokes access immediately. The route's backend becomes invalid and traffic to it stops.
ReferenceGrant was promoted to v1 and the Standard channel in Gateway API v1.5. Envoy Gateway v1.9 bundles Gateway API v1.6.1, so v1 is available. On older installations the same resource is served as v1beta1, with an identical spec.
Status Conditions That Point at the Problem
You will rarely guess wrong about which handshake is missing, because the status says so:
| Where | Condition | Reason | Meaning |
|---|---|---|---|
Route parents[] | Accepted=False | NotAllowedByListeners | The listener's allowedRoutes rejects this namespace or kind |
Route parents[] | ResolvedRefs=False | RefNotPermitted | A backendRef crosses namespaces without a ReferenceGrant |
Route parents[] | ResolvedRefs=False | BackendNotFound | The Service does not exist |
Gateway listeners[] | ResolvedRefs=False | RefNotPermitted | A certificateRef crosses namespaces without a ReferenceGrant |
When a backend is not permitted, the route can still be accepted. Envoy Gateway keeps the other rules working and answers the broken rule with an HTTP 500, so one bad reference does not take down the whole route.
ListenerSet
allowedRoutes and ReferenceGrant cover routes and backends. One thing was still centralised: listeners. If team A needed an HTTPS listener for a.example.com with its own certificate, someone had to edit the shared Gateway. Every new hostname became a ticket for the platform team, and a Gateway can only hold 64 listeners.
ListenerSet, promoted to Standard in Gateway API v1.5, removes that bottleneck. A team creates a ListenerSet in its own namespace with one or more listeners. The controller merges them onto the parent Gateway, but only if the Gateway allows it through allowedListeners:
Because the certificate now lives in the same namespace as the ListenerSet that uses it, no ReferenceGrant is needed for it. Routes attach to a ListenerSet with a parentRef of kind: ListenerSet. The Gateway itself must still have at least one listener of its own.
ListenerSet is new, and implementations are still catching up. Envoy Gateway v1.9 supports it; on any other implementation, check its conformance report before relying on it.
Hands-On: Kubernetes Commands
Show the labels on a namespace
allowedRoutes with from: Selector matches against these.
Label a namespace for the shared Gateway
This is the platform team's "onboarding" step.
See which routes are attached to each listener
ATTACHED ROUTES per listener lives in the Gateway status.
Print a route's conditions in one line each
Fastest way to find NotAllowedByListeners or RefNotPermitted.
List ReferenceGrants everywhere
Grants live in the target namespaces, so search across all of them.
Check whether ListenerSet is installed
If this returns nothing, your Gateway API CRDs predate v1.5.
Step-by-Step Example
The Scenario
The company now has two application teams and one platform team:
- The platform team owns namespace
infraand runsshared-gateway. - The storefront team owns
storefrontand serveskiosk-apionstorefront.local. - The orders team owns
ordersand servesorders-apionorders.local. - A third namespace,
sandbox, belongs to an intern who has not been onboarded.
Then the storefront team wants storefront.local/orders to reach the orders team's Service, and security wants all certificates kept in a dedicated certs namespace. You will build this step by step and watch each permission fail before you grant it.
Step 1 — Namespaces and Labels
Create 1-namespaces.yaml. Only storefront and orders carry the access label.
If storefront already exists from earlier articles, kubectl apply simply adds the label.
Step 2 — The Shared Gateway
Create 2-shared-gateway.yaml. The single HTTP listener accepts HTTPRoutes only, and only from labelled namespaces.
Wait for Accepted, not Programmed. As you saw in the installation article, Programmed stays False with reason AddressNotAssigned until the Envoy LoadBalancer Service gets an address, which on minikube only happens while minikube tunnel runs. Routing through the port-forward works either way.
On Docker Desktop's built-in kind cluster, LoadBalancer Services are published on localhost, but only one Service can own a given port. If storefront-gateway from earlier articles still holds port 80, the shared Gateway's Service stays <pending> and the Gateway is not Programmed. That is harmless here, because every test goes through a port-forward.
Step 3 — The Applications
Both teams run the same kiosk-api:1.0 image, so the response's servedByPod field tells you which namespace answered. Create 3-apps.yaml:
If kiosk-api is already running in storefront from earlier articles, the apply just updates it.
Step 4 — Each Team Attaches Its Own Route
Create 4-team-routes.yaml. Each route names the Gateway with an explicit namespace, because the Gateway is no longer in the route's own namespace.
Two routes from two namespaces share one listener. Start a port-forward to the shared Gateway's Envoy Service in a separate terminal:
Step 5 — The Unlabelled Namespace Is Rejected
The intern in sandbox tries to grab a hostname on the shared Gateway. Create 5-sandbox-route.yaml:
The second line only says that the whatever Service does not exist. The first line is the one that matters.
Kubernetes accepted the object, because the intern has rights in sandbox. The Gateway refused to attach it, because the platform team has not onboarded sandbox. No Envoy configuration was generated for it.
Step 6 — A Cross-Namespace Backend Without Permission
The storefront team wants storefront.local/orders to reach the orders team's Service directly. Update storefront-route in 4-team-routes.yaml so it has two rules:
The URLRewrite filter strips /orders before the request reaches orders-api. Without it, the backend would receive /orders, a path the sample app does not serve, and answer with an empty 404.
The route is still attached, so the rest of storefront.local keeps working. Only the rule with the forbidden reference is broken:
Look at the full message once; it names the exact reference that was refused:
Step 7 — The Orders Team Grants Access
Only the owner of orders can fix this. Create 7-referencegrant-orders.yaml in the orders namespace:
Notice that nothing changed on the route. The status flipped because the target namespace changed its mind. Now revoke it, to prove the grant is live:
Step 8 — A Certificate from Another Namespace
Security wants every TLS Secret in the certs namespace, where only they have access. Create a self-signed certificate and store it there:
Add an HTTPS listener to 2-shared-gateway.yaml that references the Secret across namespaces. Append it under listeners:
Envoy Gateway v1.9.2 reports RefNotPermitted and refuses to program the listener. Other implementations may use InvalidCertificateRef for certificate problems, but ResolvedRefs=False on the https listener is always the signal to look for.
The Gateway is in infra and the Secret is in certs, so certs must agree. Create 8-referencegrant-certs.yaml:
ResolvedRefs is now True. Attach the storefront route to the new listener too by adding a second parent to storefront-route:
Port-forward the HTTPS port in another terminal (export ENVOY_SERVICE there as in Step 4) and test. --resolve makes curl send the right SNI and Host while connecting to localhost:
-k is needed only because the certificate is self-signed. With cert-manager, as in the HTTPS article, the Secret would be issued into certs and the same ReferenceGrant would apply.
Step 9 — Optional: Team-Owned Listeners with ListenerSet
First check that the ListenerSet CRD is installed:
If it is, the platform team can stop adding listeners for every team. Allow ListenerSets from onboarded namespaces by adding this block to the Gateway's spec in 2-shared-gateway.yaml:
The orders team then creates its own certificate in its own namespace and a ListenerSet for orders.local. Create 9-orders-listenerset.yaml:
Finally, point orders-route at the ListenerSet as an additional parent:
The orders team added a hostname, a certificate and a listener without touching any file the platform team owns, and without any ReferenceGrant, because nothing crossed a namespace except the ListenerSet's attachment, which allowedListeners approved. Both HTTPS listeners share port 443 and are told apart by hostname (SNI).
Step 10 — Clean Up
Deleting the namespaces removes almost everything created in this article. storefront-route lives in storefront, which stays, so delete it explicitly.
If you created storefront only for this article, delete it as well.
Summary
- Gateway API allows cross-namespace references, but always with consent from the side being referenced. This prevents the confused deputy problem that made Ingress forbid them entirely.
- A shared Gateway's listener controls attachment with
allowedRoutes:Sameby default,All, orSelectoron namespace labels. A refused route showsAccepted=False (NotAllowedByListeners). - A route pointing at a Service in another namespace needs a ReferenceGrant in the Service's namespace. Without it the route shows
ResolvedRefs=False (RefNotPermitted)and Envoy Gateway answers that rule with500, while the other rules keep working. - A Gateway using a TLS Secret from another namespace needs a ReferenceGrant in the Secret's namespace, with
from.kind: Gateway. - ReferenceGrant names a namespace and kind in
from; always setto.nameso you grant one resource, not the whole namespace. Deleting the grant revokes access immediately. - ReferenceGrant is
gateway.networking.k8s.io/v1since Gateway API v1.5 (v1beta1on older CRDs). - ListenerSet (Standard since v1.5) lets application teams add their own listeners and certificates to a shared Gateway, approved through
allowedListeners.
In the next and final article of this series we put everything together and migrate an existing Ingress NGINX setup to Gateway API with ingress2gateway.