Skip to content
You are viewing the documentation for Solo Enterprise for Istio, formerly known as Gloo Mesh (OSS APIs).

For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.

Helm

Page as Markdown

Use Helm to link ambient service meshes across multiple clusters.

Overview

In this guide, you deploy an ambient mesh to each workload cluster, create an east-west gateway in each cluster, and link the istiod control planes across cluster networks by using peering gateways. In the next guide, you can deploy the Bookinfo sample app to the ambient mesh in each cluster, and make select services available across the multicluster mesh. Incoming requests can then be routed from an ingress gateway, such as Solo Enterprise for kgateway, to services in your mesh across all clusters.

The following diagram demonstrates an ambient mesh setup across multiple clusters. For more information about the components that are installed in these steps, see the ambient components overview.

Figure: Multicluster ambient mesh set up with the Solo distribution of Istio and Solo Enterprise for kgateway.
Figure: Multicluster ambient mesh set up with the Solo distribution of Istio and Solo Enterprise for kgateway.

Considerations

Before you set up a multicluster ambient mesh, review the following considerations and requirements.

License requirements

Note

Multicluster capabilities require an Enterprise level license for Solo Enterprise for Istio. If you do not have one, contact an account representative.

Version requirements

Review the following known Istio version requirements and restrictions.

  • Patch versions 1.26.0 and 1.26.1 of the Solo distribution of Istio lack support for FIPS-tagged images and ztunnel outlier detection. When upgrading or installing 1.26, be sure to use patch version 1.26.1-patch0 and later only.
  • In the Solo distribution of Istio 1.25 and later, you can access enterprise-level features by passing your Solo license in the license.value or license.secretRef field of the Solo distribution of the istiod Helm chart. The Solo istiod Helm chart is strongly recommended due to the included safeguards, default settings, and upgrade handling to ensure a reliable and secure Istio deployment. Though it is not recommended, you can pass your license key in the open source istiod Helm chart by using the --set pilot.env.SOLO_ISTIO_LICENSE_KEY field.
  • Ambient mode requires the Solo distribution of Istio version 1.22.3 or later (1.22.3-solo). Multicluster setups require the Solo distribution of Istio version 1.24.3 or later (1.24.3-solo), including the Solo distribution of istioctl.
  • In Istio 1.22.0-1.22.3, the ISTIO_DELTA_XDS environment variable must be set to false. For more information, see this upstream Istio issue. Note that this issue is resolved in Istio 1.22.4.
  • Due to a lack of support for the Istio CNI and iptables for the Istio proxy, you cannot run Istio (and therefore Solo Enterprise for Istio) on AWS Fargate. For more information, see the Amazon EKS issue.

Platform requirements

The steps in the following sections have options for deploying an ambient mesh to either Kubernetes or OpenShift clusters.

If you use OpenShift clusters, complete the following steps before you begin:

The commands for OpenShift in the following steps contain these required settings:

  • Your Helm settings must include global.platform=openshift for Istio 1.24 and later. If you instead install Istio 1.23 or earlier, you must use profile=openshift instead of the global.platform setting.

Revision and canary upgrade limitations

The upgrade guides in this documentation show you how to perform in-place upgrades for your Istio components, which is the recommended upgrade strategy.

The steps in the following guide include options for either the LoadBalancer or NodePort method. A status condition on each east-west and remote peer gateway indicates which dataplane service type is in use.

IPv6 on single-stack clusters

Multicluster services are exposed on *.mesh.internal hostnames with auto-allocated virtual IPs (PILOT_ENABLE_IP_AUTOALLOCATE). Istio assigns both an IPv4 and an IPv6 VIP to each of these services. On a single-stack IPv4 cluster, pods have no usable IPv6 address, so a client that prefers IPv6 tries the IPv6 VIP first, fails, and falls back to the IPv4 VIP. You might see warnings like the following in client logs, such as the Solo UI telemetry relay or collectors:

dial tcp [2001:2::2]:4316: connect: network is unreachable

Note

This is expected and not a failure on its own. The connection falls back to IPv4 and traffic flows normally. Some clients, such as gRPC-based ones, log the IPv6 attempt repeatedly while still sending data over IPv4. To suppress the warnings on a cluster with no usable IPv6, set the IPV6_ENABLED environment variable to false on ztunnel (the default is true); ztunnel then returns only the IPv4 VIP.

Migrating from multicluster community Istio

If you previously used the multicluster feature in community Istio, and want to now migrate to multicluster peering in the Solo distribution of Istio, the DISABLE_LEGACY_MULTICLUSTER environment variable is introduced in the Solo distribution of Istio version 1.28 to disable the community multicluster mechanisms. Multicluster in community Istio uses remote secrets that contain kubeconfigs to watch resources on remote clusters. This system is incompatible with the decentralized, push-based model for peering in the Solo distribution of Istio. This variable causes istiod to ignore remote secrets so that it does not attempt to set up Kubernetes clients to connect to them.

  • For fresh multicluster mesh installations with the Solo distribution of Istio, use this environment variable in your istiod settings. This setting serves as a recommended safety measure to prevent any use of remote secrets.
  • If you want to initiate a multicluster migration from community Istio, contact a Solo account representative. An account representative can help you set up two revisions of Istio that each select a different set of namespaces, and set the DISABLE_LEGACY_MULTICLUSTER variable on the revision that uses the Solo distribution of Istio for multicluster peering.

Multicluster setup method

To get started, choose one of following methods for creating a multicluster mesh.

Option 1: Install and link new ambient meshes

In each cluster, use Helm to create the ambient mesh components. Then, create an east-west gateway so that traffic requests can be routed cross-cluster, and link clusters to enable cross-cluster service discovery.

