Skip to content

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

Enterprise
Page as Markdown

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 ListenerSet always takes precedence over an EnterpriseListenerSet on 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 EnterpriseListenerSet continue to serve.
  • When a ListenerSet and an EnterpriseListenerSet share the same namespace and name, the EnterpriseListenerSet loses all its listeners, not only the conflicting ones.
  • Routes do not follow a listener when it moves between objects. A parentRef names a kind, so a route bound to kind: EnterpriseListenerSet does not attach to a ListenerSet listener automatically.
  • Cross-namespace certificate references from an EnterpriseListenerSet must be authorized by a ReferenceGrant that names kind ListenerSet in group gateway.networking.k8s.io. A grant naming kind EnterpriseListenerSet is 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 variableDefaultUse
ISTIO_KUBE_HEADER_TIMEOUT90sMaximum 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_TIMEOUT120sTotal time for a non-streaming Kubernetes API request to finish. Keep this value greater than ISTIO_KUBE_HEADER_TIMEOUT.
ISTIO_KUBE_STREAM_TIMEOUT15mTotal time for a watch, log follow, or other streaming request. Keep this value greater than the API server maximum watch duration.
ISTIO_INFORMER_REQUEST_TIMEOUT15mTotal 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 versionExisting listener setSection
1.4 or earlierNoneSet up an EnterpriseListenerSet
Moving from 1.4 or earlier to 1.5 or laterEnterpriseListenerSetMigrate from EnterpriseListenerSet to ListenerSet
Moving from 1.5 or later to 1.4 or earlierListenerSetMigrate from ListenerSet to EnterpriseListenerSet

Set up an EnterpriseListenerSet

Before you begin

  1. Install Solo Enterprise for Istio 1.30.3-patch0 or later. Earlier 1.30.x patch releases do not include EnterpriseListenerSet support.

  2. 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>
  3. Configure a Gateway or confirm that an existing Gateway has spec.allowedListeners set. Without it, the Gateway admits no listener sets, and the EnterpriseListenerSet reports Accepted=False with reason NotAllowed.

    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
  4. Confirm 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 of PILOT_GATEWAY_API_DEFAULT_GATEWAYCLASS_NAME if overridden. The istio-agentgateway class does not support EnterpriseListenerSet.

