Bring Your Own Listener: Gateway API, ListenerSets, and cert-manager
Gateway API is the Kubernetes-native successor to Istio's Gateway and VirtualService, and for a single service the migration is a mechanical, two-resource translation. The interesting part comes after: ListenerSets, standard in v1.5, let every team bring their own listeners and certificates to one shared gateway, and cert-manager makes those certificates issue and renew themselves. One front door, many owners.
Istio's Gateway and VirtualService have run our front door for years, and they still work fine. But the ecosystem has converged on Gateway API as the standard way traffic enters a Kubernetes cluster: Istio implements it natively, intends to make it the default for traffic management, and the newest capabilities are landing there first. This piece is the map I wanted when I started planning our own adoption: what actually changes coming from Istio's APIs, why a one-to-one migration is smaller than it looks, and how ListenerSets plus cert-manager turn a shared gateway into something neither API offered before: one load balancer, many teams, and TLS that manages itself.
Same engine, a new steering wheel
Gateway API is a family of typed, role-aware resources: a Gateway owns an entry point, and protocol-specific routes (HTTPRoute, GRPCRoute, TCPRoute) attach to it. In Istio it is a first-class implementation, not a compatibility layer: the same Envoy data plane and the same control plane sit underneath, so adopting it swaps the steering wheel, not the engine.
Coming from Istio's APIs, three differences do most of the explaining. First, an Istio Gateway is configuration for a proxy you deploy and select yourself; a Gateway API Gateway configures and provisions the proxy. Create one with gatewayClassName: istio and Istio spins up a Deployment and Service for it, resizing and updating them as the Gateway changes. Second, a VirtualService carries every protocol in one resource, while Gateway API splits routing into typed resources per protocol. Third, the division of responsibility is built into the API instead of held up by convention: a Gateway declares which namespaces may attach routes, and routes attach themselves with parentRefs. Tap through the pairs to see each translation side by side:
apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
name: edge
namespace: istio-system
spec:
selector:
istio: ingressgateway
# config for a proxy you
# deployed separately
servers:
- port:
number: 443
name: https
protocol: HTTPS
hosts:
- shop.example.com
tls:
mode: SIMPLE
credentialName: shop-tlsapiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: edge
namespace: edge
spec:
gatewayClassName: istio
# Istio deploys the proxy
# Deployment + Service for you
listeners:
- name: https
hostname: shop.example.com
port: 443
protocol: HTTPS
tls:
mode: Terminate
certificateRefs:
- name: shop-tlsThe fundamental shift: an Istio Gateway configures a proxy you run and select yourself, while a Gateway API Gateway configures and provisions its own proxy. The selector disappears, a hosts list becomes one hostname per listener, and the resource can live in any namespace.
Where a single Gateway runs out of road
The translation gets you parity. The architectural question is what happens when the next twenty teams arrive, because Gateway API gives you two obvious shapes and both strain at scale. Give every team its own Gateway and, in Istio's implementation, every team gets its own proxy Deployment and its own cloud load balancer: clean isolation, multiplied cost, and a new IP for every DNS record. Put everyone behind one shared Gateway instead and that Gateway becomes the busiest resource in the platform: every team's listeners live on it, every change edits it, it holds at most 64 listeners, and every TLS Secret it references concentrates in its namespace rather than with the teams the certificates belong to.
The pressure is real at surprisingly ordinary scale. GEP-1713, the proposal this all comes from, was motivated by platforms like Knative that need a certificate per service, on the order of a thousand certificates behind one entry point, where funneling every change through a single shared resource simply stops scaling.
ListenerSets: bring your own listener
Gateway API v1.5 (February 2026) promoted ListenerSet to the standard channel, and it is the missing piece for the shared shape. A ListenerSet is a bundle of listeners that lives in a team's namespace and attaches to a parent Gateway through a parentRef. The controller merges those listeners into the shared data plane as if they had been written on the Gateway itself: one proxy fleet, one load balancer, one IP, many owners. The Gateway stays a short, platform-owned resource written once; hostnames, TLS configuration, and certificates live with the teams they belong to.
What makes it safe to share is that the multi-tenancy rules are part of the API:
- Attachment is opt-in. allowedListeners on the Gateway defaults to None; the platform grants access to the same namespace, a label Selector, or all namespaces. A namespace label becomes the onboarding switch.
- Conflicts resolve deterministically, by age: the Gateway's own listeners first, then ListenerSets by creation time. A newer listener claiming an already-taken port and hostname is marked Conflicted in status and never programmed, so nobody acquires someone else's hostname by applying a newer resource.
- Certificates stay private by design. A ListenerSet references Secrets in its own namespace with no ReferenceGrant, and grants given to the Gateway are not inherited by ListenerSets (nor the reverse), so teams cannot reach each other's keys.
- The 64-listener ceiling stops mattering: each ListenerSet carries up to 64 listeners and a Gateway accepts many sets, which is how one entry point serves hundreds of hostnames.
- Routes are explicit about their parent. An HTTPRoute that targets the Gateway attaches only to the Gateway's own listeners; to use a team listener, the route names the ListenerSet in parentRefs.
One Gateway, three resources per team
In practice the platform team writes the Gateway once, and every team after that ships a self-contained pair: a ListenerSet for their hostname and an HTTPRoute for their routing, plus one annotation that takes care of the certificate in the next section. Everything a team owns sits in their namespace:
# Platform team, once: open the door to team namespaces.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: edge
namespace: edge
spec:
gatewayClassName: istio
listeners:
- name: http # port 80 stays here: ACME challenges ride it
protocol: HTTP
port: 80
allowedRoutes:
namespaces: { from: All }
allowedListeners:
namespaces:
from: Selector
selector:
matchLabels: { edge-access: granted }
---
# Application team: their hostname, their listener, their namespace.
apiVersion: gateway.networking.k8s.io/v1
kind: ListenerSet
metadata:
name: payments-edge
namespace: team-payments
annotations:
cert-manager.io/cluster-issuer: letsencrypt
acme.cert-manager.io/http01-parentreffallback: "true"
spec:
parentRef:
name: edge
namespace: edge
listeners:
- name: https
hostname: pay.example.com
port: 443
protocol: HTTPS
tls:
mode: Terminate
certificateRefs:
- name: pay-tls # a Secret in team-payments; no grant needed
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: payments
namespace: team-payments
spec:
parentRefs:
- kind: ListenerSet
name: payments-edge
hostnames: ["pay.example.com"]
rules:
- backendRefs:
- name: payments
port: 8080cert-manager closes the loop
cert-manager has understood Gateways since 1.15: annotate one with an issuer and its gateway shim manufactures a Certificate for every listener that qualifies (a hostname, tls.mode Terminate, and a certificateRef to a Secret in the same namespace), folding listeners that share a Secret name into one Certificate with merged dnsNames. cert-manager 1.20 extends the same shim to ListenerSets, and that completes the self-service story: the annotation sits on the team's own resource, so the Certificate and its Secret are created in the team's namespace, right next to the listener that uses them. When both the Gateway and a ListenerSet carry annotations, the ListenerSet's win.
From the ListenerSet above, the team never writes a Certificate. This appears on its own:
# Created by the shim, not by the team.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: pay-tls
namespace: team-payments
annotations:
acme.cert-manager.io/http01-parentrefkind: Gateway
acme.cert-manager.io/http01-parentrefname: edge
acme.cert-manager.io/http01-parentrefnamespace: edge
spec:
dnsNames: [pay.example.com]
issuerRef:
kind: ClusterIssuer
name: letsencrypt
secretName: pay-tls
usages: [digital signature, key encipherment]How a challenge flows
For ACME issuers there is one more elegant detail. With the gatewayHTTPRoute solver, cert-manager proves domain ownership by publishing a temporary HTTPRoute for /.well-known/acme-challenge, so the proof rides the same gateway being certified. An HTTPS-only ListenerSet has no port-80 listener to serve that challenge on, which is exactly what the http01-parentreffallback annotation solves: it points the challenge route at the parent Gateway's shared HTTP listener. That is why the platform Gateway keeps a port-80 listener open to all namespaces: it is the shared driveway every certificate request rolls in on.
graph LR
CM[cert-manager] -->|1. temporary challenge HTTPRoute| GW["Gateway edge<br/>listener :80"]
LE["Let's Encrypt"] -->|2. GET /.well-known/acme-challenge| GW
GW -->|3. routes the probe| SP[solver pod]
SP -->|4. token matches| LE
LE -->|5. signs the certificate| CM
CM -->|6. writes tls.crt + tls.key| SE["Secret pay-tls<br/>in team-payments"]
SE -->|7. served on :443 via SNI| GWInside the shim
End to end, here is everything that happens between a team applying an annotated ListenerSet and their hostname serving HTTPS, including the renewals that keep happening forever after. Step through it:
The team ships a ListenerSet in their own namespace: an HTTPS listener for pay.example.com, tls.mode Terminate, a certificateRef to a Secret named pay-tls that does not exist yet, and the annotation cert-manager.io/cluster-issuer: letsencrypt. The shared Gateway is never edited.
What this unlocks
Put the three pieces together and a shared front door becomes genuinely self-service, with the guardrails living in the API rather than in process. The platform team runs one Gateway, one proxy fleet, one load balancer. Application teams own their hostnames end to end: listener, route, and certificate, all in their namespace, all renewing themselves. And the pattern reaches places a single Gateway could not go: SaaS products serving a certificate per customer domain, Knative-style platforms issuing one per service, or simply an organization where twenty teams share one IP without sharing write access to anything.
The ecosystem is arriving quickly. ListenerSet is standard as of Gateway API v1.5, Envoy Gateway ships the graduated API since 1.8, Istio supports the experimental XListenerSet today with v1 support on its roadmap, and cert-manager wired ListenerSets into the shim in 1.20. The direction of travel is clear, and it is a good one: the front door stays shared, and everyone holds their own key.