For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.
EnterpriseListenerSet support
Attach extra listeners to an Istio Gateway on platforms where the Kubernetes Gateway API is pinned below version 1.5, such as OpenShift.
About
Use an EnterpriseListenerSet (enterprise.solo.io/v1alpha1) to attach extra listeners to a Kubernetes Gateway on platforms where the Kubernetes Gateway API CRD version is pinned at 1.4 or earlier, such as OpenShift. EnterpriseListenerSet provides the same listener-attachment behavior as the upstream ListenerSet. Solo Enterprise for Istio maps EnterpriseListenerSet to ListenerSet internally, so the platform does not need to provide the ListenerSet CRD. Use EnterpriseListenerSet as a temporary bridge until your platform upgrades to Gateway API 1.5.
Important
The Gloo Operator installs Kubernetes Gateway API 1.4.0. Gateway API 1.4 does not include the upstream ListenerSet CRD, so a mesh that the Gloo Operator manages has two options for extra listeners.
- Stay on Gateway API 1.4.0 and use
EnterpriseListenerSet, as described in this guide. The rest of the guide assumes this option. - Install Solo Enterprise for Istio with Helm instead of the Gloo Operator, apply the Kubernetes Gateway API 1.5.0 standard channel, and use the upstream
ListenerSet. For more information, see the Helm installation guide.
The two resources can coexist in a cluster that provides the ListenerSet CRD. On a listener that both resources claim, the ListenerSet wins. For more information, see Key behaviors.
Note
The EnterpriseListenerSet CRD is not included in the Solo Enterprise for Istio Helm charts. You must install it separately.
Key behaviors
The following rules govern how EnterpriseListenerSet interacts with ListenerSet and with the Gateway. Review these rules before you configure your resources. The setup steps and migration sections build on them.
- A Gateway API
ListenerSetalways takes precedence over anEnterpriseListenerSeton a conflicting listener, based on port and hostname. Kind determines precedence, not creation time. - Solo Enterprise for Istio evaluates conflicts listener by listener, not object by object. Only the listener that collides loses. Other listeners on the same
EnterpriseListenerSetcontinue to serve. - When a
ListenerSetand anEnterpriseListenerSetshare the same namespace and name, theEnterpriseListenerSetloses all its listeners, not only the conflicting ones. - Routes do not follow a listener when it moves between objects. A
parentRefnames a kind, so a route bound tokind: EnterpriseListenerSetdoes not attach to aListenerSetlistener automatically. - Cross-namespace certificate references from an
EnterpriseListenerSetmust be authorized by aReferenceGrantthat names kindListenerSetin groupgateway.networking.k8s.io. A grant naming kindEnterpriseListenerSetis not recognized.
Tune Kubernetes API request deadlines
The EnterpriseListenerSet controller uses the shared Istio Kubernetes client. Kubernetes API requests from that client have client-side deadlines, so a request that never receives a response is canceled instead of waiting until istiod restarts. The default deadlines suit most clusters, but you can tune them on istiod if a slow or heavily loaded Kubernetes API server needs more time.
| Environment variable | Default | Use |
|---|---|---|
ISTIO_KUBE_HEADER_TIMEOUT | 90s | Maximum time to wait for the first response headers. Keep this value above the Kubernetes API server --request-timeout, which defaults to 60s. |
ISTIO_KUBE_REQUEST_TIMEOUT | 120s | Total time for a non-streaming Kubernetes API request to finish. Keep this value greater than ISTIO_KUBE_HEADER_TIMEOUT. |
ISTIO_KUBE_STREAM_TIMEOUT | 15m | Total time for a watch, log follow, or other streaming request. Keep this value greater than the API server maximum watch duration. |
ISTIO_INFORMER_REQUEST_TIMEOUT | 15m | Total time for an informer list request. Set this value high enough for a full list of the largest resource in the cluster. |
Set any of these variables to 0 to disable that deadline and restore the previous unbounded behavior. When a deadline cancels a request, istiod logs a warning that names the Kubernetes API method and path, and increments the kube_client_deadline_fired_total metric. For the full generated reference, see Istiod environment variables.
Listener set options
Use the following table to find the right section for your situation.
| Kubernetes Gateway API version | Existing listener set | Section |
|---|---|---|
| 1.4 or earlier | None | Set up an EnterpriseListenerSet |
| Moving from 1.4 or earlier to 1.5 or later | EnterpriseListenerSet | Migrate from EnterpriseListenerSet to ListenerSet |
| Moving from 1.5 or later to 1.4 or earlier | ListenerSet | Migrate from ListenerSet to EnterpriseListenerSet |
Set up an EnterpriseListenerSet
Before you begin
Install Solo Enterprise for Istio 1.30.3-patch0 or later. Earlier 1.30.x patch releases do not include EnterpriseListenerSet support.
Set the following environment variables.
# Gateway name and namespace export GATEWAY_NS=<gateway-namespace> export GATEWAY_NAME=<gateway-name> # Do not give the EnterpriseListenerSet the same name as an existing ListenerSet. # A ListenerSet sharing that namespace and name removes all listeners from the # EnterpriseListenerSet, regardless of which Gateway each object references. export ELS_NS=<els-namespace> export ELS_NAME=<els-name> # If any listeners reference certificates in a different namespace export CERT_NS=<cert-namespace> export CERT_NAME=<cert-name>Configure a Gateway or confirm that an existing Gateway has
spec.allowedListenersset. Without it, the Gateway admits no listener sets, and the EnterpriseListenerSet reportsAccepted=Falsewith reasonNotAllowed.kubectl -n $GATEWAY_NS get gateway $GATEWAY_NAME \ -o jsonpath='{.spec.allowedListeners}{"\n"}'Example output for a Gateway that admits all namespaces:
{"namespaces":{"from":"All"}}An empty output means that the Gateway does not admit any listener set. Add the following field to your Gateway configuration.
spec: allowedListeners: namespaces: from: All # or Same, or SelectorConfirm that the Gateway class supports EnterpriseListenerSet. The EnterpriseListenerSet controller runs only under the default Istio Gateway class.
kubectl -n $GATEWAY_NS get gateway $GATEWAY_NAME \ -o jsonpath='{.spec.gatewayClassName}'The expected output is
istio, or the value ofPILOT_GATEWAY_API_DEFAULT_GATEWAYCLASS_NAMEif overridden. Theistio-agentgatewayclass does not support EnterpriseListenerSet.
Configure the EnterpriseListenerSet
Install the EnterpriseListenerSet CRD from the Solo Enterprise for kgateway CRD Helm chart, version 2.1 or later, by setting
installEnterpriseListenerSetCRD=true.- If you already use Solo Enterprise for kgateway in this cluster, upgrade the existing CRD release to add the flag.
helm upgrade enterprise-kgateway-crds oci://us-docker.pkg.dev/solo-public/enterprise-kgateway/charts/enterprise-kgateway-crds \ --namespace kgateway-system \ --version 2.2.8 \ --reuse-values \ --set installEnterpriseListenerSetCRD=true - If you do not use Solo Enterprise for kgateway, install only the EnterpriseListenerSet CRD by disabling the other CRDs in the chart.
helm upgrade -i enterprise-kgateway-crds oci://us-docker.pkg.dev/solo-public/enterprise-kgateway/charts/enterprise-kgateway-crds \ --create-namespace \ --namespace kgateway-system \ --version 2.2.8 \ --set installEnterpriseListenerSetCRD=true \ --set installExtAuthCRDs=false \ --set installRateLimitCRDs=false \ --set installKgatewayCRDs=false \ --set installWAFPolicyCRD=false
- If you already use Solo Enterprise for kgateway in this cluster, upgrade the existing CRD release to add the flag.
Verify that the CRD installed successfully.
kubectl get crd enterpriselistenersets.enterprise.solo.ioExample output:
NAME CREATED AT enterpriselistenersets.enterprise.solo.io 2026-08-14T00:00:00ZIf any listeners reference certificate secret resources in a different namespace from the EnterpriseListenerSet, authorize the cross-namespace access with a ReferenceGrant. A ReferenceGrant authorizes access by the referencing object’s kind. The grant must use kind
ListenerSetin groupgateway.networking.k8s.io, because the same grant works unchanged when you later migrate from EnterpriseListenerSet to ListenerSet. Apply the grant in the namespace where the secret (or configmap forcaCertificateRefs) lives. Itsfrom.namespacenames the namespace of the listener set, not the Gateway.Important
A missing grant causes the listener to report
ResolvedRefs=Falsewith reasonRefNotPermitted. TLS connections fail withconnection refusedeven though the Gateway service port appears open.kubectl apply -f- <<EOF apiVersion: gateway.networking.k8s.io/v1beta1 kind: ReferenceGrant metadata: name: allow-cert namespace: $CERT_NS # where the secret lives spec: from: - group: gateway.networking.k8s.io kind: ListenerSet namespace: $ELS_NS # where the EnterpriseListenerSet lives to: - group: "" kind: Secret name: $CERT_NAME # omit to allow access to all secrets in the namespace EOFCreate an EnterpriseListenerSet in the namespace where your listeners exist. The
parentRefmust name both the Gateway and the Gateway’s namespace.Warning
Do not give the EnterpriseListenerSet the same name as an existing ListenerSet. A ListenerSet sharing that namespace and name removes all listeners from the EnterpriseListenerSet, regardless of which Gateway each object references.
The EnterpriseListenerSet
specmirrors the Kubernetes Gateway API ListenerSetspec. Fields such ashostname,port,protocol,tls, andallowedRouteshave the same semantics.kubectl apply -f- <<EOF apiVersion: enterprise.solo.io/v1alpha1 kind: EnterpriseListenerSet metadata: name: $ELS_NAME namespace: $ELS_NS spec: parentRef: group: gateway.networking.k8s.io kind: Gateway name: $GATEWAY_NAME namespace: $GATEWAY_NS listeners: - name: http hostname: example.com port: 8080 protocol: HTTP allowedRoutes: namespaces: from: All - name: https hostname: secure.example.com port: 8443 protocol: HTTPS tls: mode: Terminate certificateRefs: - group: "" kind: Secret name: $CERT_NAME namespace: $CERT_NS allowedRoutes: namespaces: from: All EOFVerify that each listener is accepted.
kubectl -n $ELS_NS get enterpriselistenerset $ELS_NAME \ -o jsonpath='{range .status.listeners[*]}{.name}: {range .conditions[?(@.type=="Accepted")]}{.status}{end}{"\n"}{end}'Example output:
http: True https: TrueVerify that the HTTPS listener resolves its certificate.
kubectl -n $ELS_NS get enterpriselistenerset $ELS_NAME \ -o jsonpath='{range .status.listeners[?(@.name=="https")]}{range .conditions[*]}{.type}={.status}{"\n"}{end}{end}'Example output:
Accepted=True Conflicted=False Programmed=True ResolvedRefs=True
Note
Use EnterpriseListenerSet as a temporary bridge until your platform upgrades to Gateway API 1.5. When that upgrade happens, see Migrate from EnterpriseListenerSet to ListenerSet.
Migrate from EnterpriseListenerSet to ListenerSet
Move listeners from an EnterpriseListenerSet to a ListenerSet once your platform provides the ListenerSet CRD (Gateway API 1.5 or later). This is the off-ramp from EnterpriseListenerSet when the platform constraint that required it no longer applies.
Because a ListenerSet outranks an EnterpriseListenerSet immediately on creation, routes must be re-pointed before the ListenerSet exists. A route that still names kind: EnterpriseListenerSet does not follow the listener when the ListenerSet takes over, and the new listener serves 404s until the route is updated. The recommended procedure migrates one listener at a time so that each step can be verified independently before the previous listener is removed.
Warning
Do not create the ListenerSet listener before re-pointing routes. The ListenerSet wins the port on arrival. Any route still pointing at kind: EnterpriseListenerSet is immediately orphaned and the listener serves 404s for an unbounded window while you update routes that may live in other teams’ namespaces.
Before you begin
Set the following environment variables.
# Gateway name and namespace export GATEWAY_NS=<gateway-namespace> export GATEWAY_NAME=<gateway-name> # EnterpriseListenerSet to migrate from export ELS_NS=<els-namespace> export ELS_NAME=<els-name> # ListenerSet to migrate to # Do not use the same namespace and name as the EnterpriseListenerSet. # A ListenerSet sharing that namespace and name removes all listeners from the # EnterpriseListenerSet at once, bypassing the per-listener migration. export LS_NS=<ls-namespace> export LS_NAME=<ls-name> # HTTPRoute that names the EnterpriseListenerSet export ROUTE_NS=<route-namespace> export ROUTE_NAME=<route-name> # If any listeners reference certificates in a different namespace export CERT_NS=<cert-namespace> export CERT_NAME=<cert-name> export GRANT_NAME=<grant-name>Confirm that the ListenerSet CRD is installed.
kubectl get crd listenersets.gateway.networking.k8s.ioIf the CRD is not present, the cluster does not have the Kubernetes Gateway API version 1.5 installed. This is expected on a mesh that the Gloo Operator manages, because the operator installs Gateway API 1.4.0. Continue using the
EnterpriseListenerSetresource until the cluster uses the Gateway API 1.5 or later.Confirm that the Gateway has
spec.allowedListenersset. Without it, the Gateway admits no listener sets, and the ListenerSet reportsAccepted=Falsewith reasonNotAllowed.kubectl -n $GATEWAY_NS get gateway $GATEWAY_NAME \ -o jsonpath='{.spec.allowedListeners}{"\n"}'Example output for a Gateway that admits all namespaces:
{"namespaces":{"from":"All"}}An empty output means that the Gateway does not admit any listener set. Add the following field to your Gateway configuration.
spec: allowedListeners: namespaces: from: All # or Same, or SelectorConfirm that the Gateway class supports ListenerSet. The ListenerSet controller runs only under the default Istio Gateway class.
kubectl -n $GATEWAY_NS get gateway $GATEWAY_NAME \ -o jsonpath='{.spec.gatewayClassName}'The expected output is
istio, or the value ofPILOT_GATEWAY_API_DEFAULT_GATEWAYCLASS_NAMEif overridden. Theistio-agentgatewayclass does not support ListenerSet.Save rollback backups. If the migration fails, use these files to restore the original state.
Save the EnterpriseListenerSet configuration.
kubectl -n $ELS_NS get enterpriselistenerset $ELS_NAME -o json \ | jq 'del(.status, .metadata.uid, .metadata.resourceVersion, .metadata.generation, .metadata.creationTimestamp, .metadata.managedFields)' > els.bak.jsonSave all HTTPRoutes that reference the EnterpriseListenerSet.
kubectl get httproute -A -o json \ | jq '[.items[] | select(.spec.parentRefs[]?.kind == "EnterpriseListenerSet")]' > routes.bak.jsonSave all ReferenceGrants.
kubectl get referencegrant -A -o yaml > grants.bak.yaml
Save baselines to verify against after the migration.
Save the Gateway service ports. The clean up section compares this file against the live service to confirm that no ports changed.
kubectl -n $GATEWAY_NS get svc $GATEWAY_NAME-istio \ -o jsonpath='{range .spec.ports[*]}{.name}={.port}{"\n"}{end}' > service-ports.bak.txtVerify that every listener you want to migrate actively serves. Record the HTTP status code for each hostname. The verification steps later in this procedure compare against these baselines.
ADDR=$(kubectl -n $GATEWAY_NS get gateway $GATEWAY_NAME \ -o jsonpath='{.status.addresses[0].value}') curl -s -o /dev/null -w '%{http_code}\n' \ --resolve plain.example.com:8080:$ADDR http://plain.example.com:8080/Note
If the Gateway has no external LoadBalancer address, use the service
ClusterIPasADDRand runcurlfrom inside the cluster with a short-lived pod (kubectl run curltest --image=curlimages/curl:8.11.1 --restart=Never -- sleep 3600).
Migrate each listener
Migrate your listeners. Be sure to complete all steps for one listener before starting the next.
Save the name and namespace of a listener to migrate. Change these values each time you repeat these steps for each listener.
export LISTENER_NAME=<listener-name> export LISTENER_PORT=<port>If any listener references a certificate secret in a different namespace, widen the ReferenceGrant to also authorize the ListenerSet before creating it. A ReferenceGrant authorizes access by the referring object’s kind. The grant covering the EnterpriseListenerSet does not cover the ListenerSet.
Widen the existing grant with a JSON patch rather than re-applying it by hand, to avoid accidentally dropping an entry. Loop over every grant that covers a secret referenced by the listener you are migrating.
kubectl -n $CERT_NS patch referencegrant $GRANT_NAME --type=json \ -p='[{"op":"add","path":"/spec/from/-","value":{"group":"gateway.networking.k8s.io","kind":"ListenerSet","namespace":"$LS_NS"}}]'Verify the grant includes both kinds.
kubectl -n $CERT_NS get referencegrant $GRANT_NAME \ -o jsonpath='{.spec.from}'
Warning
A staged listener that has not yet won its port does not evaluate its
certificateRef. If you skip this step and the grant does not cover the ListenerSet, the certificate fails to resolve the moment the ListenerSet takes over. The service port stays open but nothing serves behind it, and clients receiveconnection refused. Also check whether the parent Gateway validates client certificates withcaCertificateRefs. Those references are re-evaluated against the listener set’s kind and namespace. Add theListenerSetkind to any grant covering those references, and add atoentry withkind: ConfigMapif the reference points at a configmap rather than a secret.Add the ListenerSet as an additional
parentRefto every route that currently names this listener on the EnterpriseListenerSet. Keep the existing EnterpriseListenerSetparentRefin place. Append with a JSON patch to avoid losing any existingparentRefs or route filters.kubectl -n $ROUTE_NS patch httproute $ROUTE_NAME --type=json \ -p='[{"op":"add","path":"/spec/parentRefs/-","value":{"group":"gateway.networking.k8s.io","kind":"ListenerSet","name":"$LS_NAME","namespace":"$LS_NS","sectionName":"$LISTENER_NAME"}}]'Verify the route
spec. The new ListenerSet listener does not exist yet, so the route carries no status entry for it.kubectl -n $ROUTE_NS get httproute $ROUTE_NAME \ -o jsonpath='{range .spec.parentRefs[*]}{.kind}/{.name}/{.sectionName} {end}'Each
sectionNamemust match the listener name you are about to create on the ListenerSet.EnterpriseListenerSet/$ELS_NAME/plain ListenerSet/$LS_NAME/plainCreate the ListenerSet listener.
- First listener: If this is the first listener you are migrating, create the ListenerSet.
kubectl apply -f- <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: ListenerSet metadata: name: $LS_NAME # must not equal the EnterpriseListenerSet name in the same namespace namespace: $LS_NS spec: parentRef: group: gateway.networking.k8s.io kind: Gateway name: $GATEWAY_NAME namespace: $GATEWAY_NS listeners: - name: plain hostname: plain.example.com port: 8080 protocol: HTTP allowedRoutes: namespaces: from: All EOF - Subsequent listeners: If you already migrated an initial listener, the ListenerSet already exists. Append the subsequent listener to the ListenerSet with a JSON patch rather than re-applying the full manifest.
kubectl -n $LS_NS patch listenerset $LS_NAME --type=json \ -p='[{"op":"add","path":"/spec/listeners/-","value":{"name":"secure","hostname":"secure.example.com","port":8443,"protocol":"HTTPS","tls":{"mode":"Terminate","certificateRefs":[{"kind":"Secret","name":"$CERT_NAME","namespace":"$CERT_NS"}]},"allowedRoutes":{"namespaces":{"from":"All"}}}}]'
- First listener: If this is the first listener you are migrating, create the ListenerSet.
Verify that the EnterpriseListenerSet listener you just replaced now reports
Conflicted=True, and that no other listeners changed state.kubectl -n $ELS_NS get enterpriselistenerset $ELS_NAME \ -o jsonpath='{range .status.listeners[?(@.name=="plain")]}{range .conditions[*]}{.type}={.status} reason={.reason}{"\n"}{end}{end}'Example output:
Accepted=False reason=HostnameConflict Conflicted=True reason=HostnameConflict Programmed=False reason=HostnameConflictVerify that the ListenerSet listener has routes attached.
kubectl -n $LS_NS get listenerset $LS_NAME \ -o jsonpath='{range .status.listeners[*]}{.name}={.attachedRoutes} {end}'Example output:
plain=1Verify that the proxy route table shows the ListenerSet as the owner. Query every Gateway pod.
for pod in $(kubectl -n $GATEWAY_NS get pods \ -l gateway.networking.k8s.io/gateway-name=$GATEWAY_NAME -o name); do echo "== $pod" istioctl -n $GATEWAY_NS proxy-config route "${pod#pod/}" | grep $LISTENER_PORT istioctl -n $GATEWAY_NS proxy-config secret "${pod#pod/}" | grep $CERT_NAME doneThe route owner field (the
~-separated identifier) must name the ListenerSet. The certificate must showVALID CERT trueon every pod.Verify that traffic to this hostname and all remaining hostnames matches the baselines from before you began.
curl -s -o /dev/null -w '%{http_code}\n' \ --resolve plain.example.com:8080:$ADDR http://plain.example.com:8080/Warning
If any gate fails, delete the ListenerSet listener. The EnterpriseListenerSet listener resumes serving as soon as the
Conflictedcondition clears. Nothing else requires undoing, because the dualparentReffrom step 2 is harmless on its own.Read the current listener order to find the index of the listener you are removing. Each removal shifts later entries down, so re-read the index before every removal.
kubectl -n $ELS_NS get enterpriselistenerset $ELS_NAME \ -o jsonpath='{range .spec.listeners[*]}{.name} {end}'Remove the migrated listener. Replace
0in the path with the index from step 10. Guard the removal with atestassertion so that the API server rejects the patch atomically if the index does not contain the expected listener.kubectl -n $ELS_NS patch enterpriselistenerset $ELS_NAME --type=json \ -p='[{"op":"test","path":"/spec/listeners/0/name","value":"plain"},{"op":"remove","path":"/spec/listeners/0"}]'Note
The CRD requires at least one listener. On the last listener, the patch is rejected; delete the object instead by running
kubectl -n $ELS_NS delete enterpriselistenerset $ELS_NAME.
Repeat steps 1–11 for each remaining listener on the EnterpriseListenerSet before continuing.
Clean up
After you migrate all listeners, remove the temporary widenings from steps 1 and 2. Remove route parentRefs before narrowing the ReferenceGrant. Narrowing the grant while the EnterpriseListenerSet still references the secret revokes the certificate on the live object.
List the parentRef kinds on each route to find the index of the EnterpriseListenerSet entry.
kubectl -n $ROUTE_NS get httproute $ROUTE_NAME \ -o jsonpath='{range .spec.parentRefs[*]}{.kind}{"\n"}{end}'Remove the EnterpriseListenerSet
parentReffrom the route, leaving the ListenerSetparentRef. Replace0in the path with the index of the EnterpriseListenerSet entry from step 1.kubectl -n $ROUTE_NS patch httproute $ROUTE_NAME --type=json \ -p='[{"op":"test","path":"/spec/parentRefs/0/kind","value":"EnterpriseListenerSet"},{"op":"remove","path":"/spec/parentRefs/0"}]'List the
fromentries on each ReferenceGrant to find the index of the EnterpriseListenerSet entry.kubectl -n $CERT_NS get referencegrant $GRANT_NAME \ -o jsonpath='{range .spec.from[*]}{.kind}{"\n"}{end}'Remove the EnterpriseListenerSet
fromentry from the ReferenceGrant. Replace0in the path with the index of the EnterpriseListenerSet entry from step 3.kubectl -n $CERT_NS patch referencegrant $GRANT_NAME --type=json \ -p='[{"op":"test","path":"/spec/from/0/kind","value":"EnterpriseListenerSet"},{"op":"remove","path":"/spec/from/0"}]'Verify that the service ports match the baseline saved before you began.
diff service-ports.bak.txt \ <(kubectl -n $GATEWAY_NS get svc $GATEWAY_NAME-istio \ -o jsonpath='{range .spec.ports[*]}{.name}={.port}{"\n"}{end}')If you used a
curltestpod during this procedure, delete it.kubectl delete pod curltest
Migrate from ListenerSet to EnterpriseListenerSet
Move listeners from a ListenerSet to an EnterpriseListenerSet. This migration applies when a cluster is re-platformed onto a Kubernetes distribution that pins the Kubernetes Gateway API CRD version below 1.5, where ListenerSet is unavailable and EnterpriseListenerSet is the supported alternative.
Because a ListenerSet always outranks an EnterpriseListenerSet on a conflicting listener, the staged EnterpriseListenerSet remains inert until the ListenerSet releases its claim. The recommended procedure creates the EnterpriseListenerSet first, verifies it is staged and conflict-detected, and then removes the ListenerSet. The Gateway service port stays open throughout and the transition happens in a single reconcile, with no listener gap.
Warning
Do not remove ListenerSet listeners before creating the EnterpriseListenerSet. When neither object claims the port, the Gateway service withdraws the port entry, listeners leave the proxy, and requests fail at the TCP level until the replacement is programmed. Create and verify the staged object first.
Before you begin
Set the following environment variables.
# Gateway name and namespace export GATEWAY_NS=<gateway-namespace> export GATEWAY_NAME=<gateway-name> # ListenerSet to migrate from export LS_NS=<ls-namespace> export LS_NAME=<ls-name> # EnterpriseListenerSet to migrate to (deployed in the same namespace as the ListenerSet) # Do not use the same name as the ListenerSet. A ListenerSet sharing that namespace and name # removes all listeners from the EnterpriseListenerSet, defeating the staged migration. export ELS_NAME=<els-name> # HTTPRoute that names the ListenerSet export ROUTE_NS=<route-namespace> export ROUTE_NAME=<route-name> # Port used to verify the proxy route table and certificate during cutover export LISTENER_PORT=<listener-port> # If any listeners reference certificates in a different namespace: export CERT_NS=<cert-namespace> export CERT_NAME=<cert-name> export GRANT_NAME=<grant-name>Confirm that you run Solo Enterprise for Istio 1.30.3-patch0 or later. Earlier 1.30.x patch releases do not include EnterpriseListenerSet support.
istioctl versionInstall the EnterpriseListenerSet CRD from the Solo Enterprise for kgateway CRD Helm chart, version 2.1 or later, by setting
installEnterpriseListenerSetCRD=true.- If you already use Solo Enterprise for kgateway in this cluster, upgrade the existing CRD release to add the flag.
helm upgrade enterprise-kgateway-crds oci://us-docker.pkg.dev/solo-public/enterprise-kgateway/charts/enterprise-kgateway-crds \ --namespace kgateway-system \ --version 2.2.8 \ --reuse-values \ --set installEnterpriseListenerSetCRD=true - If you do not use Solo Enterprise for kgateway, install only the EnterpriseListenerSet CRD by disabling the other CRDs in the chart.
helm upgrade -i enterprise-kgateway-crds oci://us-docker.pkg.dev/solo-public/enterprise-kgateway/charts/enterprise-kgateway-crds \ --create-namespace \ --namespace kgateway-system \ --version 2.2.8 \ --set installEnterpriseListenerSetCRD=true \ --set installExtAuthCRDs=false \ --set installRateLimitCRDs=false \ --set installKgatewayCRDs=false \ --set installWAFPolicyCRD=false
- If you already use Solo Enterprise for kgateway in this cluster, upgrade the existing CRD release to add the flag.
Verify that the CRD installed successfully.
kubectl get crd enterpriselistenersets.enterprise.solo.ioExample output:
NAME CREATED AT enterpriselistenersets.enterprise.solo.io 2026-08-14T00:00:00ZConfirm that the Gateway class supports EnterpriseListenerSet. The EnterpriseListenerSet controller runs only under the default Istio Gateway class.
kubectl -n $GATEWAY_NS get gateway $GATEWAY_NAME \ -o jsonpath='{.spec.gatewayClassName}'The expected output is
istio, or the value ofPILOT_GATEWAY_API_DEFAULT_GATEWAYCLASS_NAMEif overridden. Theistio-agentgatewayclass programs a real ListenerSet but does not support EnterpriseListenerSet; this migration cannot be performed on an agentgateway Gateway.Confirm that the Gateway has
spec.allowedListenersset. Without it, the Gateway admits no listener sets, and the EnterpriseListenerSet reportsAccepted=Falsewith reasonNotAllowed.kubectl -n $GATEWAY_NS get gateway $GATEWAY_NAME \ -o jsonpath='{.spec.allowedListeners}{"\n"}'Example output for a Gateway that admits all namespaces:
{"namespaces":{"from":"All"}}An empty output means that the Gateway does not admit any listener set. Add the following field to your Gateway configuration.
spec: allowedListeners: namespaces: from: All # or Same, or Selector
Set up the EnterpriseListenerSet
Save rollback backups. If the migration fails, use these files to restore the original state.
Save the ListenerSet configuration.
kubectl -n $LS_NS get listenerset $LS_NAME -o json \ | jq 'del(.status, .metadata.uid, .metadata.resourceVersion, .metadata.generation, .metadata.creationTimestamp, .metadata.managedFields)' > ls.bak.jsonImportant
The
jqfilter removes server-side fields that the API server rejects on re-create. The filteredls.bak.jsonfile is the rollback for the cutover; a plainkubectl get -o yamlbackup cannot be used to restore. The Gateway service is named$GATEWAY_NAME-<gateway-class>unless the Gateway has agateway.istio.io/name-overrideannotation.Save all HTTPRoutes that reference the ListenerSet.
kubectl get httproute -A -o json \ | jq '[.items[] | select(.spec.parentRefs[]?.kind == "ListenerSet")]' > routes.bak.jsonSave all ReferenceGrants.
kubectl get referencegrant -A -o yaml > grants.bak.yamlSave the Gateway service ports. The clean up section compares this file against the live service to confirm no ports changed.
kubectl -n $GATEWAY_NS get svc $GATEWAY_NAME-istio \ -o jsonpath='{range .spec.ports[*]}{.name}={.port}{"\n"}{end}' > service-ports.bak.txt
If any listener references a certificate secret in a different namespace, confirm that the existing ReferenceGrant uses
kind: ListenerSetin groupgateway.networking.k8s.io. The EnterpriseListenerSet controller resolves certificate references against akind: ListenerSetgrant, because it maps theEnterpriseListenerSettoListenerSetinternally before the grant lookup. The existingkind: ListenerSetgrant covers theEnterpriseListenerSet. No additional grant entries are needed.kubectl -n $CERT_NS get referencegrant $GRANT_NAME \ -o jsonpath='{.spec.from}'Warning
A staged listener that is conflicted does not evaluate its
certificateRef. A missing grant is invisible during staging and causes a TLS failure the moment the cutover promotes the listener. At that point, the service port stays open but nothing serves behind it and clients receiveconnection refused. If the parent Gateway validates client certificates withcaCertificateRefs, those references are also resolved againstkind: ListenerSetgrants. Confirm that any grant covering those references also useskind: ListenerSetin groupgateway.networking.k8s.io. If the reference points to a configmap, confirm the grant has atoentry withkind: ConfigMap.Create the EnterpriseListenerSet with every listener that you want to migrate. Copy each listener’s port, protocol, hostname,
allowedRoutes, andtlsexactly, and copy the ListenerSet’s labels to the new object. TheparentRefmust name the same Gateway as the ListenerSet.Warning
A mistyped port or hostname does not remain inert. It conflicts with nothing, so it is accepted immediately, opens that port on the Gateway service, and begins serving with no routes attached. A listener with
allowedRoutesomitted silently defaults tonamespaces.from: Sameand serves 404s at cutover for routes in other namespaces.kubectl apply -f- <<EOF apiVersion: enterprise.solo.io/v1alpha1 kind: EnterpriseListenerSet metadata: name: $ELS_NAME namespace: $LS_NS spec: parentRef: group: gateway.networking.k8s.io kind: Gateway name: $GATEWAY_NAME namespace: $GATEWAY_NS listeners: - name: plain hostname: plain.example.com port: 8080 protocol: HTTP allowedRoutes: namespaces: from: All - name: secure hostname: secure.example.com port: 8443 protocol: HTTPS tls: mode: Terminate certificateRefs: - kind: Secret group: "" name: $CERT_NAME namespace: $CERT_NS allowedRoutes: namespaces: from: All EOFVerify that every staged listener reports
Conflicted=True. A listener that is not reported as conflicted has a mistyped port or hostname, was attached to the wrong Gateway, or is already serving with no routes.kubectl -n $LS_NS get enterpriselistenerset $ELS_NAME \ -o jsonpath='{range .status.listeners[*]}{.name}{":\n"}{range .conditions[*]}{" "}{.type}={.status} reason={.reason}{"\n"}{end}{end}'Example output:
plain: Accepted=False reason=HostnameConflict Conflicted=True reason=HostnameConflict Programmed=False reason=HostnameConflict secure: Accepted=False reason=HostnameConflict Conflicted=True reason=HostnameConflict Programmed=False reason=HostnameConflictConfirm that existing traffic is unaffected before continuing. You can undo steps 3 and 4 without affecting traffic: neither the staged EnterpriseListenerSet nor the additional route
parentRefadded in the next section serves traffic while the staged listeners remain conflicted.
Re-point routes
Add the EnterpriseListenerSet as an additional
parentRefto every route that names the ListenerSet. Keep the existing ListenerSetparentRefin place.spec: parentRefs: - group: gateway.networking.k8s.io kind: ListenerSet name: $LS_NAME namespace: $LS_NS sectionName: plain - group: enterprise.solo.io kind: EnterpriseListenerSet name: $ELS_NAME namespace: $LS_NS sectionName: plainVerify the route
specrather than its status. While the staged listener is conflicted, no route can attach to it.attachedRouteson the EnterpriseListenerSet reads0and the route’s status carries no entry for the staged parent. Verify that eachsectionNamematches a listener name in the EnterpriseListenerSet.kubectl -n $ROUTE_NS get httproute $ROUTE_NAME \ -o jsonpath='{range .spec.parentRefs[*]}{.kind}/{.name}/{.sectionName} {end}'Example output:
ListenerSet/$LS_NAME/plain EnterpriseListenerSet/$ELS_NAME/plain
Switch to the EnterpriseListenerSet
Remove the ListenerSet. Removing it releases its claim and promotes the staged EnterpriseListenerSet in the same reconcile, with no listener gap. The Gateway service port stays open because both kinds can hold the same port simultaneously.
To move all listeners at once, delete the ListenerSet.
kubectl -n $LS_NS delete listenerset $LS_NAMETo move individual listeners, list the listener names first to find the index, then use a JSON patch with a
testassertion. Thetestoperation makes the patch atomic: if the named index does not contain the expected listener, the API server rejects the entire patch and nothing changes.kubectl -n $LS_NS get listenerset $LS_NAME \ -o jsonpath='{range .spec.listeners[*]}{.name} {end}'Replace
0with the index of the listener you are removing.kubectl -n $LS_NS patch listenerset $LS_NAME --type=json \ -p='[{"op":"test","path":"/spec/listeners/0/name","value":"plain"},{"op":"remove","path":"/spec/listeners/0"}]'Note
The CRD requires at least one listener. On the last listener, the patch is rejected; delete the object instead by running
kubectl -n $LS_NS delete listenerset $LS_NAME.
Verify that the EnterpriseListenerSet is serving all listeners in the control plane.
kubectl -n $LS_NS get enterpriselistenerset $ELS_NAME \ -o jsonpath='{range .status.listeners[*]}{.name}: attachedRoutes={.attachedRoutes}{"\n"}{range .conditions[*]} {.type}={.status} reason={.reason}{"\n"}{end}{end}'Example output:
plain: attachedRoutes=1 Accepted=True reason=Accepted Conflicted=False reason=NoConflicts Programmed=True reason=Programmed ResolvedRefs=True reason=ResolvedRefs secure: attachedRoutes=1 Accepted=True reason=Accepted Conflicted=False reason=NoConflicts Programmed=True reason=Programmed ResolvedRefs=True reason=ResolvedRefsVerify that the certificate reached the proxy on every Gateway pod.
for pod in $(kubectl -n $GATEWAY_NS get pods \ -l gateway.networking.k8s.io/gateway-name=$GATEWAY_NAME -o name); do echo "== $pod" istioctl -n $GATEWAY_NS proxy-config route "${pod#pod/}" | grep $LISTENER_PORT istioctl -n $GATEWAY_NS proxy-config secret "${pod#pod/}" | grep $CERT_NAME doneThe certificate row must show
VALID CERT trueon every pod.Warning
If the route status still names the ListenerSet as the parent after 30 seconds,
VALID CERTisfalseon any pod, orattachedRoutesis0on a listener that should have routes, recover immediately. Re-create the ListenerSet withkubectl create -f ls.bak.json. The ListenerSet becomes the winner again on arrival and traffic returns without waiting for a diagnosis.
Clean up
Remove the temporary route parentRefs. Do not modify the ReferenceGrant: the EnterpriseListenerSet resolves its certificate references through the kind: ListenerSet grant, so removing that entry revokes the certificate on the live object.
List the
parentRefkinds on each route to find the index of the ListenerSet entry.kubectl -n $ROUTE_NS get httproute $ROUTE_NAME \ -o jsonpath='{range .spec.parentRefs[*]}{.kind}{"\n"}{end}'Remove the ListenerSet
parentReffrom the route, leaving the EnterpriseListenerSetparentRef. Replace0in the path with the index of the ListenerSet entry from step 1.kubectl -n $ROUTE_NS patch httproute $ROUTE_NAME --type=json \ -p='[{"op":"test","path":"/spec/parentRefs/0/kind","value":"ListenerSet"},{"op":"remove","path":"/spec/parentRefs/0"}]'Verify that the service ports match the baseline saved before you began.
diff service-ports.bak.txt \ <(kubectl -n $GATEWAY_NS get svc $GATEWAY_NAME-istio \ -o jsonpath='{range .spec.ports[*]}{.name}={.port}{"\n"}{end}')
Warning
To roll back after this step, re-add the kind: ListenerSet parentRef to every route before re-creating the ListenerSet. A ListenerSet created while no route names it wins the port immediately and serves 404s until the parentRefs are in place.