Configure the EnterpriseListenerSet

  1. 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
  2. Verify that the CRD installed successfully.

    kubectl get crd enterpriselistenersets.enterprise.solo.io

    Example output:

    NAME                                          CREATED AT
    enterpriselistenersets.enterprise.solo.io     2026-08-14T00:00:00Z
  3. If 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 ListenerSet in group gateway.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 for caCertificateRefs) lives. Its from.namespace names the namespace of the listener set, not the Gateway.

    Important

    A missing grant causes the listener to report ResolvedRefs=False with reason RefNotPermitted. TLS connections fail with connection refused even 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
    EOF
  4. Create an EnterpriseListenerSet in the namespace where your listeners exist. The parentRef must 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 spec mirrors the Kubernetes Gateway API ListenerSet spec. Fields such as hostname, port, protocol, tls, and allowedRoutes have 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
    EOF
  5. Verify 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: True
  6. Verify 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

  1. 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>
  2. Confirm that the ListenerSet CRD is installed.

    kubectl get crd listenersets.gateway.networking.k8s.io

    If 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 EnterpriseListenerSet resource until the cluster uses the Gateway API 1.5 or later.

  3. Confirm that the Gateway has spec.allowedListeners set. Without it, the Gateway admits no listener sets, and the ListenerSet reports Accepted=False with reason NotAllowed.

    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
  4. Confirm 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 of PILOT_GATEWAY_API_DEFAULT_GATEWAYCLASS_NAME if overridden. The istio-agentgateway class does not support ListenerSet.

  5. Save rollback backups. If the migration fails, use these files to restore the original state.

    1. 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.json
    2. Save all HTTPRoutes that reference the EnterpriseListenerSet.

      kubectl get httproute -A -o json \
        | jq '[.items[] | select(.spec.parentRefs[]?.kind == "EnterpriseListenerSet")]' > routes.bak.json
    3. Save all ReferenceGrants.

      kubectl get referencegrant -A -o yaml > grants.bak.yaml
  6. Save baselines to verify against after the migration.

    1. 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.txt
    2. Verify 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 ClusterIP as ADDR and run curl from 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.

  1. 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>
  2. 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.

    1. 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"}}]'
    2. 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 receive connection refused. Also check whether the parent Gateway validates client certificates with caCertificateRefs. Those references are re-evaluated against the listener set’s kind and namespace. Add the ListenerSet kind to any grant covering those references, and add a to entry with kind: ConfigMap if the reference points at a configmap rather than a secret.

  3. Add the ListenerSet as an additional parentRef to every route that currently names this listener on the EnterpriseListenerSet. Keep the existing EnterpriseListenerSet parentRef in place. Append with a JSON patch to avoid losing any existing parentRefs 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"}}]'
  4. 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 sectionName must match the listener name you are about to create on the ListenerSet.

    EnterpriseListenerSet/$ELS_NAME/plain ListenerSet/$LS_NAME/plain
  5. Create 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"}}}}]'
  6. 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=HostnameConflict
  7. Verify 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=1

  8. Verify 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
    done

    The route owner field (the ~-separated identifier) must name the ListenerSet. The certificate must show VALID CERT true on every pod.

  9. 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 Conflicted condition clears. Nothing else requires undoing, because the dual parentRef from step 2 is harmless on its own.

  10. 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}'
  11. Remove the migrated listener. Replace 0 in the path with the index from step 10. Guard the removal with a test assertion 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.

  1. 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}'
  2. Remove the EnterpriseListenerSet parentRef from the route, leaving the ListenerSet parentRef. Replace 0 in 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"}]'
  3. List the from entries 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}'
  4. Remove the EnterpriseListenerSet from entry from the ReferenceGrant. Replace 0 in 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"}]'
  5. 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}')
  6. If you used a curltest pod 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

  1. 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>
  2. 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 version
  3. 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
  4. Verify that the CRD installed successfully.

    kubectl get crd enterpriselistenersets.enterprise.solo.io

    Example output:

    NAME                                          CREATED AT
    enterpriselistenersets.enterprise.solo.io     2026-08-14T00:00:00Z
  5. Confirm 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 of PILOT_GATEWAY_API_DEFAULT_GATEWAYCLASS_NAME if overridden. The istio-agentgateway class programs a real ListenerSet but does not support EnterpriseListenerSet; this migration cannot be performed on an agentgateway Gateway.

  6. Confirm that the Gateway has spec.allowedListeners set. Without it, the Gateway admits no listener sets, and the EnterpriseListenerSet reports Accepted=False with reason NotAllowed.

    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

  1. Save rollback backups. If the migration fails, use these files to restore the original state.

    1. 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.json

      Important

      The jq filter removes server-side fields that the API server rejects on re-create. The filtered ls.bak.json file is the rollback for the cutover; a plain kubectl get -o yaml backup cannot be used to restore. The Gateway service is named $GATEWAY_NAME-<gateway-class> unless the Gateway has a gateway.istio.io/name-override annotation.

    2. Save all HTTPRoutes that reference the ListenerSet.

      kubectl get httproute -A -o json \
        | jq '[.items[] | select(.spec.parentRefs[]?.kind == "ListenerSet")]' > routes.bak.json
    3. Save all ReferenceGrants.

      kubectl get referencegrant -A -o yaml > grants.bak.yaml
    4. Save 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
  2. If any listener references a certificate secret in a different namespace, confirm that the existing ReferenceGrant uses kind: ListenerSet in group gateway.networking.k8s.io. The EnterpriseListenerSet controller resolves certificate references against a kind: ListenerSet grant, because it maps the EnterpriseListenerSet to ListenerSet internally before the grant lookup. The existing kind: ListenerSet grant covers the EnterpriseListenerSet. 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 receive connection refused. If the parent Gateway validates client certificates with caCertificateRefs, those references are also resolved against kind: ListenerSet grants. Confirm that any grant covering those references also uses kind: ListenerSet in group gateway.networking.k8s.io. If the reference points to a configmap, confirm the grant has a to entry with kind: ConfigMap.

  3. Create the EnterpriseListenerSet with every listener that you want to migrate. Copy each listener’s port, protocol, hostname, allowedRoutes, and tls exactly, and copy the ListenerSet’s labels to the new object. The parentRef must 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 allowedRoutes omitted silently defaults to namespaces.from: Same and 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
    EOF
  4. Verify 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=HostnameConflict
  5. Confirm 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 parentRef added in the next section serves traffic while the staged listeners remain conflicted.

Re-point routes

  1. Add the EnterpriseListenerSet as an additional parentRef to every route that names the ListenerSet. Keep the existing ListenerSet parentRef in 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: plain
  2. Verify the route spec rather than its status. While the staged listener is conflicted, no route can attach to it. attachedRoutes on the EnterpriseListenerSet reads 0 and the route’s status carries no entry for the staged parent. Verify that each sectionName matches 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

  1. 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_NAME
    • To move individual listeners, list the listener names first to find the index, then use a JSON patch with a test assertion. The test operation 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 0 with 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.

  2. 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=ResolvedRefs
  3. Verify 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
    done

    The certificate row must show VALID CERT true on every pod.

    Warning

    If the route status still names the ListenerSet as the parent after 30 seconds, VALID CERT is false on any pod, or attachedRoutes is 0 on a listener that should have routes, recover immediately. Re-create the ListenerSet with kubectl 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.

  1. List the parentRef kinds 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}'
  2. Remove the ListenerSet parentRef from the route, leaving the EnterpriseListenerSet parentRef. Replace 0 in 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"}]'
  3. 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.