Set up tools

  1. Set your Enterprise level license for Solo Enterprise for Istio as an environment variable. If you do not have one, contact an account representative. If you prefer to specify license keys in a secret instead, see Licensing. Note that you might have previously saved this key in another variable, such as ${SOLO_LICENSE_KEY} or ${GLOO_MESH_LICENSE_KEY}.

    export SOLO_ISTIO_LICENSE_KEY=<enterprise_license_key>
  2. Choose the version of Istio that you want to install or upgrade to by reviewing the supported versions.

  3. Save the Solo distribution of Istio version.

    export ISTIO_VERSION=1.26.8-patch7
    export ISTIO_IMAGE=${ISTIO_VERSION}-solo
  4. Save the repo key for the minor version of the Solo distribution of Istio that you want to install. This is the 12-character hash at the end of the repo URL us-docker.pkg.dev/gloo-mesh/istio-<repo-key>, which you can find in the Istio images built by Solo.io support article.

    # 12-character hash at the end of the repo URL
    export REPO_KEY=<repo_key>
    export REPO=us-docker.pkg.dev/gloo-mesh/istio-${REPO_KEY}
    export HELM_REPO=us-docker.pkg.dev/gloo-mesh/istio-helm-${REPO_KEY}
  5. Get the Solo distribution of Istio binary and install istioctl, which you use for multicluster linking and gateway commands. This script automatically detects your OS and architecture, downloads the appropriate Solo distribution of Istio binary, and verifies the installation.

    bash <(curl -sSfL https://raw.githubusercontent.com/solo-io/doc-examples/main/istio/install-istioctl.sh)
    export PATH=${HOME}/.istioctl/bin:${PATH}
  6. Save the names and kubeconfig contexts of each cluster. This guide uses two clusters as an example. To add more clusters to the multicluster setup, include them in the arrays.

    export cluster1=<cluster1_name>
    export context1=<cluster1_context>
    export cluster2=<cluster2_name>
    export context2=<cluster2_context>

Create a shared root of trust

Each cluster in the multicluster setup must have a shared root of trust. This can be achieved by providing a root certificate signed by a PKI provider, or a custom root certificate created for this purpose. The root certificate signs a unique intermediate CA certificate for each cluster.

By default, the Istio CA generates a self-signed root certificate and key, and uses them to sign the workload certificates. For more information, see the Plug in CA Certificates guide in the community Istio documentation.

For demo installations, you can run the following function to quickly generate and plug in the certificates and key for the Istio CA. Change the Istio version as needed.

curl -L https://istio.io/downloadIstio | ISTIO_VERSION=${ISTIO_VERSION} sh -
cd istio-${ISTIO_VERSION}

mkdir -p certs
pushd certs
make -f ../tools/certs/Makefile.selfsigned.mk root-ca

function create_cacerts_secret() {
  context=${1:?context}
  cluster=${2:?cluster}
  make -f ../tools/certs/Makefile.selfsigned.mk ${cluster}-cacerts
  kubectl --context=${context} create ns istio-system || true
  kubectl --context=${context} create secret generic cacerts -n istio-system \
    --from-file=${cluster}/ca-cert.pem \
    --from-file=${cluster}/ca-key.pem \
    --from-file=${cluster}/root-cert.pem \
    --from-file=${cluster}/cert-chain.pem
}

create_cacerts_secret ${context1} ${cluster1}
create_cacerts_secret ${context2} ${cluster2}

cd ../..

To enhance the security of your setup even further and have full control over the Istio CA lifecycle, you can generate and store the root and intermediate CA certificates and keys with your own PKI provider. You can then use tools such as cert-manager to send certificate signing requests on behalf of istiod to your PKI provider. Cert-manager stores the signed intermediate certificates and keys in the cacerts Kubernetes secret so that istiod can use these credentials to issue leaf certificates for the workloads in the service mesh. You can set up cert-manager to also check the certificates and renew them before they expire.

AWS Private CA issuer and cert-manager: For steps on how to deploy this certificate setup, check out this Solo.io blog post. Be sure to repeat the steps so that a cacerts secret exists in each cluster.

Deploy ambient components

  1. Apply the CRDs for the Kubernetes Gateway API to your cluster, which are required to create components such as waypoint proxies for L7 traffic policies, gateways with the Gateway resource, and more.

    kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml --context ${context1}
    kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml --context ${context2}
  2. Install the base chart, which contains the CRDs and cluster roles required to set up Istio, in both clusters.

    function install_base() {
      context=${1:?context}
      helm upgrade --install istio-base oci://${HELM_REPO}/base \
      --namespace istio-system \
      --create-namespace \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f - <<EOF
    defaultRevision: ""
    profile: ambient
    EOF
    }
    
    install_base ${context1}
    install_base ${context2}
    function install_base() {
      context=${1:?context}
      helm upgrade --install istio-base oci://${HELM_REPO}/base \
      --namespace istio-system \
      --create-namespace \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f - <<EOF
    defaultRevision: ""
    profile: ambient
    global:
      platform: openshift
    EOF
    }
    
    install_base ${context1}
    install_base ${context2}

    You can optionally verify that the CRDs are successfully installed in both clusters.

    kubectl get crds -l app.kubernetes.io/instance=istio-base --kube-context ${context1}
    kubectl get crds -l app.kubernetes.io/instance=istio-base --kube-context ${context2}

    Example output:

    NAME                                       CREATED AT
    authorizationpolicies.security.istio.io    2025-12-16T22:56:00Z
    destinationrules.networking.istio.io       2025-12-16T22:56:00Z
    envoyfilters.networking.istio.io           2025-12-16T22:56:00Z
    gateways.networking.istio.io               2025-12-16T22:56:00Z
    peerauthentications.security.istio.io      2025-12-16T22:56:00Z
    proxyconfigs.networking.istio.io           2025-12-16T22:56:00Z
    requestauthentications.security.istio.io   2025-12-16T22:56:00Z
    segments.admin.solo.io                     2025-12-16T22:56:00Z
    serviceentries.networking.istio.io         2025-12-16T22:56:00Z
    sidecars.networking.istio.io               2025-12-16T22:56:00Z
    telemetries.telemetry.istio.io             2025-12-16T22:56:00Z
    virtualservices.networking.istio.io        2025-12-16T22:56:00Z
    wasmplugins.extensions.istio.io            2025-12-16T22:56:00Z
    workloadentries.networking.istio.io        2025-12-16T22:56:00Z
    workloadgroups.networking.istio.io         2025-12-16T22:56:00Z
  3. Label the istio-system namespace with the clusters’ network names, which you previously set to each cluster name in the global.network field of the istiod installations. The ambient control plane uses this label internally to group pods that exist in the same L3 network.

    Note

    Apply the topology.istio.io/network label before you install istiod in the next step. If you apply the label after istiod starts, the peering controller logs topology.istio.io/network is not set and enters exponential backoff. Recovery requires restarting istiod after applying the label.

    kubectl label namespace istio-system --context ${context1} topology.istio.io/network=${cluster1}
    kubectl label namespace istio-system --context ${context2} topology.istio.io/network=${cluster2}
  4. Create the istiod control plane in both clusters.

    function install_istiod() {
      context=${1:?context}
      cluster=${2:?cluster}
      helm upgrade --install istiod oci://${HELM_REPO}/istiod \
      --namespace istio-system \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f - <<EOF
    env:
      # Assigns IP addresses to multicluster services
      PILOT_ENABLE_IP_AUTOALLOCATE: "true"
      # Required when meshConfig.trustDomain is set
      PILOT_SKIP_VALIDATE_TRUST_DOMAIN: "true"
    global:
      hub: ${REPO}
      multiCluster:
        clusterName: ${cluster}
      network: ${cluster}
      proxy:
        clusterDomain: cluster.local
      tag: ${ISTIO_IMAGE}
    meshConfig:
      accessLogFile: /dev/stdout
      defaultConfig:
        proxyMetadata:
          ISTIO_META_DNS_CAPTURE: "true"
      # Assign each cluster a unique trust domain to apply policies to specific clusters
      trustDomain: "${cluster}.local"
    pilot:
      cni:
        namespace: istio-system
        enabled: true
    # Required to enable multicluster support
    platforms:
      peering:
        enabled: true
    profile: ambient
    license:
      value: ${SOLO_ISTIO_LICENSE_KEY}
      # Uncomment if you prefer to specify your license secret instead of an inline value.
      # The secret data key must be one of: license-key, gloo-mesh-license-key, or gloo-trial-license-key
      # secretRef:
      #   name: <secret-name>
      #   namespace: <secret-namespace>
    EOF
    }
    
    install_istiod ${context1} ${cluster1}
    install_istiod ${context2} ${cluster2}
    function install_istiod() {
      context=${1:?context}
      cluster=${2:?cluster}
      helm upgrade --install istiod oci://${HELM_REPO}/istiod \
      --namespace istio-system \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f - <<EOF
    env:
      # Assigns IP addresses to multicluster services
      PILOT_ENABLE_IP_AUTOALLOCATE: "true"
      # Required when meshConfig.trustDomain is set
      PILOT_SKIP_VALIDATE_TRUST_DOMAIN: "true"
    global:
      hub: ${REPO}
      multiCluster:
        clusterName: ${cluster}
      network: ${cluster}
      platform: openshift
      proxy:
        clusterDomain: cluster.local
      tag: ${ISTIO_IMAGE}
    meshConfig:
      accessLogFile: /dev/stdout
      defaultConfig:
        proxyMetadata:
          ISTIO_META_DNS_CAPTURE: "true"
      # Assign each cluster a unique trust domain to apply policies to specific clusters
      trustDomain: "${cluster}.local"
    pilot:
      cni:
        namespace: istio-system
        enabled: true
    # Required to enable multicluster support
    platforms:
      peering:
        enabled: true
    profile: ambient
    license:
      value: ${SOLO_ISTIO_LICENSE_KEY}
      # Uncomment if you prefer to specify your license secret instead of an inline value.
      # The secret data key must be one of: license-key, gloo-mesh-license-key, or gloo-trial-license-key
      # secretRef:
      #   name: <secret-name>
      #   namespace: <secret-namespace>
    EOF
    }
    
    install_istiod ${context1} ${cluster1}
    install_istiod ${context2} ${cluster2}

  5. Install the Istio CNI node agent daemonset in both clusters.

    function install_cni() {
      context=${1:?context}
      helm upgrade --install istio-cni oci://${HELM_REPO}/cni \
      --namespace istio-system \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f - <<EOF
    # Assigns IP addresses to multicluster services
    ambient:
      dnsCapture: true
    excludeNamespaces:
      - istio-system
      - kube-system
    global:
      hub: ${REPO}
      tag: ${ISTIO_IMAGE}
    profile: ambient
    EOF
    }
    
    install_cni ${context1}
    install_cni ${context2}
    function install_cni() {
      context=${1:?context}
      helm upgrade --install istio-cni oci://${HELM_REPO}/cni \
      --namespace istio-system \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f - <<EOF
    # Assigns IP addresses to multicluster services
    ambient:
      dnsCapture: true
    excludeNamespaces:
      - istio-system
      - kube-system
    global:
      hub: ${REPO}
      platform: openshift
      tag: ${ISTIO_IMAGE}
    profile: ambient
    EOF
    }
    
    install_cni ${context1}
    install_cni ${context2}

  6. Install the ztunnel daemonset in both clusters.

    function install_ztunnel() {
      context=${1:?context}
      cluster=${2:?cluster}
      helm upgrade --install ztunnel oci://${HELM_REPO}/ztunnel \
      --namespace istio-system \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f - <<EOF
    configValidation: true
    enabled: true
    env:
      L7_ENABLED: "true"
      # Required when a unique trust domain is set for each cluster
      SKIP_VALIDATE_TRUST_DOMAIN: "true"
    hub: ${REPO}
    istioNamespace: istio-system
    multiCluster:
      clusterName: ${cluster}
    namespace: istio-system
    network: ${cluster}
    profile: ambient
    proxy:
      clusterDomain: cluster.local
    tag: ${ISTIO_IMAGE}
    terminationGracePeriodSeconds: 29
    variant: distroless
    EOF
    }
    
    install_ztunnel ${context1} ${cluster1}
    install_ztunnel ${context2} ${cluster2}
    function install_ztunnel() {
      context=${1:?context}
      cluster=${2:?cluster}
      helm upgrade --install ztunnel oci://${HELM_REPO}/ztunnel \
      --namespace istio-system \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f - <<EOF
    configValidation: true
    enabled: true
    env:
      L7_ENABLED: "true"
      # Required when a unique trust domain is set for each cluster
      SKIP_VALIDATE_TRUST_DOMAIN: "true"
    global:
      platform: openshift
    hub: ${REPO}
    istioNamespace: istio-system
    multiCluster:
      clusterName: ${cluster}
    namespace: istio-system
    network: ${cluster}
    profile: ambient
    proxy:
      clusterDomain: cluster.local
    tag: ${ISTIO_IMAGE}
    terminationGracePeriodSeconds: 29
    variant: distroless
    EOF
    }
    
    install_ztunnel ${context1} ${cluster1}
    install_ztunnel ${context2} ${cluster2}

  7. Verify that the components of the Istio ambient control and data plane are successfully installed in both clusters. Because the Istio CNI and ztunnel are deployed as daemon sets, the number of CNI and ztunnel pods equals the number of nodes in your cluster. Note that it might take a few seconds for the pods to become available.

    kubectl get pods -A --context ${context1} | grep -E 'istio|ztunnel'
    kubectl get pods -A --context ${context2} | grep -E 'istio|ztunnel'

    Example output:

    istiod-85c4dfd97f-mncj5      1/1     Running   0          40s
    istio-cni-node-pr5rl         1/1     Running   0          9s
    istio-cni-node-pvmx2         1/1     Running   0          9s
    istio-cni-node-6q26l         1/1     Running   0          9s
    ztunnel-tvtzn                1/1     Running   0          7s
    ztunnel-vtpjm                1/1     Running   0          4s
    ztunnel-hllxg                1/1     Running   0          4s
  8. Optional: Check the istiod logs in both clusters to verify that the certificate you generated earlier is picked up by istiod.

    kubectl logs deploy/istiod -n istio-system --context ${context1} | grep x509
    kubectl logs deploy/istiod -n istio-system --context ${context2} | grep x509

    Example output:

    2025-12-16T22:59:06.783901Z     info    x509 cert - Issuer: "CN=Intermediate CA,O=Istio,L=cluster-1", Subject: "", SN: def320623729b8370172413749143836, NotBefore: "2025-12-16T22:57:06Z", NotAfter: "2035-12-14T22:59:06Z"
    2025-12-16T22:59:06.783937Z     info    x509 cert - Issuer: "CN=Root CA,O=Istio", Subject: "CN=Intermediate CA,O=Istio,L=cluster-1", SN: 452d45254328667ccf8434c64c79fe789612bb5a, NotBefore: "2025-12-16T22:54:30Z", NotAfter: "2035-12-14T22:54:30Z"
    2025-12-16T22:59:06.783966Z     info    x509 cert - Issuer: "CN=Root CA,O=Istio", Subject: "CN=Root CA,O=Istio", SN: 38dad68e56ab8c506ae07454801f65b134bd9580, NotBefore: "2025-12-16T22:54:30Z", NotAfter: "2035-12-14T22:54:30Z"
  9. Create an east-west gateway in the istio-eastwest namespace. In each cluster, the east-west gateway is implemented as a ztunnel that facilitates traffic between services across clusters in your multicluster mesh. Create an east-west gateway in both clusters. In this example Gateway resource, the gatewayClassName that is used, istio-eastwest, is included by default when you install Istio in ambient mode. For customization options, see the gateway guide in the Istio docs.

    apiVersion: gateway.networking.k8s.io/v1
    kind: Gateway
    metadata:
      labels:
        istio.io/expose-istiod: "15012"
        topology.istio.io/network: "cluster1"
      name: istio-eastwest
      namespace: istio-eastwest
    spec:
      gatewayClassName: istio-eastwest
      listeners:
      - name: cross-network
        port: 15008
        protocol: HBONE
        tls:
          mode: Passthrough
      - name: xds-tls
        port: 15012
        protocol: TLS
        tls:
          mode: Passthrough
  10. Verify that the east-west gateway is successfully deployed in both clusters.

    kubectl get svc -n istio-eastwest --context ${context1}
    kubectl get svc -n istio-eastwest --context ${context2}

    Example output:

    NAME             TYPE           CLUSTER-IP       EXTERNAL-IP             PORT(S)                                           AGE
    istio-eastwest   LoadBalancer   172.20.205.104   <external_address>      15021:31655/TCP,15008:32699/TCP,15012:32166/TCP   55s
    NAME             TYPE           CLUSTER-IP       EXTERNAL-IP             PORT(S)                                           AGE
    istio-eastwest   LoadBalancer   172.20.21.117    <external_address>      15021:30324/TCP,15008:31875/TCP,15012:31050/TCP   77s

Link clusters

Link clusters to enable cross-cluster service discovery and allow traffic to be routed through east-west gateways across clusters.

  1. Link clusters to enable cross-cluster service discovery and allow traffic to be routed through east-west gateways across clusters. Note that you can either link the clusters bi-directionally or asymmetrically. In a standard bi-directional setup, services in any of the linked clusters can send requests to and receive requests from the services in any of the other linked clusters. In an asymmetrical setup, you allow one cluster to send requests to another cluster, but the other cluster cannot send requests back to the first cluster.

    In the Solo distribution of Istio 1.29 or later, you can use the peering Helm chart to link your clusters.

    1. Get the addresses of the east-west gateway in each cluster. The following commands show examples for two clusters.

      export CLUSTER1_EW_ADDRESS=$(kubectl get svc -n istio-eastwest istio-eastwest --context ${context1} -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
      export CLUSTER2_EW_ADDRESS=$(kubectl get svc -n istio-eastwest istio-eastwest --context ${context2} -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
      echo "Cluster1 east-west gateway: $CLUSTER1_EW_ADDRESS"
      echo "Cluster2 east-west gateway: $CLUSTER2_EW_ADDRESS"
    2. Link the clusters by creating Helm releases in each cluster to represent the other clusters. In each cluster where you create Helm releases, a Gateway resource is created that uses the istio-remote GatewayClass. This class allows the gateway to connect to other clusters by using the addresses of the east-west gateways.

      • Be sure to update the region, and optionally zone, of each cluster.
      • If you want to use NodePorts instead of the gateway LoadBalancer IP address for cross-cluster traffic, change the preferredDataplaneServiceType to nodeport. This automatically configures the peering gateways for NodePort-based cross-cluster traffic.
      • The following example depicts a bi-directional setup. In an asymmetrical setup, you create Helm releases to only represent the directionality you want to allow.
      helm upgrade -i peering-remote oci://${HELM_REPO}/peering \
        --version ${ISTIO_IMAGE} \
        --namespace istio-eastwest \
        --kube-context ${context1} \
        -f - <<EOF
      remote:
        create: true
        items:
          # Remote peer configuration for cluster2
          - name: istio-remote-peer-${cluster2}
            cluster: ${cluster2}
            network: ${cluster2}
            # Change to "Hostname" if using hostname
            addressType: IPAddress
            address: ${CLUSTER2_EW_ADDRESS}
            # Change to "nodeport" if using NodePorts
            preferredDataplaneServiceType: loadbalancer
            trustDomain: "${cluster2}.local"
            region: "<cluster2_region>"
            # Uncomment as needed
            # zone: "<cluster2_zone>"
      EOF
      helm upgrade -i peering-remote oci://${HELM_REPO}/peering \
        --version ${ISTIO_IMAGE} \
        --namespace istio-eastwest \
        --kube-context ${context2} \
        -f - <<EOF
      remote:
        create: true
        items:
          # Remote peer configuration for cluster1
          - name: istio-remote-peer-${cluster1}
            cluster: ${cluster1}
            network: ${cluster1}
            # Change to "Hostname" if using hostname
            addressType: IPAddress
            address: ${CLUSTER1_EW_ADDRESS}
            # Change to "nodeport" if using NodePorts
            preferredDataplaneServiceType: loadbalancer
            trustDomain: "${cluster1}.local"
            region: "<cluster1_region>"
            # Uncomment as needed
            # zone: "<cluster1_zone>"
      EOF

    Use the istioctl multicluster link command to quickly link clusters.

    1. Verify that the contexts for the clusters that you want to include in the multicluster mesh are listed in your kubeconfig file, which is required for the istioctl multicluster link command. If you do not have access to the kubeconfig files, use the declarative resources tab.

      kubectl config get-contexts
      • If you have multiple kubeconfig files, you can generate a merged kubeconfig file by running the following command.
        KUBECONFIG=<kubeconfig_file1>.yaml:<file2>.yaml kubectl config view --flatten
    2. Using the names of the cluster contexts, link the clusters so that they can communicate. To take a look at the Gateway resources that this command creates, you can include the --generate flag in the command. For more information about this command, see the CLI reference.

      • Bi-directional: You can use the following istioctl command to quickly link the clusters bi-directionally. In each cluster, Gateway resources are created that use the istio-remote GatewayClass. This class allows the gateways to connect to other clusters by using the addresses of the east-west gateways.

        istioctl multicluster link --namespace istio-eastwest --contexts="$context1,$context2"

        Example output for two clusters:

        Gateway istio-eastwest/istio-remote-peer-cluster1 applied to cluster "cluster2" pointing to cluster "cluster1" (network "cluster1")
        Gateway istio-eastwest/istio-remote-peer-cluster2 applied to cluster "cluster1" pointing to cluster "cluster2" (network "cluster2")
      • Asymmetrical: You can use the following istioctl command to quickly link the clusters asymmetrically. The services in the cluster in the --from flag can send requests to services in the cluster in the --to flag, but sending requests in the reverse direction is not permitted.

        For example, this command allows services in cluster1’s mesh to send requests to services in cluster2’s mesh through cluster2’s east-west gateway. However, the reverse is not permitted: services in cluster2’s mesh cannot send requests through cluster1’s east-west gateway to services in cluster1.

        istioctl multicluster link --namespace istio-eastwest --from ${context1} --to ${context2}

        Example output:

        Gateway istio-eastwest/istio-remote-peer-cluster2 applied to cluster "cluster1" pointing to cluster "cluster2" (network "cluster2")

    Link the clusters by declaratively creating istio-remote peer gateways.

    Bi-directional: Use the following Gateway resources to create an istio-remote peer gateway in each cluster. The istio-remote GatewayClass allows the gateways to connect to other clusters by using the addresses of the east-west gateways.

    1. Get the addresses of the east-west gateway in each cluster. The following commands show examples for two clusters.

      export CLUSTER1_EW_ADDRESS=$(kubectl get svc -n istio-eastwest istio-eastwest --context ${context1} -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
      export CLUSTER2_EW_ADDRESS=$(kubectl get svc -n istio-eastwest istio-eastwest --context ${context2} -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
      
      echo "Cluster1 east-west gateway: $CLUSTER1_EW_ADDRESS"
      echo "Cluster2 east-west gateway: $CLUSTER2_EW_ADDRESS"
    2. Using the east-west gateway addresses, create a Gateway resource in each cluster to represent the other cluster.

      kubectl apply --context ${context1} -f- <<EOF
      apiVersion: gateway.networking.k8s.io/v1
      kind: Gateway
      metadata:
        annotations:
          gateway.istio.io/service-account: istio-eastwest
          gateway.istio.io/trust-domain: ${cluster2}
        labels:
          topology.istio.io/network: ${cluster2}
        name: istio-remote-peer-${cluster2}
        namespace: istio-eastwest
      spec:
        addresses:
        # Change to 'type: Hostname' as needed, such as for AWS hostnames
        - type: IPAddress
          value: $CLUSTER2_EW_ADDRESS
        gatewayClassName: istio-remote
        listeners:
        - name: cross-network
          port: 15008
          protocol: HBONE
          tls:
            mode: Passthrough
        - name: xds-tls
          port: 15012
          protocol: TLS
          tls:
            mode: Passthrough
      EOF
      kubectl apply --context ${context2} -f- <<EOF
      apiVersion: gateway.networking.k8s.io/v1
      kind: Gateway
      metadata:
        annotations:
          gateway.istio.io/service-account: istio-eastwest
          gateway.istio.io/trust-domain: ${cluster1}
        labels:
          topology.istio.io/network: ${cluster1}
        name: istio-remote-peer-${cluster1}
        namespace: istio-eastwest
      spec:
        addresses:
        # Change to 'type: Hostname' as needed, such as for AWS hostnames
        - type: IPAddress
          value: $CLUSTER1_EW_ADDRESS
        gatewayClassName: istio-remote
        listeners:
        - name: cross-network
          port: 15008
          protocol: HBONE
          tls:
            mode: Passthrough
        - name: xds-tls
          port: 15012
          protocol: TLS
          tls:
            mode: Passthrough
      EOF

    Asymmetrical: Use the following Gateway resources to create an istio-remote peer gateway in only some clusters. The istio-remote GatewayClass allows the gateway in one cluster to connect to another cluster by using the address of the east-west gateway, but sending requests in the reverse direction is not permitted.

    1. Get the address of the east-west gateway in the cluster that you want to send traffic to. The following command shows an example for cluster2.

      export CLUSTER2_EW_ADDRESS=$(kubectl get svc -n istio-eastwest istio-eastwest --context ${context2} -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
      
      echo "Cluster2 east-west gateway: $CLUSTER2_EW_ADDRESS"
    2. Using the east-west gateway address, create a Gateway resource in the cluster that you want to send requests from. For example, this Gateway resource allows services in cluster1’s mesh to send requests to services in cluster2’s mesh through cluster2’s east-west gateway. However, the reverse is not permitted: services in cluster2’s mesh cannot send requests through cluster1’s east-west gateway to services in cluster1.

      kubectl apply --context ${context1} -f- <<EOF
      apiVersion: gateway.networking.k8s.io/v1
      kind: Gateway
      metadata:
        annotations:
          gateway.istio.io/service-account: istio-eastwest
          gateway.istio.io/trust-domain: ${cluster2}
        labels:
          topology.istio.io/network: ${cluster2}
        name: istio-remote-peer-${cluster2}
        namespace: istio-eastwest
      spec:
        addresses:
        # Change to 'type: Hostname' as needed, such as for AWS hostnames
        - type: IPAddress
          value: $CLUSTER2_EW_ADDRESS
        gatewayClassName: istio-remote
        listeners:
        - name: cross-network
          port: 15008
          protocol: HBONE
          tls:
            mode: Passthrough
        - name: xds-tls
          port: 15012
          protocol: TLS
          tls:
            mode: Passthrough
      EOF
  2. Optional: Verify that the istiod control plane for each peered cluster is included in each cluster’s proxy status list.

    istioctl proxy-status --context ${context1}
    istioctl proxy-status --context ${context2}

    Example output for cluster1, in which you can verify that the istiod control plane for cluster2 is listed:

    NAME                                               CLUSTER          ISTIOD                      VERSION              SUBSCRIBED TYPES
    istio-eastwest-67fd5679dc-fhsxs.istio-eastwest     cluster1         istiod-7b7c9cc4c6-bdm9c     1.26.8-patch7-solo-fips     2 (WADS,WDS)
    istiod-6bc6765484-5bbhd.istio-system               cluster2         istiod-7b7c9cc4c6-bdm9c     1.26.8-patch7-solo-fips     3 (FSDS,SGDS,WDS)
    ztunnel-5f8rb.kube-system                          cluster1         istiod-7b7c9cc4c6-bdm9c     1.26.8-patch7-solo-fips     2 (WADS,WDS)
    ztunnel-f96kh.kube-system                          cluster1         istiod-7b7c9cc4c6-bdm9c     1.26.8-patch7-solo-fips     2 (WADS,WDS)
    ztunnel-vtj4f.kube-system                          cluster1         istiod-7b7c9cc4c6-bdm9c     1.26.8-patch7-solo-fips     2 (WADS,WDS)

Next: Add apps to the ambient mesh. For multicluster setups, this includes making specific services available across your linked cluster setup.

Option 2: Upgrade and link existing ambient meshes

Upgrade your existing ambient meshes installed with Helm and link them to create a multicluster ambient mesh.

Set up tools

  1. Set your Enterprise level license for Solo Enterprise for Istio as an environment variable. If you do not have one, contact an account representative. If you prefer to specify license keys in a secret instead, see Licensing. Note that you might have previously saved this key in another variable, such as ${SOLO_LICENSE_KEY} or ${GLOO_MESH_LICENSE_KEY}.

    export SOLO_ISTIO_LICENSE_KEY=<enterprise_license_key>
  2. Choose the version of Istio that you want to install or upgrade to by reviewing the supported versions.

  3. Save the Solo distribution of Istio version.

    export ISTIO_VERSION=1.26.8-patch7
    export ISTIO_IMAGE=${ISTIO_VERSION}-solo
  4. Save the repo key for the minor version of the Solo distribution of Istio that you want to install. This is the 12-character hash at the end of the repo URL us-docker.pkg.dev/gloo-mesh/istio-<repo-key>, which you can find in the Istio images built by Solo.io support article.

    # 12-character hash at the end of the repo URL
    export REPO_KEY=<repo_key>
    export REPO=us-docker.pkg.dev/gloo-mesh/istio-${REPO_KEY}
    export HELM_REPO=us-docker.pkg.dev/gloo-mesh/istio-helm-${REPO_KEY}
  5. Get the Solo distribution of Istio binary and install istioctl, which you use for multicluster linking and gateway commands. This script automatically detects your OS and architecture, downloads the appropriate Solo distribution of Istio binary, and verifies the installation.

    bash <(curl -sSfL https://raw.githubusercontent.com/solo-io/doc-examples/main/istio/install-istioctl.sh)
    export PATH=${HOME}/.istioctl/bin:${PATH}
  6. Save the names and kubeconfig contexts of each cluster. This guide uses two clusters as an example. To add more clusters to the multicluster setup, include them in the arrays.

    export cluster1=<cluster1_name>
    export context1=<cluster1_context>
    export cluster2=<cluster2_name>
    export context2=<cluster2_context>

Create a shared root of trust

Each cluster in the multicluster setup must have a shared root of trust. This can be achieved by providing a root certificate signed by a PKI provider, or a custom root certificate created for this purpose. The root certificate signs a unique intermediate CA certificate for each cluster.

By default, the Istio CA generates a self-signed root certificate and key, and uses them to sign the workload certificates. For more information, see the Plug in CA Certificates guide in the community Istio documentation.

For demo installations, you can run the following function to quickly generate and plug in the certificates and key for the Istio CA. Change the Istio version as needed.

curl -L https://istio.io/downloadIstio | ISTIO_VERSION=${ISTIO_VERSION} sh -
cd istio-${ISTIO_VERSION}

mkdir -p certs
pushd certs
make -f ../tools/certs/Makefile.selfsigned.mk root-ca

function create_cacerts_secret() {
  context=${1:?context}
  cluster=${2:?cluster}
  make -f ../tools/certs/Makefile.selfsigned.mk ${cluster}-cacerts
  kubectl --context=${context} create ns istio-system || true
  kubectl --context=${context} create secret generic cacerts -n istio-system \
    --from-file=${cluster}/ca-cert.pem \
    --from-file=${cluster}/ca-key.pem \
    --from-file=${cluster}/root-cert.pem \
    --from-file=${cluster}/cert-chain.pem
}

create_cacerts_secret ${context1} ${cluster1}
create_cacerts_secret ${context2} ${cluster2}

cd ../..

To enhance the security of your setup even further and have full control over the Istio CA lifecycle, you can generate and store the root and intermediate CA certificates and keys with your own PKI provider. You can then use tools such as cert-manager to send certificate signing requests on behalf of istiod to your PKI provider. Cert-manager stores the signed intermediate certificates and keys in the cacerts Kubernetes secret so that istiod can use these credentials to issue leaf certificates for the workloads in the service mesh. You can set up cert-manager to also check the certificates and renew them before they expire.

AWS Private CA issuer and cert-manager: For steps on how to deploy this certificate setup, check out this Solo.io blog post. Be sure to repeat the steps so that a cacerts secret exists in each cluster.

Upgrade settings

In each cluster, update the ambient mesh components for multicluster, and create an east-west gateway so that traffic requests can be routed cross-cluster.

  1. Get the current values for the istiod Helm release in both clusters.

    function get_istiod_values() {
      context=${1:?context}
      cluster=${2:?cluster}
      helm get values --kube-context ${context} istiod -n istio-system -o yaml > istiod-${cluster}.yaml
    }
    
    get_istiod_values ${context1} ${cluster1}
    get_istiod_values ${context2} ${cluster2}
  2. Label the istio-system namespace with the clusters’ network names, which you previously set to each cluster name in the global.network field of the istiod installations. The ambient control plane uses this label internally to group pods that exist in the same L3 network.

    Note

    Apply the topology.istio.io/network label before you upgrade istiod in the next step. If istiod starts without the label set, the peering controller logs topology.istio.io/network is not set and enters exponential backoff. Recovery requires restarting istiod after applying the label.

    kubectl label namespace istio-system --context ${context1} topology.istio.io/network=${cluster1} --overwrite
    kubectl label namespace istio-system --context ${context2} topology.istio.io/network=${cluster2} --overwrite
  3. Update your Helm release with the following multicluster values in both clusters. If you must update the Istio minor version, include the --set global.tag=${ISTIO_IMAGE} and --set global.hub=${REPO} flags too.

    If you prefer to specify your license secret instead of an inline value, you can include --set license.secretRef.name=<name> and --set license.secretRef.namespace=<namespace>. The secret data key must be one of license-key, gloo-mesh-license-key, or gloo-trial-license-key. For an example secret manifest, see https://docs.solo.io/istio/1.26.x/setup/setup/prepare/licensing/.

    function upgrade_istiod() {
      context=${1:?context}
      cluster=${2:?cluster}
      helm upgrade istiod oci://${HELM_REPO}/istiod \
      --namespace istio-system \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f istiod-${cluster}.yaml \
      --set env.PILOT_ENABLE_IP_AUTOALLOCATE="true" \
      --set env.PILOT_SKIP_VALIDATE_TRUST_DOMAIN="true" \
      --set global.multiCluster.clusterName=${cluster} \
      --set global.network=${cluster} \
      --set global.trustDomain="${cluster}.local" \
      --set platforms.peering.enabled=true \
      --set license.value=${SOLO_ISTIO_LICENSE_KEY}
    }
    
    upgrade_istiod ${context1} ${cluster1}
    upgrade_istiod ${context2} ${cluster2}

    If you prefer to specify your license secret instead of an inline value, you can include --set license.secretRef.name=<name> and --set license.secretRef.namespace=<namespace>. The secret data key must be one of license-key, gloo-mesh-license-key, or gloo-trial-license-key. For an example secret manifest, see https://docs.solo.io/istio/1.26.x/setup/setup/prepare/licensing/.

    function upgrade_istiod() {
      context=${1:?context}
      cluster=${2:?cluster}
      helm upgrade istiod oci://${HELM_REPO}/istiod \
      --namespace istio-system \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f istiod-${cluster}.yaml \
      --set env.PILOT_ENABLE_IP_AUTOALLOCATE="true" \
      --set env.PILOT_SKIP_VALIDATE_TRUST_DOMAIN="true" \
      --set global.multiCluster.clusterName=${cluster} \
      --set global.network=${cluster} \
      --set global.platform=openshift \
      --set global.trustDomain="${cluster}.local" \
      --set platforms.peering.enabled=true \
      --set license.value=${SOLO_ISTIO_LICENSE_KEY}
    }
    
    upgrade_istiod ${context1} ${cluster1}
    upgrade_istiod ${context2} ${cluster2}

  4. Verify that the istiod pods are successfully restarted in both clusters. Note that it might take a few seconds for the pods to become available.

    kubectl get pods --context ${context1} -n istio-system | grep istiod
    kubectl get pods --context ${context2} -n istio-system | grep istiod

    Example output:

    istiod-b84c55cff-tllfr   1/1     Running   0          58s
  5. Optional: Check the istiod logs in both clusters to verify that the certificate you generated earlier is picked up by istiod.

    kubectl logs deploy/istiod -n istio-system --context ${context1} | grep x509
    kubectl logs deploy/istiod -n istio-system --context ${context2} | grep x509

    Example output:

    2025-12-16T22:59:06.783901Z     info    x509 cert - Issuer: "CN=Intermediate CA,O=Istio,L=cluster-1", Subject: "", SN: def320623729b8370172413749143836, NotBefore: "2025-12-16T22:57:06Z", NotAfter: "2035-12-14T22:59:06Z"
    2025-12-16T22:59:06.783937Z     info    x509 cert - Issuer: "CN=Root CA,O=Istio", Subject: "CN=Intermediate CA,O=Istio,L=cluster-1", SN: 452d45254328667ccf8434c64c79fe789612bb5a, NotBefore: "2025-12-16T22:54:30Z", NotAfter: "2035-12-14T22:54:30Z"
    2025-12-16T22:59:06.783966Z     info    x509 cert - Issuer: "CN=Root CA,O=Istio", Subject: "CN=Root CA,O=Istio", SN: 38dad68e56ab8c506ae07454801f65b134bd9580, NotBefore: "2025-12-16T22:54:30Z", NotAfter: "2035-12-14T22:54:30Z"
  6. Get the current values for the ztunnel Helm release in both clusters.

    function get_ztunnel_values() {
      context=${1:?context}
      cluster=${2:?cluster}
      helm get values ztunnel --kube-context ${context} -n istio-system -o yaml > ztunnel-${cluster}.yaml
    }
    
    get_ztunnel_values ${context1} ${cluster1}
    get_ztunnel_values ${context2} ${cluster2}
    function get_ztunnel_values() {
      context=${1:?context}
      cluster=${2:?cluster}
      helm get values ztunnel --kube-context ${context} -n istio-system -o yaml > ztunnel-${cluster}.yaml
    }
    
    get_ztunnel_values ${context1} ${cluster1}
    get_ztunnel_values ${context2} ${cluster2}

  7. Update your Helm release with the following multicluster values in both clusters. If you must update the Istio minor version, include the --set tag=${ISTIO_IMAGE} and --set hub=${REPO} flags too.

    function upgrade_ztunnel() {
      context=${1:?context}
      cluster=${2:?cluster}
      helm upgrade ztunnel oci://${HELM_REPO}/ztunnel \
      -n istio-system \
      --version ${ISTIO_IMAGE} \
      --kube-context ${context} \
      -f ztunnel-${cluster}.yaml \
      --set env.SKIP_VALIDATE_TRUST_DOMAIN="true" \
      --set multiCluster.clusterName=${cluster} \
      --set network=${cluster}
    }
    
    upgrade_ztunnel ${context1} ${cluster1}
    upgrade_ztunnel ${context2} ${cluster2}
    function upgrade_ztunnel() {
      context=${1:?context}
      cluster=${2:?cluster}
      helm upgrade ztunnel oci://${HELM_REPO}/ztunnel \
      -n istio-system \
      --version ${ISTIO_IMAGE} \
      --kube-context ${context} \
      -f ztunnel-${cluster}.yaml \
      --set env.SKIP_VALIDATE_TRUST_DOMAIN="true" \
      --set multiCluster.clusterName=${cluster} \
      --set network=${cluster}
    }
    
    upgrade_ztunnel ${context1} ${cluster1}
    upgrade_ztunnel ${context2} ${cluster2}

  8. Verify that the ztunnel pods are successfully installed in both clusters. Because the ztunnel is deployed as a daemon set, the number of pods equals the number of nodes in your cluster. Note that it might take a few seconds for the pods to become available.

    kubectl get pods -A --context ${context1} | grep ztunnel
    kubectl get pods -A --context ${context2} | grep ztunnel

    Example output for one cluster:

    ztunnel-tvtzn             1/1     Running   0          7s
    ztunnel-vtpjm             1/1     Running   0          4s
    ztunnel-hllxg             1/1     Running   0          4s
  9. Create an east-west gateway in the istio-eastwest namespace. In each cluster, the east-west gateway is implemented as a ztunnel that facilitates traffic between services across clusters in your multicluster mesh. Create an east-west gateway in both clusters. In this example Gateway resource, the gatewayClassName that is used, istio-eastwest, is included by default when you install Istio in ambient mode. For customization options, see the gateway guide in the Istio docs.

    apiVersion: gateway.networking.k8s.io/v1
    kind: Gateway
    metadata:
      labels:
        istio.io/expose-istiod: "15012"
        topology.istio.io/network: "cluster1"
      name: istio-eastwest
      namespace: istio-eastwest
    spec:
      gatewayClassName: istio-eastwest
      listeners:
      - name: cross-network
        port: 15008
        protocol: HBONE
        tls:
          mode: Passthrough
      - name: xds-tls
        port: 15012
        protocol: TLS
        tls:
          mode: Passthrough
  10. Verify that the east-west gateway is successfully deployed in both clusters.

    kubectl get svc -n istio-eastwest --context ${context1}
    kubectl get svc -n istio-eastwest --context ${context2}

    Example output:

    NAME             TYPE           CLUSTER-IP       EXTERNAL-IP             PORT(S)                                           AGE
    istio-eastwest   LoadBalancer   172.20.205.104   <external_address>      15021:31655/TCP,15008:32699/TCP,15012:32166/TCP   55s
    NAME             TYPE           CLUSTER-IP       EXTERNAL-IP             PORT(S)                                           AGE
    istio-eastwest   LoadBalancer   172.20.21.117    <external_address>      15021:30324/TCP,15008:31875/TCP,15012:31050/TCP   77s

Link clusters

Link clusters to enable cross-cluster service discovery and allow traffic to be routed through east-west gateways across clusters.

  1. Link clusters to enable cross-cluster service discovery and allow traffic to be routed through east-west gateways across clusters. Note that you can either link the clusters bi-directionally or asymmetrically. In a standard bi-directional setup, services in any of the linked clusters can send requests to and receive requests from the services in any of the other linked clusters. In an asymmetrical setup, you allow one cluster to send requests to another cluster, but the other cluster cannot send requests back to the first cluster.

    In the Solo distribution of Istio 1.29 or later, you can use the peering Helm chart to link your clusters.

    1. Get the addresses of the east-west gateway in each cluster. The following commands show examples for two clusters.

      export CLUSTER1_EW_ADDRESS=$(kubectl get svc -n istio-eastwest istio-eastwest --context ${context1} -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
      export CLUSTER2_EW_ADDRESS=$(kubectl get svc -n istio-eastwest istio-eastwest --context ${context2} -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
      echo "Cluster1 east-west gateway: $CLUSTER1_EW_ADDRESS"
      echo "Cluster2 east-west gateway: $CLUSTER2_EW_ADDRESS"
    2. Link the clusters by creating Helm releases in each cluster to represent the other clusters. In each cluster where you create Helm releases, a Gateway resource is created that uses the istio-remote GatewayClass. This class allows the gateway to connect to other clusters by using the addresses of the east-west gateways.

      • Be sure to update the region, and optionally zone, of each cluster.
      • If you want to use NodePorts instead of the gateway LoadBalancer IP address for cross-cluster traffic, change the preferredDataplaneServiceType to nodeport. This automatically configures the peering gateways for NodePort-based cross-cluster traffic.
      • The following example depicts a bi-directional setup. In an asymmetrical setup, you create Helm releases to only represent the directionality you want to allow.
      helm upgrade -i peering-remote oci://${HELM_REPO}/peering \
        --version ${ISTIO_IMAGE} \
        --namespace istio-eastwest \
        --kube-context ${context1} \
        -f - <<EOF
      remote:
        create: true
        items:
          # Remote peer configuration for cluster2
          - name: istio-remote-peer-${cluster2}
            cluster: ${cluster2}
            network: ${cluster2}
            # Change to "Hostname" if using hostname
            addressType: IPAddress
            address: ${CLUSTER2_EW_ADDRESS}
            # Change to "nodeport" if using NodePorts
            preferredDataplaneServiceType: loadbalancer
            trustDomain: "${cluster2}.local"
            region: "<cluster2_region>"
            # Uncomment as needed
            # zone: "<cluster2_zone>"
      EOF
      helm upgrade -i peering-remote oci://${HELM_REPO}/peering \
        --version ${ISTIO_IMAGE} \
        --namespace istio-eastwest \
        --kube-context ${context2} \
        -f - <<EOF
      remote:
        create: true
        items:
          # Remote peer configuration for cluster1
          - name: istio-remote-peer-${cluster1}
            cluster: ${cluster1}
            network: ${cluster1}
            # Change to "Hostname" if using hostname
            addressType: IPAddress
            address: ${CLUSTER1_EW_ADDRESS}
            # Change to "nodeport" if using NodePorts
            preferredDataplaneServiceType: loadbalancer
            trustDomain: "${cluster1}.local"
            region: "<cluster1_region>"
            # Uncomment as needed
            # zone: "<cluster1_zone>"
      EOF

    Use the istioctl multicluster link command to quickly link clusters.

    1. Verify that the contexts for the clusters that you want to include in the multicluster mesh are listed in your kubeconfig file, which is required for the istioctl multicluster link command. If you do not have access to the kubeconfig files, use the declarative resources tab.

      kubectl config get-contexts
      • If you have multiple kubeconfig files, you can generate a merged kubeconfig file by running the following command.
        KUBECONFIG=<kubeconfig_file1>.yaml:<file2>.yaml kubectl config view --flatten
    2. Using the names of the cluster contexts, link the clusters so that they can communicate. To take a look at the Gateway resources that this command creates, you can include the --generate flag in the command. For more information about this command, see the CLI reference.

      • Bi-directional: You can use the following istioctl command to quickly link the clusters bi-directionally. In each cluster, Gateway resources are created that use the istio-remote GatewayClass. This class allows the gateways to connect to other clusters by using the addresses of the east-west gateways.

        istioctl multicluster link --namespace istio-eastwest --contexts="$context1,$context2"

        Example output for two clusters:

        Gateway istio-eastwest/istio-remote-peer-cluster1 applied to cluster "cluster2" pointing to cluster "cluster1" (network "cluster1")
        Gateway istio-eastwest/istio-remote-peer-cluster2 applied to cluster "cluster1" pointing to cluster "cluster2" (network "cluster2")
      • Asymmetrical: You can use the following istioctl command to quickly link the clusters asymmetrically. The services in the cluster in the --from flag can send requests to services in the cluster in the --to flag, but sending requests in the reverse direction is not permitted.

        For example, this command allows services in cluster1’s mesh to send requests to services in cluster2’s mesh through cluster2’s east-west gateway. However, the reverse is not permitted: services in cluster2’s mesh cannot send requests through cluster1’s east-west gateway to services in cluster1.

        istioctl multicluster link --namespace istio-eastwest --from ${context1} --to ${context2}

        Example output:

        Gateway istio-eastwest/istio-remote-peer-cluster2 applied to cluster "cluster1" pointing to cluster "cluster2" (network "cluster2")

    Link the clusters by declaratively creating istio-remote peer gateways.

    Bi-directional: Use the following Gateway resources to create an istio-remote peer gateway in each cluster. The istio-remote GatewayClass allows the gateways to connect to other clusters by using the addresses of the east-west gateways.

    1. Get the addresses of the east-west gateway in each cluster. The following commands show examples for two clusters.

      export CLUSTER1_EW_ADDRESS=$(kubectl get svc -n istio-eastwest istio-eastwest --context ${context1} -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
      export CLUSTER2_EW_ADDRESS=$(kubectl get svc -n istio-eastwest istio-eastwest --context ${context2} -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
      
      echo "Cluster1 east-west gateway: $CLUSTER1_EW_ADDRESS"
      echo "Cluster2 east-west gateway: $CLUSTER2_EW_ADDRESS"
    2. Using the east-west gateway addresses, create a Gateway resource in each cluster to represent the other cluster.

      kubectl apply --context ${context1} -f- <<EOF
      apiVersion: gateway.networking.k8s.io/v1
      kind: Gateway
      metadata:
        annotations:
          gateway.istio.io/service-account: istio-eastwest
          gateway.istio.io/trust-domain: ${cluster2}
        labels:
          topology.istio.io/network: ${cluster2}
        name: istio-remote-peer-${cluster2}
        namespace: istio-eastwest
      spec:
        addresses:
        # Change to 'type: Hostname' as needed, such as for AWS hostnames
        - type: IPAddress
          value: $CLUSTER2_EW_ADDRESS
        gatewayClassName: istio-remote
        listeners:
        - name: cross-network
          port: 15008
          protocol: HBONE
          tls:
            mode: Passthrough
        - name: xds-tls
          port: 15012
          protocol: TLS
          tls:
            mode: Passthrough
      EOF
      kubectl apply --context ${context2} -f- <<EOF
      apiVersion: gateway.networking.k8s.io/v1
      kind: Gateway
      metadata:
        annotations:
          gateway.istio.io/service-account: istio-eastwest
          gateway.istio.io/trust-domain: ${cluster1}
        labels:
          topology.istio.io/network: ${cluster1}
        name: istio-remote-peer-${cluster1}
        namespace: istio-eastwest
      spec:
        addresses:
        # Change to 'type: Hostname' as needed, such as for AWS hostnames
        - type: IPAddress
          value: $CLUSTER1_EW_ADDRESS
        gatewayClassName: istio-remote
        listeners:
        - name: cross-network
          port: 15008
          protocol: HBONE
          tls:
            mode: Passthrough
        - name: xds-tls
          port: 15012
          protocol: TLS
          tls:
            mode: Passthrough
      EOF

    Asymmetrical: Use the following Gateway resources to create an istio-remote peer gateway in only some clusters. The istio-remote GatewayClass allows the gateway in one cluster to connect to another cluster by using the address of the east-west gateway, but sending requests in the reverse direction is not permitted.

    1. Get the address of the east-west gateway in the cluster that you want to send traffic to. The following command shows an example for cluster2.

      export CLUSTER2_EW_ADDRESS=$(kubectl get svc -n istio-eastwest istio-eastwest --context ${context2} -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
      
      echo "Cluster2 east-west gateway: $CLUSTER2_EW_ADDRESS"
    2. Using the east-west gateway address, create a Gateway resource in the cluster that you want to send requests from. For example, this Gateway resource allows services in cluster1’s mesh to send requests to services in cluster2’s mesh through cluster2’s east-west gateway. However, the reverse is not permitted: services in cluster2’s mesh cannot send requests through cluster1’s east-west gateway to services in cluster1.

      kubectl apply --context ${context1} -f- <<EOF
      apiVersion: gateway.networking.k8s.io/v1
      kind: Gateway
      metadata:
        annotations:
          gateway.istio.io/service-account: istio-eastwest
          gateway.istio.io/trust-domain: ${cluster2}
        labels:
          topology.istio.io/network: ${cluster2}
        name: istio-remote-peer-${cluster2}
        namespace: istio-eastwest
      spec:
        addresses:
        # Change to 'type: Hostname' as needed, such as for AWS hostnames
        - type: IPAddress
          value: $CLUSTER2_EW_ADDRESS
        gatewayClassName: istio-remote
        listeners:
        - name: cross-network
          port: 15008
          protocol: HBONE
          tls:
            mode: Passthrough
        - name: xds-tls
          port: 15012
          protocol: TLS
          tls:
            mode: Passthrough
      EOF
  2. Optional: Verify that the istiod control plane for each peered cluster is included in each cluster’s proxy status list.

    istioctl proxy-status --context ${context1}
    istioctl proxy-status --context ${context2}

    Example output for cluster1, in which you can verify that the istiod control plane for cluster2 is listed:

    NAME                                               CLUSTER          ISTIOD                      VERSION              SUBSCRIBED TYPES
    istio-eastwest-67fd5679dc-fhsxs.istio-eastwest     cluster1         istiod-7b7c9cc4c6-bdm9c     1.26.8-patch7-solo-fips     2 (WADS,WDS)
    istiod-6bc6765484-5bbhd.istio-system               cluster2         istiod-7b7c9cc4c6-bdm9c     1.26.8-patch7-solo-fips     3 (FSDS,SGDS,WDS)
    ztunnel-5f8rb.kube-system                          cluster1         istiod-7b7c9cc4c6-bdm9c     1.26.8-patch7-solo-fips     2 (WADS,WDS)
    ztunnel-f96kh.kube-system                          cluster1         istiod-7b7c9cc4c6-bdm9c     1.26.8-patch7-solo-fips     2 (WADS,WDS)
    ztunnel-vtj4f.kube-system                          cluster1         istiod-7b7c9cc4c6-bdm9c     1.26.8-patch7-solo-fips     2 (WADS,WDS)

Next: Add apps to the ambient mesh. For multicluster setups, this includes making specific services available across your linked cluster setup.

Option 3: Automatically link clusters (deprecated)

In each cluster, use Helm to create the ambient mesh components, and create an east-west gateway so that traffic requests can be routed cross-cluster. Then, use the Gloo management plane to automate multicluster linking, which enables cross-cluster service discovery.

Warning

Automated multicluster peering is deprecated and is removed in Solo Enterprise for Istio 1.30. Do not use this feature in production deployments.

Review the following considerations:

  • Multicluster mesh capabilities require an Enterprise level license for Solo Enterprise for Istio. If you do not have one, contact an account representative.
  • Automated peering requires Istio to be installed in the same cluster that the Gloo management plane is deployed to.

Set up tools

  1. Set your Enterprise level license for Solo Enterprise for Istio as an environment variable. If you do not have one, contact an account representative. If you prefer to specify license keys in a secret instead, see Licensing. Note that you might have previously saved this key in another variable, such as ${SOLO_LICENSE_KEY} or ${GLOO_MESH_LICENSE_KEY}.

    export SOLO_ISTIO_LICENSE_KEY=<enterprise_license_key>
  2. Choose the version of Istio that you want to install or upgrade to by reviewing the supported versions.

  3. Save the Solo distribution of Istio version.

    export ISTIO_VERSION=1.26.8-patch7
    export ISTIO_IMAGE=${ISTIO_VERSION}-solo
  4. Save the repo key for the minor version of the Solo distribution of Istio that you want to install. This is the 12-character hash at the end of the repo URL us-docker.pkg.dev/gloo-mesh/istio-<repo-key>, which you can find in the Istio images built by Solo.io support article.

    # 12-character hash at the end of the repo URL
    export REPO_KEY=<repo_key>
    export REPO=us-docker.pkg.dev/gloo-mesh/istio-${REPO_KEY}
    export HELM_REPO=us-docker.pkg.dev/gloo-mesh/istio-helm-${REPO_KEY}
  5. Get the Solo distribution of Istio binary and install istioctl, which you use for multicluster linking and gateway commands. This script automatically detects your OS and architecture, downloads the appropriate Solo distribution of Istio binary, and verifies the installation.

    bash <(curl -sSfL https://raw.githubusercontent.com/solo-io/doc-examples/main/istio/install-istioctl.sh)
    export PATH=${HOME}/.istioctl/bin:${PATH}
  6. Save the names and kubeconfig contexts of each cluster. This guide uses two clusters as an example. To add more clusters to the multicluster setup, include them in the arrays.

    export cluster1=<cluster1_name>
    export context1=<cluster1_context>
    export cluster2=<cluster2_name>
    export context2=<cluster2_context>

Enable automatic peering of clusters

Upgrade Solo Enterprise for Istio in your multicluster setup to enable the ConfigDistribution feature flag and install the enterprise CRDs, which are required for Solo Enterprise for Istio to automate peering and distribute gateways between clusters.

Note

These steps assume you already installed Solo Enterprise for Istio, and show you how to upgrade your Helm install values. If you have not yet installed Solo Enterprise for Istio, follow the steps in Set up multicluster management.

  1. In the cluster where you installed the management plane, upgrade your gloo-platform-crds Helm release to include the following settings.

    helm get values gloo-platform-crds -n gloo-mesh -o yaml --kube-context ${context1} > mgmt-crds.yaml
    helm upgrade gloo-platform-crds gloo-platform/gloo-platform-crds \
      --kube-context ${context1} \
      --namespace gloo-mesh \
      -f mgmt-crds.yaml \
      --set featureGates.ConfigDistribution=true \
      --set installEnterpriseCrds=true
  2. In the cluster where you installed the management plane, upgrade your gloo-platform Helm release to include the following settings.

    helm get values gloo-platform -n gloo-mesh -o yaml --kube-context ${context1} > mgmt-plane.yaml
    helm upgrade gloo-platform gloo-platform/gloo-platform \
      --kube-context ${context1} \
      --namespace gloo-mesh \
      -f mgmt-plane.yaml \
      --set featureGates.ConfigDistribution=true
  3. In each connected cluster, upgrade your gloo-platform-crds Helm release to include the following settings. Repeat this step for each workload cluster.

    helm get values gloo-platform-crds -n gloo-mesh -o yaml --kube-context ${context2} > crds.yaml
    helm upgrade gloo-platform-crds gloo-platform/gloo-platform-crds \
      --kube-context ${context2} \
      --namespace gloo-mesh \
      -f crds.yaml \
      --set installEnterpriseCrds=true

Create a shared root of trust

Each cluster in the multicluster setup must have a shared root of trust. This can be achieved by providing a root certificate signed by a PKI provider, or a custom root certificate created for this purpose. The root certificate signs a unique intermediate CA certificate for each cluster.

By default, the Istio CA generates a self-signed root certificate and key, and uses them to sign the workload certificates. For more information, see the Plug in CA Certificates guide in the community Istio documentation.

For demo installations, you can run the following function to quickly generate and plug in the certificates and key for the Istio CA. Change the Istio version as needed.

curl -L https://istio.io/downloadIstio | ISTIO_VERSION=${ISTIO_VERSION} sh -
cd istio-${ISTIO_VERSION}

mkdir -p certs
pushd certs
make -f ../tools/certs/Makefile.selfsigned.mk root-ca

function create_cacerts_secret() {
  context=${1:?context}
  cluster=${2:?cluster}
  make -f ../tools/certs/Makefile.selfsigned.mk ${cluster}-cacerts
  kubectl --context=${context} create ns istio-system || true
  kubectl --context=${context} create secret generic cacerts -n istio-system \
    --from-file=${cluster}/ca-cert.pem \
    --from-file=${cluster}/ca-key.pem \
    --from-file=${cluster}/root-cert.pem \
    --from-file=${cluster}/cert-chain.pem
}

create_cacerts_secret ${context1} ${cluster1}
create_cacerts_secret ${context2} ${cluster2}

cd ../..

To enhance the security of your setup even further and have full control over the Istio CA lifecycle, you can generate and store the root and intermediate CA certificates and keys with your own PKI provider. You can then use tools such as cert-manager to send certificate signing requests on behalf of istiod to your PKI provider. Cert-manager stores the signed intermediate certificates and keys in the cacerts Kubernetes secret so that istiod can use these credentials to issue leaf certificates for the workloads in the service mesh. You can set up cert-manager to also check the certificates and renew them before they expire.

AWS Private CA issuer and cert-manager: For steps on how to deploy this certificate setup, check out this Solo.io blog post. Be sure to repeat the steps so that a cacerts secret exists in each cluster.

Deploy ambient components

  1. Apply the CRDs for the Kubernetes Gateway API to your cluster, which are required to create components such as waypoint proxies for L7 traffic policies, gateways with the Gateway resource, and more.

    kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml --context ${context1}
    kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml --context ${context2}
  2. Install the base chart, which contains the CRDs and cluster roles required to set up Istio, in both clusters.

    function install_base() {
      context=${1:?context}
      helm upgrade --install istio-base oci://${HELM_REPO}/base \
      --namespace istio-system \
      --create-namespace \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f - <<EOF
    defaultRevision: ""
    profile: ambient
    EOF
    }
    
    install_base ${context1}
    install_base ${context2}
    function install_base() {
      context=${1:?context}
      helm upgrade --install istio-base oci://${HELM_REPO}/base \
      --namespace istio-system \
      --create-namespace \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f - <<EOF
    defaultRevision: ""
    profile: ambient
    global:
      platform: openshift
    EOF
    }
    
    install_base ${context1}
    install_base ${context2}

    You can optionally verify that the CRDs are successfully installed in both clusters.

    kubectl get crds -l app.kubernetes.io/instance=istio-base --kube-context ${context1}
    kubectl get crds -l app.kubernetes.io/instance=istio-base --kube-context ${context2}

    Example output:

    NAME                                       CREATED AT
    authorizationpolicies.security.istio.io    2025-12-16T22:56:00Z
    destinationrules.networking.istio.io       2025-12-16T22:56:00Z
    envoyfilters.networking.istio.io           2025-12-16T22:56:00Z
    gateways.networking.istio.io               2025-12-16T22:56:00Z
    peerauthentications.security.istio.io      2025-12-16T22:56:00Z
    proxyconfigs.networking.istio.io           2025-12-16T22:56:00Z
    requestauthentications.security.istio.io   2025-12-16T22:56:00Z
    segments.admin.solo.io                     2025-12-16T22:56:00Z
    serviceentries.networking.istio.io         2025-12-16T22:56:00Z
    sidecars.networking.istio.io               2025-12-16T22:56:00Z
    telemetries.telemetry.istio.io             2025-12-16T22:56:00Z
    virtualservices.networking.istio.io        2025-12-16T22:56:00Z
    wasmplugins.extensions.istio.io            2025-12-16T22:56:00Z
    workloadentries.networking.istio.io        2025-12-16T22:56:00Z
    workloadgroups.networking.istio.io         2025-12-16T22:56:00Z
  3. Create the istiod control plane in both clusters.

    function install_istiod() {
      context=${1:?context}
      cluster=${2:?cluster}
      helm upgrade --install istiod oci://${HELM_REPO}/istiod \
      --namespace istio-system \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f - <<EOF
    env:
      # Enables automatic creation of remote peer gateways
      PEERING_AUTOMATIC_LOCAL_GATEWAY: "true"
      # Assigns IP addresses to multicluster services
      PILOT_ENABLE_IP_AUTOALLOCATE: "true"
      # Required when meshConfig.trustDomain is set
      PILOT_SKIP_VALIDATE_TRUST_DOMAIN: "true"
    global:
      hub: ${REPO}
      multiCluster:
        clusterName: ${cluster}
      network: ${cluster}
      proxy:
        clusterDomain: cluster.local
      tag: ${ISTIO_IMAGE}
    meshConfig:
      accessLogFile: /dev/stdout
      defaultConfig:
        proxyMetadata:
          ISTIO_META_DNS_CAPTURE: "true"
      # Assign each cluster a unique trust domain to apply policies to specific clusters
      trustDomain: "${cluster}.local"
    pilot:
      cni:
        namespace: istio-system
        enabled: true
    # Required to enable multicluster support
    platforms:
      peering:
        enabled: true
    profile: ambient
    license:
      value: ${SOLO_ISTIO_LICENSE_KEY}
      # Uncomment if you prefer to specify your license secret instead of an inline value.
      # The secret data key must be one of: license-key, gloo-mesh-license-key, or gloo-trial-license-key
      # secretRef:
      #   name: <secret-name>
      #   namespace: <secret-namespace>
    EOF
    }
    
    install_istiod ${context1} ${cluster1}
    install_istiod ${context2} ${cluster2}
    function install_istiod() {
      context=${1:?context}
      cluster=${2:?cluster}
      helm upgrade --install istiod oci://${HELM_REPO}/istiod \
      --namespace istio-system \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f - <<EOF
    env:
      # Enables automatic creation of remote peer gateways
      PEERING_AUTOMATIC_LOCAL_GATEWAY: "true"
      # Assigns IP addresses to multicluster services
      PILOT_ENABLE_IP_AUTOALLOCATE: "true"
      # Required when meshConfig.trustDomain is set
      PILOT_SKIP_VALIDATE_TRUST_DOMAIN: "true"
    global:
      hub: ${REPO}
      multiCluster:
        clusterName: ${cluster}
      network: ${cluster}
      platform: openshift
      proxy:
        clusterDomain: cluster.local
      tag: ${ISTIO_IMAGE}
    meshConfig:
      accessLogFile: /dev/stdout
      defaultConfig:
        proxyMetadata:
          ISTIO_META_DNS_CAPTURE: "true"
      # Assign each cluster a unique trust domain to apply policies to specific clusters
      trustDomain: "${cluster}.local"
    pilot:
      cni:
        namespace: istio-system
        enabled: true
    # Required to enable multicluster support
    platforms:
      peering:
        enabled: true
    profile: ambient
    license:
      value: ${SOLO_ISTIO_LICENSE_KEY}
      # Uncomment if you prefer to specify your license secret instead of an inline value.
      # The secret data key must be one of: license-key, gloo-mesh-license-key, or gloo-trial-license-key
      # secretRef:
      #   name: <secret-name>
      #   namespace: <secret-namespace>
    EOF
    }
    
    install_istiod ${context1} ${cluster1}
    install_istiod ${context2} ${cluster2}
  4. Install the Istio CNI node agent daemonset in both clusters. Note that although the CNI is included in this section, it is technically not part of the control plane or data plane.

    function install_cni() {
      context=${1:?context}
      helm upgrade --install istio-cni oci://${HELM_REPO}/cni \
      --namespace istio-system \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f - <<EOF
    # Assigns IP addresses to multicluster services
    ambient:
      dnsCapture: true
    excludeNamespaces:
      - istio-system
      - kube-system
    global:
      hub: ${REPO}
      tag: ${ISTIO_IMAGE}
    profile: ambient
    EOF
    }
    
    install_cni ${context1}
    install_cni ${context2}
    function install_cni() {
      context=${1:?context}
      helm upgrade --install istio-cni oci://${HELM_REPO}/cni \
      --namespace istio-system \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f - <<EOF
    # Assigns IP addresses to multicluster services
    ambient:
      dnsCapture: true
    excludeNamespaces:
      - istio-system
      - kube-system
    global:
      hub: ${REPO}
      platform: openshift
      tag: ${ISTIO_IMAGE}
    profile: ambient
    EOF
    }
    
    install_cni ${context1}
    install_cni ${context2}
  5. Verify that the components of the Istio ambient control plane are successfully installed in both clusters. Because the Istio CNI is deployed as a daemon set, the number of CNI pods equals the number of nodes in your cluster. Note that it might take a few seconds for the pods to become available.

    kubectl get pods -A --context ${context1} | grep istio
    kubectl get pods -A --context ${context2} | grep istio

    Example output:

    istio-system   istiod-85c4dfd97f-mncj5                             1/1     Running   0               40s
    istio-system   istio-cni-node-pr5rl                                1/1     Running   0               9s
    istio-system   istio-cni-node-pvmx2                                1/1     Running   0               9s
    istio-system   istio-cni-node-6q26l                                1/1     Running   0               9s
  6. Optional: Check the istiod logs in both clusters to verify that the certificate you generated earlier is picked up by istiod.

    kubectl logs deploy/istiod -n istio-system --context ${context1} | grep x509
    kubectl logs deploy/istiod -n istio-system --context ${context2} | grep x509

    Example output:

    2025-12-16T22:59:06.783901Z     info    x509 cert - Issuer: "CN=Intermediate CA,O=Istio,L=cluster-1", Subject: "", SN: def320623729b8370172413749143836, NotBefore: "2025-12-16T22:57:06Z", NotAfter: "2035-12-14T22:59:06Z"
    2025-12-16T22:59:06.783937Z     info    x509 cert - Issuer: "CN=Root CA,O=Istio", Subject: "CN=Intermediate CA,O=Istio,L=cluster-1", SN: 452d45254328667ccf8434c64c79fe789612bb5a, NotBefore: "2025-12-16T22:54:30Z", NotAfter: "2035-12-14T22:54:30Z"
    2025-12-16T22:59:06.783966Z     info    x509 cert - Issuer: "CN=Root CA,O=Istio", Subject: "CN=Root CA,O=Istio", SN: 38dad68e56ab8c506ae07454801f65b134bd9580, NotBefore: "2025-12-16T22:54:30Z", NotAfter: "2035-12-14T22:54:30Z"
  7. Install the ztunnel daemonset in both clusters.

    function install_ztunnel() {
      context=${1:?context}
      cluster=${2:?cluster}
      helm upgrade --install ztunnel oci://${HELM_REPO}/ztunnel \
      --namespace istio-system \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f - <<EOF
    configValidation: true
    enabled: true
    env:
      L7_ENABLED: "true"
      # Required when a unique trust domain is set for each cluster
      SKIP_VALIDATE_TRUST_DOMAIN: "true"
    hub: ${REPO}
    istioNamespace: istio-system
    multiCluster:
      clusterName: ${cluster}
    namespace: istio-system
    network: ${cluster}
    profile: ambient
    proxy:
      clusterDomain: cluster.local
    tag: ${ISTIO_IMAGE}
    terminationGracePeriodSeconds: 29
    variant: distroless
    EOF
    }
    
    install_ztunnel ${context1} ${cluster1}
    install_ztunnel ${context2} ${cluster2}
    function install_ztunnel() {
      context=${1:?context}
      cluster=${2:?cluster}
      helm upgrade --install ztunnel oci://${HELM_REPO}/ztunnel \
      --namespace istio-system \
      --kube-context ${context} \
      --version ${ISTIO_IMAGE} \
      -f - <<EOF
    configValidation: true
    enabled: true
    env:
      L7_ENABLED: "true"
      # Required when a unique trust domain is set for each cluster
      SKIP_VALIDATE_TRUST_DOMAIN: "true"
    global:
      platform: openshift
    hub: ${REPO}
    istioNamespace: istio-system
    multiCluster:
      clusterName: ${cluster}
    namespace: istio-system
    network: ${cluster}
    profile: ambient
    proxy:
      clusterDomain: cluster.local
    tag: ${ISTIO_IMAGE}
    terminationGracePeriodSeconds: 29
    variant: distroless
    EOF
    }
    
    install_ztunnel ${context1} ${cluster1}
    install_ztunnel ${context2} ${cluster2}
  8. Verify that the ztunnel pods are successfully installed in both clusters. Because the ztunnel is deployed as a daemon set, the number of pods equals the number of nodes in your cluster. Note that it might take a few seconds for the pods to become available.

    kubectl get pods -A --context ${context1} | grep ztunnel
    kubectl get pods -A --context ${context2} | grep ztunnel

    Example output:

    ztunnel-tvtzn             1/1     Running   0          7s
    ztunnel-vtpjm             1/1     Running   0          4s
    ztunnel-hllxg             1/1     Running   0          4s
  9. Label the istio-system namespace with the clusters’ network names, which you previously set to each cluster name in the global.network field of the istiod installations. The ambient control plane uses this label internally to group pods that exist in the same L3 network.

    kubectl label namespace istio-system --context ${context1} topology.istio.io/network=${cluster1}
    kubectl label namespace istio-system --context ${context2} topology.istio.io/network=${cluster2}
  10. Create an east-west gateway in the istio-eastwest namespace. In each cluster, the east-west gateway is implemented as a ztunnel that facilitates traffic between services across clusters in your multicluster mesh. Create an east-west gateway in both clusters. In this example Gateway resource, the gatewayClassName that is used, istio-eastwest, is included by default when you install Istio in ambient mode. For customization options, see the gateway guide in the Istio docs.

    apiVersion: gateway.networking.k8s.io/v1
    kind: Gateway
    metadata:
      labels:
        istio.io/expose-istiod: "15012"
        topology.istio.io/network: "cluster1"
      name: istio-eastwest
      namespace: istio-eastwest
    spec:
      gatewayClassName: istio-eastwest
      listeners:
      - name: cross-network
        port: 15008
        protocol: HBONE
        tls:
          mode: Passthrough
      - name: xds-tls
        port: 15012
        protocol: TLS
        tls:
          mode: Passthrough
  11. Verify that the east-west gateway is successfully deployed in both clusters.

    kubectl get svc -n istio-eastwest --context ${context1}
    kubectl get svc -n istio-eastwest --context ${context2}

    Example output:

    NAME             TYPE           CLUSTER-IP       EXTERNAL-IP             PORT(S)                                           AGE
    istio-eastwest   LoadBalancer   172.20.205.104   <external_address>      15021:31655/TCP,15008:32699/TCP,15012:32166/TCP   55s
    NAME             TYPE           CLUSTER-IP       EXTERNAL-IP             PORT(S)                                           AGE
    istio-eastwest   LoadBalancer   172.20.21.117    <external_address>      15021:30324/TCP,15008:31875/TCP,15012:31050/TCP   77s

Review remote peer gateways

After you complete the steps for each cluster, verify that Solo Enterprise for Istio successfully created and distributed the remote peering gateways. These gateways use the istio-remote GatewayClass, which allows the istiod control plane in each cluster to discover the east-west gateway addresses of other clusters. Solo Enterprise for Istio generates one istio-remote resource in the cluster where the management plane is deployed for each connected cluster, and then distributes the gateway to each cluster respectively.

  1. Verify that an istio-remote gateway for each connected cluster is copied to the cluster where the management plane is deployed.

    kubectl get gateways -n istio-eastwest --context ${context1}

    In this example output, the istio-remote gateway that was auto-generated for connected cluster cluster2 is copied to cluster1 where the management plane is deployed, alongside cluster1’s own istio-remote gateway and east-west gateway.

    NAMESPACE        NAME                            CLASS           ADDRESS                                                                   PROGRAMMED   AGE
    istio-eastwest   istio-eastwest                 istio-eastwest   a7f6f1a2611fc4eb3864f8d688622fd4-1234567890.us-east-1.elb.amazonaws.com   True         6s
    istio-eastwest   istio-remote-peer-cluster1     istio-remote     a5082fe9522834b8192a6513eb8c6b01-0987654321.us-east-1.elb.amazonaws.com   True         4s
    istio-eastwest   istio-remote-peer-cluster2     istio-remote     aaad62dc3ffb142a1bfc13df7fe9665b-5678901234.us-east-1.elb.amazonaws.com   True         4s
  2. In each connected cluster, verify that all istio-remote gateways are successfully distributed to all workload clusters. This ensures that services in each workload cluster can now access the east-west gateways in other clusters of the multicluster mesh setup.

    kubectl get gateways -n istio-eastwest --context ${context2}

Next

  • Add apps to the ambient mesh. For multicluster setups, this includes making specific services available across your linked cluster setup.
  • In a multicluster mesh, the east-west gateway serves as a ztunnel that allows traffic requests to flow across clusters, but it does not modify requests in any way. To control in-mesh traffic, you can instead apply policies to waypoint proxies that you create for a workload namespace.

Optional: Validate your multicluster setup

Both before and after you link clusters into a multicluster mesh, you can use the istioctl multicluster check command, along with other observability checks, to verify multiple aspects of multicluster ambient mesh support and status.

istioctl multicluster check

You can use the istioctl multicluster check --precheck command to check the individual readiness of each cluster before running istioctl multicluster link to link them in a multicluster mesh, and run it again after linking to confirm that the connections were successful. This command performs checks listed in the following sections, which you can review to understand what each check validates. Additionally, if any of the checks fail, run the command with the --verbose option, and review the following troubleshooting recommendations.

istioctl multicluster check --verbose --contexts="$context1,$context2"

Incompatible environment variables

Checks whether the ENABLE_PEERING_DISCOVERY=true and optionally K8S_SELECT_WORKLOAD_ENTRIES=true environment variables are set incorrectly or are not supported for multicluster ambient mesh.

Example verbose output:

--- Incompatible Environment Variable Check ---

✅ Incompatible Environment Variable Check: K8S_SELECT_WORKLOAD_ENTRIES is valid ("")
✅ Incompatible Environment Variable Check: ENABLE_PEERING_DISCOVERY is valid ("true")
✅ Incompatible Environment Variable Check: all relevant environment variables are valid

If this check fails, check your environment variables in your istiod configuration, such as by running helm get values --kube-context ${CLUSTER_CONTEXT} istiod -n istio-system -o yaml, and update your configuration.

License validity

Checks whether the license in use by istiod is valid for multicluster ambient mesh. Multicluster capabilities require an Enterprise level license for Solo Enterprise for Istio.

Example verbose output:

--- License Check ---

✅ License Check: license is valid for multicluster

If your license does not support multicluster ambient mesh, contact your Solo account representative.

Pod health

Checks the health of the pods in the cluster. All istiod, ztunnel, and east-west gateway pods across the checked clusters must be healthy and running for the multicluster mesh to function correctly.

Example verbose output:

--- Pod Check (istiod) ---

NAME                        READY     STATUS      RESTARTS     AGE
istiod-6d9cdf88cf-l47tf     1/1       Running     0            10m18s

✅ Pod Check (istiod): all pods healthy


--- Pod Check (ztunnel) ---

NAME              READY     STATUS      RESTARTS     AGE
ztunnel-dvlwk     1/1       Running     0            10m6s

✅ Pod Check (ztunnel): all pods healthy


--- Pod Check (eastwest gateway) ---

NAME                                READY     STATUS      RESTARTS     AGE
istio-eastwest-857b77fc5d-qgnrl     1/1       Running     0            9m33s

✅ Pod Check (eastwest gateway): all pods healthy

To check any unhealthy pods, run the following commands. Consider checking the pod logs, and review Debug Istio.

kubectl get po -n istio-system
kubectl get po -n istio-eastwest

East-west gateway status

Checks the status of the east-west gateways in the cluster. When an east-west gateway is created, the gateway controller creates a Kubernetes service to expose the gateway. Once this service is correctly attached to the gateway and has an address assigned, the east-west gateway has a Programmed status of true.

Example verbose output:

--- Gateway Check ---

Gateway: istio-eastwest
Addresses:
- 172.18.7.110
Status: programmed ✅

✅ Gateway Check: all eastwest gateways programmed

If the Programmed status is not true, an issue might exist with the address allocation for the service. Check the east-west gateway with a command such as kubectl get svc -n istio-eastwest, and verify that your cloud provider can correctly allocate addresses to the service.

Remote peer gateway status

Checks the status of the remote peer gateways in the cluster, which represent the other peered clusters in the multicluster setup. These remote gateways configure the connection between the local cluster’s istiod control plane, and the peered clusters’ remote networks to enable xDS communication between peers. When the initial network connection between istiod and a remote peer is made, the gateway’s gloo.solo.io/PeerConnected status updates to true. Then, when the full xDS sync occurs between peers, the gateway’s gloo.solo.io/PeeringSucceeded status also updates to true. This check ensures that both statuses are true.

Example verbose output:

--- Peers Check ---

Cluster: cluster2
Addresses:
- 172.18.7.130
Conditions:
- Accepted: True
- Programmed: True
- gloo.solo.io/PeerConnected: True
- gloo.solo.io/PeeringSucceeded: True
- gloo.solo.io/PeerDataPlaneProgrammed: True
Status: connected ✅

✅ Peers Check: all clusters connected

If the connection is severed between the peers, the gloo.solo.io/PeerConnected status becomes false. A failed connection between peers can be due to either a misconfiguration in the peering setup, or a network issue blocking port 15008 on the remote cluster, which is the cross-network HBONE port that the east-west gateway listens on. Review the steps you took to link clusters together, such as the steps outlined in the Helm default network guide. Additionally, review any firewall rules or network policies that might block access through port 15008 on the remote cluster.

Intermediate certificate compatibility

Confirms the certificate compatibility between peered clusters. This check reads the root-cert.pem from the istio-ca-root-cert configmap in the istio-system namespace, and uses x509 certificate validation to confirm the root cert is compatible with all of the clusters’ ca-cert.pem intermediate certificate chains from the cacerts secret.

Example verbose output:

--- Intermediate Certs Compatibility Check ---

ℹ  Intermediate Certs Compatibility Check: cluster cluster1 root certificate SHA256 sum: 6d18f32e134824c158d97f32618657c45d5a83839f838ada751757139481537e
ℹ  Intermediate Certs Compatibility Check: cluster cluster2 root certificate SHA256 sum: 6d18f32e134824c158d97f32618657c45d5a83839f838ada751757139481537e
✅ Intermediate Certs Compatibility Check: cluster cluster1 has compatible intermediate certificates with cluster cluster2 
✅ Intermediate Certs Compatibility Check: cluster cluster2 has compatible intermediate certificates with cluster cluster1 
✅ Intermediate Certs Compatibility Check: all clusters have compatible intermediate certificates

If this check fails because the root certs are not valid for each peered clusters’ intermediate certificate chain, you can check the istiod logs for TLS errors when attempting to communicate with a peered cluster, such as the following:

2025-12-04T22:09:22.474517Z     warn    deltaadsc       disconnected, retrying in 24.735483751s: delta stream: rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: remote error: tls: unknown certificate authority"       target=peering-cluster2

Ensure each cluster has a cacerts secret in the istio-system namespace. To regenerate invalid certificates for each cluster, follow the example steps in Create a shared root of trust.

Network configuration

Confirms the network configuration of the multicluster mesh. For multicluster peering setups that do not use a flat network topology, each cluster must occupy a unique network. The network name must be defined with the label topology.istio.io/network and set on both the istio-system namespace and the istio-eastwest gateway resource. The same network name must also be set as the NETWORK environment variable on the ztunnel daemonset. Each remote gateway that represents that cluster must have the topology.istio.io/network label equal to the network of the remote cluster.

Example verbose output:

--- Network Configuration Check ---

✅ Cluster cluster1 has network: cluster1
✅ Eastwest gateway istio-eastwest/istio-eastwest has correct network label: cluster1
✅ Cluster cluster2 has network: cluster2
✅ Eastwest gateway istio-eastwest/istio-eastwest has correct network label: cluster2
✅ Remote gateway istio-eastwest/istio-remote-peer-cluster2 references network cluster2 (clusters: [cluster2])
✅ Remote gateway istio-eastwest/istio-remote-peer-cluster1 references network cluster1 (clusters: [cluster1])
✅ Network Configuration Check: all network configurations are valid

Mismatched network identities cause errors in cross-cluster communication, which leads to error logs in ztunnel pods that indicate a network timeout on the outbound communication. Notably, the destination address on these errors is a 240.X.X.X address, instead of the correct remote peer gateway address. You can run kubectl logs -l app=ztunnel -n istio-system --tail=10 --context ${CLUSTER_CONTEXT} | grep -iE "error|warn" to review logs such as the following:

2025-11-18T16:14:53.490573Z     error   access  connection complete     src.addr=240.0.2.27:46802 src.workload="ratings-v1-5dc79b6bcd-zm8v6" src.namespace="bookinfo" src.identity="spiffe://cluster.local/ns/bookinfo/sa/bookinfo-ratings" dst.addr=240.0.9.43:15008 dst.hbone_addr=240.0.9.43:9080 dst.service="productpage.bookinfo.mesh.internal" dst.workload="autogenflat.portfolio1-soloiopoc-cluster1.bookinfo.productpage-v1-54bb874995-hblwp.ee508601917c" dst.namespace="bookinfo" dst.identity="spiffe://cluster.local/ns/bookinfo/sa/bookinfo-productpage" direction="outbound" bytes_sent=0 bytes_recv=0 duration="10001ms" error="connection timed out, maybe a NetworkPolicy is blocking HBONE port 15008: deadline has elapsed"

To troubleshoot these issues, be sure that you use unique network names to represent each cluster, and that you correctly labeled the cluster’s istio-system namespace with that network name, such as by running kubectl label namespace istio-system --context ${CLUSTER_CONTEXT} topology.istio.io/network=${CLUSTER_NAME}. You can also relabel the east-west gateway in the cluster, and the remote peer gateways in other clusters that represent this cluster.

Note

The topology.istio.io/network label on istio-system must be applied before istiod starts. If you apply or correct the label while istiod is already running, the peering controller enters exponential backoff and does not recover on its own. After applying the label, restart istiod by running kubectl rollout restart deployment/istiod -n istio-system --context ${CLUSTER_CONTEXT}.

Stale workload entries

In flat network setups, checks for any outdated workload entries that must be removed from the multicluster mesh. Stale workload entries might exist from pods that were deleted, but the autogenerated entries for those workloads were not correctly cleaned up. If you do not use a flat network topology, no autogenerated workload entries exist to be validated, and this check can be ignored.

Example verbose output for a non-flat network setup:

--- Stale Workloads Check ---

⚠  Stale Workloads Check: no autogenflat workload entries found

If you use a flat network topology, and this check fails with stale workload entries, run kubectl get workloadentries -n istio-system | grep autogenflat to list the autogenerated workload entries in the remote cluster, and compare the list to the output of kubectl get pods in the source cluster for those workloads. You can safely manually delete the stale workload entries in the remote cluster for pods that no longer exist in the source cluster, such as by running kubectl get workloadentries -n istio-system <entry_name>.

Further debugging and observability

For additional guidance around observing your multicluster ambient mesh, check out the observability overview, which contains links to guides on using logs, metrics, and traces in your Istio environment.

For additional guidance around debugging your multicluster ambient mesh, check out the Istio troubleshooting guide.