ListenerSet¶
ListenerSet (gateway.networking.k8s.io/v1, Standard channel as of v1.5.0) lets a separate resource attach additional listeners to an existing Gateway without modifying the Gateway itself. The typical use case is multi-tenant Gateway management — a platform team owns the Gateway, individual teams own a ListenerSet per tenant that contributes hostnames and route-binding rules without ever touching the Gateway spec.
Quick example¶
The Gateway opts in to ListenerSet attachment via spec.allowedListeners:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: shared-gateway
namespace: platform
spec:
gatewayClassName: cloudflare-tunnel
listeners:
- name: http
port: 80
protocol: HTTP
hostname: shared.example.com
allowedRoutes:
namespaces:
from: All
allowedListeners:
namespaces:
from: Same
A team adds their own listener through a ListenerSet in the same namespace:
apiVersion: gateway.networking.k8s.io/v1
kind: ListenerSet
metadata:
name: team-a-listeners
namespace: platform
spec:
parentRef:
group: gateway.networking.k8s.io
kind: Gateway
name: shared-gateway
listeners:
- name: team-a-http
port: 80
protocol: HTTP
hostname: team-a.example.com
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
tenant: team-a
Routes attach via the ListenerSet (or directly to the Gateway):
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: team-a-app
namespace: team-a
labels:
tenant: team-a
spec:
parentRefs:
- group: gateway.networking.k8s.io
kind: ListenerSet
name: team-a-listeners
namespace: platform
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: team-a-app
port: 8080
The route's effective hostname is the parent listener's team-a.example.com (inherited because the route declares no spec.hostnames).
How attachment works¶
A ListenerSet is successfully attached to a Gateway when:
- The parent Gateway's
spec.allowedListeners.namespaces.frompermits the ListenerSet's namespace (Same,All,Selector, or unset/Noneto reject). - The ListenerSet has
Accepted: Trueon its status — at least one of its listener entries is conflict-free AND has its TLS cert refs resolved (ResolvedRefs: True, or it carries no TLS material).
The Gateway's status.attachedListenerSets field is the count of ListenerSets meeting both criteria.
Precedence and conflict resolution¶
Per Gateway API spec the effective listener list is concatenated as follows:
- Listeners declared on the Gateway itself.
- Listeners from attached ListenerSets, ordered by
metadata.creationTimestamp(oldest first). - Within the same timestamp, ListenerSets are ordered alphabetically by
namespace/name.
When two listeners share the same (port, hostname) tuple, the higher-precedence one wins; the lower-precedence one is marked Conflicted: true with reason HostnameConflict and Accepted: false. When two listeners share a port but disagree on protocol, the same precedence applies with reason ProtocolConflict. Gateway listeners always win conflicts against ListenerSets.
A ListenerSet with at least one conflict-free, fully-resolved (ResolvedRefs: True) listener still surfaces Accepted: true overall; only the individual conflicting or unresolved entries are rejected. A ListenerSet whose every listener conflicts (or has unresolved refs) gets Accepted: false / ListenersNotValid.
ReferenceGrant scoping¶
ReferenceGrants applied to a Gateway are not inherited by child ListenerSets. A ListenerSet referencing a Secret or Service in another namespace needs its own ReferenceGrant whose from.kind is ListenerSet:
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: cert-for-listener-set
namespace: certs
spec:
from:
- group: gateway.networking.k8s.io
kind: ListenerSet
namespace: platform
to:
- group: ""
kind: Secret
Status conditions¶
Top-level ListenerSet conditions¶
| Type | Status | Reason | Description |
|---|---|---|---|
Accepted | True | Accepted | Permitted by Gateway and at least one entry is valid |
Accepted | False | NotAllowed | Gateway's spec.allowedListeners rejects this ListenerSet |
Accepted | False | ListenersNotValid | All entries are conflict-marked or have unresolved refs |
Programmed | True | Programmed | Attached and programmed against the parent Gateway |
Programmed | False | ListenersNotValid / NotAllowed / Pending | Mirrors the Accepted reason when not programmed |
Per-entry conditions (status.listeners[])¶
| Type | Status | Reason | Description |
|---|---|---|---|
Accepted | True | Accepted | Entry accepted |
Accepted | False | HostnameConflict | Same (port, hostname) claimed by a higher-precedence listener |
Accepted | False | ProtocolConflict | Different protocol claimed for the same port |
Programmed | True | Programmed | Entry programmed; routes can bind |
Conflicted | True | HostnameConflict / ProtocolConflict | Conflict surfaced |
Conflicted | False | NoConflicts | Entry has no conflicts |
ResolvedRefs | True | ResolvedRefs | Cert refs (if any) resolved |
ResolvedRefs | False | RefNotPermitted | Cross-namespace cert ref denied by missing ReferenceGrant |
ResolvedRefs | False | InvalidCertificateRef | Cert Secret missing, wrong type, or missing data |
AttachedRoutes¶
Each per-entry status reports attachedRoutes — the number of Routes bound to that listener entry. Per the Gateway API spec, attachment depends solely on the entry's allowedRoutes and the Route's parentRefs (plus the Route's own Accepted state); the listener's own status does not change the count. A Route attached to an entry that is Conflicted, or whose Programmed is False because its TLS certificate ref failed to resolve, is still counted — the spec requires attachedRoutes to be set even when the entry's own Accepted condition is False. The field therefore measures binding and blast radius, not whether the entry currently serves traffic. A ListenerSet rejected at the resource level (not permitted by the parent Gateway's allowedListeners) reports attachedRoutes: 0 for every entry, because the entries are not part of any merged Gateway.
DNS automation (external-dns)¶
If you rely on external-dns to publish the tunnel CNAME for your hostnames, note that a route attached only via a ListenerSet parentRef needs external-dns to follow that parentRef to the parent Gateway's status address. external-dns supports this through the opt-in --gateway-listener-sets flag (available since external-dns v0.21.0). Without the flag, external-dns skips Kind=ListenerSet parentRefs and the hostname gets no DNS record even though the controller programs the route correctly.
Two ways to handle it:
- Enable the flag (recommended): add
--gateway-listener-setsto the external-dns deployment args. external-dns then resolves the target through the ListenerSet → parent Gateway chain. Theexternal-dns.kubernetes.io/targetannotation is also honoured directly onListenerSetresources, taking precedence over the parent Gateway's target annotation. - Keep a direct Gateway parentRef alongside the ListenerSet one: the route then has two parents, the controller programs it once, and external-dns resolves the DNS record via the Gateway parent regardless of the flag.
This is an external-dns behaviour, not a controller limitation — the controller programs the route through the tunnel in both cases.
Accurate as of late spring 2026
The flag name, the minimum external-dns version, and the ListenerSet handling above reflect external-dns as of late spring 2026. external-dns moves fast — before relying on this, confirm against the upstream Gateway API source docs, which are the authoritative source.
Tunnel-specific notes¶
Cloudflare Tunnel is a single ingress point — port, protocol, and tls on Gateway listeners are accepted for spec compliance but the real TLS termination happens at Cloudflare's edge. The same constraint applies to ListenerSet listeners: their TLS material is validated (existence, type, PEM, ReferenceGrant) but not used for cipher negotiation. Multi-port and protocol-specific behaviour (TCP/UDP) is not supported.