Repository: kiali/kiali

Issue:
Add resource memory parameter to keycloak hack scripts
### What do you want to improve?
For some reason, in some OS, keycloak is using more resources than allowed and the pod is not starting. It is tricky to know the reason because the pod is not starting but restarted, and there are no errors in the logs. In can be reproduced (In some OS) when running the multi primary hack scripts, for example:

`hack/run-integration-tests.sh --test-suite frontend-multi-primary --setup-only true`

Add a parameter to be able to increase the resource limits and avoid this issue. Defaults should not be changed (CI is Ok)
It was preventing me to start the environment locally.

```
resources.limits.memory
resources.requests.memory
```

Hints from issue comments:

Solution patch:
diff --git a/README.adoc b/README.adoc
index cd4e2b148..2063d89ab 100644
--- a/README.adoc
+++ b/README.adoc
@@ -238,11 +238,22 @@ link:./hack[hack] directory. Check the `--help` output for available options.
 By default, the script uses the configuration template file located in the same directory it uses, and can be found in the link:./hack/run-kiali-config-template.yaml[config template file]
 also located in the `hack` directory. For additional information, refer to the comments at the top of both files.
 
+The following command has a typical set of options when running this with a typical "all-in-one" environment (single cluster, co-located Istio and Kiali, default "Kubernetes" cluster name), and
+an intent to start a debug server in your IDE (like vscode). It overrides some ports so they don't conflict with defaults:
 
 [source,shell]
 ----
 cd $KIALI_SOURCES/kiali/hack
-./run-kiali.sh
+./run-kiali.sh --enable-server false --tmp-root-dir $HOME/tmp -pg 3005:3005 -cn Kubernetes
+----
+
+The following command has a typical set of options when running this in an external Kiali environment, setup using the install-external-kiali.sh hack script. In this
+environment you will have two clusters, "mesh" has the Istio and deployed apps,  and "mgmt" has Kiali.
+
+[source,shell]
+----
+cd $KIALI_SOURCES/kiali/hack
+./run-kiali.sh --enable-server false --tmp-root-dir $HOME/tmp -pg 3005:3005 -kc mesh -cn mgmt -hkc mgmt
 ----
 
 === Running integration tests
@@ -502,50 +513,3 @@ spec:
       mode: DISABLE
 ...
 ----
-
-== Experimental
-
-=== Observing a Remote Cluster
-
-[NOTE]
-The "Central IstioD" setup is currently named "Primary-remote" multi-cluster setup.
-
-[WARNING]
-When this support was incorporated into Kiali, the "Central IstioD" setup of Istio was in an early development phase. These instructions are probably now broken.
-
-There are certain use cases where Kiali needs to be deployed in one cluster (Control Plane) and observe a different cluster (Data Plane). 
-image:https://user-images.githubusercontent.com/6889074/87819080-ad099980-c839-11ea-834b-56eec038ce4d.png[Diagram]
-
-Follow these steps:
-
-1: Have the link:https://istio.io/latest/docs/setup/install/external-controlplane[Istio with an External Control Plane] setup running
-
-2: Create the link:https://github.com/istio/istio/blob/master/samples/addons/kiali.yaml[Kiali ClusterRole, ClusterRoleBinding, and ServiceAccount] in the Data Plane cluster
-
-3: Create a remote secret in the Control Plane, using the Data Plane ServiceAccount that was just created. This allows the Control Plane to read from and modify the Data Plane
-[source,shell]
-----
-istioctl create-remote-secret --service-account kiali-service-account --context=$DataPlane --name kiali | kubectl apply -n istio-system --context=$ControlPlane -f -
-----
-
-4: Kiali will now run in the Control Plane. The remote secret must be added to the Kiali Deployment by specifying a Volume and VolumeMount. When Kiali detects */kiali-remote-secret/kiali* it will use the remote cluster's API server instead of the local API server
-[source,yaml]
-----
-spec:
-  template:
-    spec:
-      containers:
-      - volumeMounts:
-        - mountPath: /kiali-remote-secret
-          name: kiali-remote-secret
-      volumes:
-      - name: kiali-remote-secret
-        secret:
-          defaultMode: 420
-          optional: true
-          secretName: istio-remote-secret-kiali
-----
-
-5: Kiali now requires Istio metrics from the sidecars. Prometheus must be run in the Control Plane and configured to scrape the metrics from an link:https://istio.io/latest/docs/reference/config/istio.mesh.v1alpha1/#ProxyConfig[envoyMetricsService]. These link:https://kiali.io/docs/faq/general/#which-istio-metrics-and-attributes-are-required-by-kiali[metrics] are *required*.
-
-6: Kiali in the Control Plane should now be fully functional with the Data Plane
diff --git a/business/apps.go b/business/apps.go
index 71b5e1d7d..7979414a0 100644
--- a/business/apps.go
+++ b/business/apps.go
@@ -105,7 +105,7 @@ func (in *AppService) GetClusterAppList(ctx context.Context, criteria AppCriteri
 
 	allApps, err := in.businessLayer.App.fetchNamespaceApps(ctx, namespace, cluster, "")
 	if err != nil {
-		log.Errorf("Error fetching Applications for cluster %s per namespace %s: %s", cluster, namespace, err)
+		log.Errorf("Error fetching Applications for cluster [%s] per namespace [%s]: %s", cluster, namespace, err)
 		return *appList, err
 	}
 
@@ -125,7 +125,7 @@ func (in *AppService) GetClusterAppList(ctx context.Context, criteria AppCriteri
 	if criteria.IncludeIstioResources {
 		istioConfigList, err = in.businessLayer.IstioConfig.GetIstioConfigListForNamespace(ctx, cluster, namespace, icCriteria)
 		if err != nil {
-			log.Errorf("Error fetching Istio Config for Cluster %s per namespace %s: %s", cluster, namespace, err)
+			log.Errorf("Error fetching Istio Config for Cluster [%s] per namespace [%s]: %s", cluster, namespace, err)
 			return *appList, err
 		}
 	}
@@ -252,10 +252,10 @@ func (in *AppService) GetAppList(ctx context.Context, criteria AppCriteria) (mod
 		if resultCh.err != nil {
 			// Return failure if we are in single cluster
 			if resultCh.cluster == in.conf.KubernetesConfig.ClusterName && len(in.userClients) == 1 {
-				log.Errorf("Error fetching Applications for local cluster %s: %s", resultCh.cluster, resultCh.err)
+				log.Errorf("Error fetching Applications for local cluster [%s]: %s", resultCh.cluster, resultCh.err)
 				return models.AppList{}, resultCh.err
 			} else {
-				log.Infof("Error fetching Applications for cluster %s: %s", resultCh.cluster, resultCh.err)
+				log.Infof("Error fetching Applications for cluster [%s]: %s", resultCh.cluster, resultCh.err)
 			}
 		}
 		allApps = append(allApps, resultCh.nsApps)
diff --git a/business/checkers/service_checker.go b/business/checkers/service_checker.go
index 2c95f858b..e35d1f2dc 100644
--- a/business/checkers/service_checker.go
+++ b/business/checkers/service_checker.go
@@ -6,16 +6,28 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/schema"
 
 	"github.com/kiali/kiali/business/checkers/services"
+	"github.com/kiali/kiali/istio"
 	"github.com/kiali/kiali/models"
 )
 
 const ServiceCheckerType = "service"
 
 type ServiceChecker struct {
-	Services    []corev1.Service
-	Deployments []apps_v1.Deployment
-	Pods        []corev1.Pod
-	Cluster     string
+	Cluster       string
+	Deployments   []apps_v1.Deployment
+	MeshDiscovery istio.MeshDiscovery
+	Pods          []corev1.Pod
+	Services      []corev1.Service
+}
+
+func NewServiceChecker(cluster string, deployments []apps_v1.Deployment, meshDiscovery istio.MeshDiscovery, pods []corev1.Pod, services []corev1.Service) ServiceChecker {
+	return ServiceChecker{
+		Cluster:       cluster,
+		Deployments:   deployments,
+		MeshDiscovery: meshDiscovery,
+		Pods:          pods,
+		Services:      services,
+	}
 }
 
 func (sc ServiceChecker) Check() models.IstioValidations {
@@ -33,7 +45,7 @@ func (sc ServiceChecker) runSingleChecks(service corev1.Service) models.IstioVal
 	key, validations := EmptyValidValidation(service.GetObjectMeta().GetName(), service.GetObjectMeta().GetNamespace(), schema.GroupVersionKind{Group: "", Version: "", Kind: ServiceCheckerType}, sc.Cluster)
 
 	enabledCheckers := []Checker{
-		services.PortMappingChecker{Service: service, Deployments: sc.Deployments, Pods: sc.Pods},
+		services.NewPortMappingChecker(sc.Deployments, sc.MeshDiscovery, sc.Pods, service),
 	}
 
 	for _, checker := range enabledCheckers {
diff --git a/business/checkers/services/port_mapping_checker.go b/business/checkers/services/port_mapping_checker.go
index eee137e23..929f66d7b 100644
--- a/business/checkers/services/port_mapping_checker.go
+++ b/business/checkers/services/port_mapping_checker.go
@@ -1,6 +1,7 @@
 package services
 
 import (
+	"context"
 	"fmt"
 	"strings"
 
@@ -10,15 +11,26 @@ import (
 	"k8s.io/apimachinery/pkg/util/intstr"
 
 	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/istio"
 	"github.com/kiali/kiali/kubernetes"
 	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/models"
 )
 
 type PortMappingChecker struct {
-	Service     corev1.Service
-	Deployments []apps_v1.Deployment
-	Pods        []corev1.Pod
+	Deployments   []apps_v1.Deployment
+	MeshDiscovery istio.MeshDiscovery
+	Pods          []corev1.Pod
+	Service       corev1.Service
+}
+
+func NewPortMappingChecker(deployments []apps_v1.Deployment, meshDiscovery istio.MeshDiscovery, pods []corev1.Pod, service corev1.Service) PortMappingChecker {
+	return PortMappingChecker{
+		Deployments:   deployments,
+		MeshDiscovery: meshDiscovery,
+		Pods:          pods,
+		Service:       service,
+	}
 }
 
 func (p PortMappingChecker) Check() ([]*models.IstioCheck, bool) {
@@ -42,7 +54,7 @@ func (p PortMappingChecker) Check() ([]*models.IstioCheck, bool) {
 	}
 
 	// Ignoring istio-system Services as some ports are used for debug purposes and not exposed in deployments
-	if config.IsIstioNamespace(p.Service.Namespace) {
+	if p.MeshDiscovery.IsControlPlane(context.TODO(), "", p.Service.Namespace) {
 		log.Tracef("Skipping Port matching check for Service %s from Istio Namespace %s", p.Service.Name, p.Service.Namespace)
 		return validations, len(validations) == 0
 	}
@@ -60,7 +72,7 @@ func (p PortMappingChecker) Check() ([]*models.IstioCheck, bool) {
 
 func (p PortMappingChecker) hasMatchingPodsWithSidecar(service corev1.Service) bool {
 	sPods := models.Pods{}
-	sPods.Parse(kubernetes.FilterPodsByService(&service, p.Pods))
+	sPods.Parse(kubernetes.FilterPodsByService(&service, p.Pods), p.MeshDiscovery.IsControlPlane)
 	return sPods.HasIstioSidecar()
 }
 
diff --git a/business/istio_config.go b/business/istio_config.go
index 7be3b197f..74e8f895b 100644
--- a/business/istio_config.go
+++ b/business/istio_config.go
@@ -203,12 +203,6 @@ func ToPtrs[T any](s []T) []*T {
 }
 
 func (in *IstioConfigService) getIstioConfigList(ctx context.Context, cluster string, namespace string, criteria IstioConfigCriteria) (*models.IstioConfigList, error) {
-	var end observability.EndFunc
-	_, end = observability.StartSpan(ctx, "GetIstioConfigListForNamespace",
-		observability.Attribute("package", "business"),
-	)
-	defer end()
-
 	istioConfigList := &models.IstioConfigList{
 		DestinationRules: []*networking_v1.DestinationRule{},
 		EnvoyFilters:     []*networking_v1alpha3.EnvoyFilter{},
@@ -234,6 +228,18 @@ func (in *IstioConfigService) getIstioConfigList(ctx context.Context, cluster st
 		RequestAuthentications: []*security_v1.RequestAuthentication{},
 	}
 
+	// This is called from many places but should be ignored for an external kiali home cluster. It's easier to check here
+	// than at all of the callers. If not applicable, just return.
+	if in.conf.Clustering.IgnoreHomeCluster && cluster == in.conf.KubernetesConfig.ClusterName {
+		return istioConfigList, nil
+	}
+
+	var end observability.EndFunc
+	_, end = observability.StartSpan(ctx, "GetIstioConfigListForNamespace",
+		observability.Attribute("package", "business"),
+	)
+	defer end()
+
 	saClient := in.saClients[cluster]
 	if saClient == nil {
 		return nil, fmt.Errorf("kiali ServiceAccount Client for cluster [%s] is not found or is not accessible for Kiali", cluster)
diff --git a/business/istio_status.go b/business/istio_status.go
index 8f7b02017..5dae58831 100644
--- a/business/istio_status.go
+++ b/business/istio_status.go
@@ -124,15 +124,18 @@ func (iss *IstioStatusService) getIstioComponentStatus(ctx context.Context, clus
 		}
 	}
 
-	// if no control plane and no any other control plane which manages this cluster
-	if len(istiodStatus) == 0 && !isManaged {
-		istiodStatus = append(istiodStatus, kubernetes.ComponentStatus{
-			Cluster:   cluster,
-			Name:      "istiod",
-			Namespace: iss.conf.IstioNamespace,
-			Status:    kubernetes.ComponentNotFound,
-			IsCore:    true,
-		})
+	// Ignore istiod status for a mgmt cluster (external kiali)
+	if mesh.ExternalKiali == nil || cluster != mesh.ExternalKiali.Cluster.Name {
+		// if no control plane and no any other control plane which manages this cluster
+		if len(istiodStatus) == 0 && !isManaged {
+			istiodStatus = append(istiodStatus, kubernetes.ComponentStatus{
+				Cluster:   cluster,
+				Name:      "istiod",
+				Namespace: "",
+				Status:    kubernetes.ComponentNotFound,
+				IsCore:    true,
+			})
+		}
 	}
 
 	// Autodiscover gateways.
@@ -208,9 +211,6 @@ func (iss *IstioStatusService) getComponentNamespacesWorkloads(ctx context.Conte
 func getComponentNamespaces(conf *config.Config) []string {
 	nss := make([]string, 0)
 
-	// By default, add the istio control plane namespace
-	nss = append(nss, conf.IstioNamespace)
-
 	// Adding Istio Components namespaces
 	externalServices := conf.ExternalServices
 	for _, cmp := range externalServices.Istio.ComponentStatuses.Components {
@@ -289,7 +289,7 @@ func (iss *IstioStatusService) getStatusOf(workloads []*models.Workload, cluster
 						if stat.Namespace != "" {
 							return stat.Namespace
 						}
-						return iss.conf.IstioNamespace
+						return ""
 					}(),
 					Name:   comp,
 					Status: kubernetes.ComponentNotFound,
diff --git a/business/istio_validations.go b/business/istio_validations.go
index 58ed5f8c5..485810745 100644
--- a/business/istio_validations.go
+++ b/business/istio_validations.go
@@ -103,7 +103,7 @@ func (in *IstioValidationsService) GetValidationsForService(ctx context.Context,
 
 func (in *IstioValidationsService) GetValidationsForWorkload(ctx context.Context, cluster, namespace, workload string) (models.IstioValidations, error) {
 	if namespace == "" {
-		return nil, fmt.Errorf("namespace param should be set for Validations in cluster %s", cluster)
+		return nil, fmt.Errorf("namespace param should be set for Validations in cluster [%s]", cluster)
 	}
 	// Check if user has access to the namespace (RBAC) in cache scenarios and/or
 	// if namespace is accessible from Kiali (Deployment.AccessibleNamespaces)
diff --git a/business/mesh.go b/business/mesh.go
index 476c47aca..0b94ef923 100644
--- a/business/mesh.go
+++ b/business/mesh.go
@@ -52,7 +52,14 @@ func (in *MeshService) IsValidCluster(cluster string) bool {
 	return exists
 }
 
-// GetMeshConfig returns the home cluster's mesh config.
+// IsControlPlane is just a convenience method that calls MeshDiscovery.IsControlPlane()
+func (in *MeshService) IsControlPlane(ctx context.Context, cluster, namespace string) bool {
+	return in.discovery.IsControlPlane(ctx, cluster, namespace)
+}
+
+// This returns the home cluster's mesh config, if possible. For external
+// Kiali deployments it returns the mesh config of a random control plane.
+// If no mesh config can be found, an empty mesh config is returned.
 // TODO: Remove when validations can read from a specific controlplane.
 func (in *MeshService) GetMeshConfig() *models.MeshConfig {
 	mesh, err := in.discovery.Mesh(context.TODO())
@@ -67,8 +74,9 @@ func (in *MeshService) GetMeshConfig() *models.MeshConfig {
 			return controlPlane.MeshConfig
 		}
 	}
+	if len(mesh.ControlPlanes) > 0 {
+		return mesh.ControlPlanes[0].MeshConfig
+	}
 
-	// This should not happen
-	log.Warningf("No Kiali Home cluster found while getting mesh config")
 	return &models.MeshConfig{MeshConfig: &istiov1alpha1.MeshConfig{}}
 }
diff --git a/business/namespaces.go b/business/namespaces.go
index 6e99086b8..2827716b6 100644
--- a/business/namespaces.go
+++ b/business/namespaces.go
@@ -216,7 +216,7 @@ func (in *NamespaceService) getNamespacesByCluster(ctx context.Context, cluster
 		}
 	}
 
-	namespaces = istio.FilterNamespacesWithDiscoverySelectors(namespaces, istio.GetDiscoverySelectorsForCluster(cluster, in.conf))
+	namespaces = istio.FilterNamespacesWithDiscoverySelectors(namespaces, istio.GetDiscoverySelectorsForCluster(ctx, in.discovery, cluster, in.conf))
 
 	return namespaces, nil
 }
@@ -290,7 +290,7 @@ func (in *NamespaceService) GetClusterNamespace(ctx context.Context, namespace s
 		result = models.CastNamespace(*ns, cluster)
 	}
 
-	if !in.isAccessibleNamespace(result) {
+	if !in.isAccessibleNamespace(ctx, result) {
 		return nil, &AccessibleNamespaceError{msg: "Namespace [" + namespace + "] in cluster [" + cluster + "] is not accessible to Kiali"}
 	}
 
@@ -386,8 +386,34 @@ func (in *NamespaceService) getNamespacesUsingKialiSA(cluster string, labelSelec
 // isAccessibleNamespace will look at the discovery selectors and see if the namespace is allowed to be accessed.
 // This ignores cluster-wide-access mode since we can have discovery selectors even when given cluster wide access.
 // Also, this may be asking for the accessibility of a namespace in a remote cluster, in which case cluster-wide-access is moot.
-func (in *NamespaceService) isAccessibleNamespace(namespace models.Namespace) bool {
-	selectors := istio.GetDiscoverySelectorsForCluster(namespace.Cluster, in.conf)
+func (in *NamespaceService) isAccessibleNamespace(ctx context.Context, namespace models.Namespace) bool {
+	selectors := istio.GetDiscoverySelectorsForCluster(ctx, in.discovery, namespace.Cluster, in.conf)
 	// see if the discovery selectors match the one namespace we are checking
 	return len(istio.FilterNamespacesWithDiscoverySelectors([]models.Namespace{namespace}, selectors)) == 1
 }
+
+// HasMeshAccess expects a request context and returns true if the the context can access
+// any control plane namespace. The test is narrowed to the cluster, if provided.
+func (in *NamespaceService) HasMeshAccess(ctx context.Context, cluster string) bool {
+	var namespaces []models.Namespace
+	var err error
+	if cluster == "" {
+		namespaces, err = in.GetNamespaces(ctx)
+	} else {
+		namespaces, err = in.GetClusterNamespaces(ctx, cluster)
+	}
+	if err != nil {
+		log.Errorf("failed HasMeshAccess: %s", err)
+		return false
+	}
+
+	for _, cpNamespace := range in.discovery.GetControlPlaneNamespaces(ctx, cluster) {
+		for _, ns := range namespaces {
+			if cpNamespace == ns.Name && (cluster == "" || cluster == ns.Cluster) {
+				return true
+			}
+		}
+	}
+
+	return false
+}
diff --git a/business/services.go b/business/services.go
index 3458ba79d..2469caa8f 100644
--- a/business/services.go
+++ b/business/services.go
@@ -179,7 +179,7 @@ func (in *SvcService) getServiceListForCluster(ctx context.Context, criteria Ser
 		}
 		istioConfigs, err := in.businessLayer.IstioConfig.GetIstioConfigList(ctx, cluster, istioCriteria)
 		if err != nil {
-			log.Errorf("Error fetching IstioConfigList per cluster %s per namespace %s: %s", cluster, criteria.Namespace, err)
+			log.Errorf("Error fetching IstioConfigList per cluster [%s] per namespace [%s]: %s", cluster, criteria.Namespace, err)
 			return nil, err
 		}
 		istioConfigList = *istioConfigs
@@ -261,7 +261,7 @@ func (in *SvcService) buildKubernetesServices(svcs []core_v1.Service, pods []cor
 			/** Check if Service has istioSidecar deployed */
 			// TODO: This won't work if pods are scaled to zero.
 			mPods := models.Pods{}
-			mPods.Parse(sPods)
+			mPods.Parse(sPods, in.businessLayer.Mesh.IsControlPlane)
 			hasSidecar = mPods.HasAnyIstioSidecar()
 			hasAmbient = mPods.HasAnyAmbient()
 			svcVirtualServices := kubernetes.FilterAutogeneratedVirtualServices(kubernetes.FilterVirtualServicesByService(istioConfigList.VirtualServices, item.Namespace, item.Name, in.conf))
@@ -619,7 +619,7 @@ func (in *SvcService) GetServiceDetails(ctx context.Context, cluster, namespace,
 
 	s := models.ServiceDetails{Workloads: wo, Health: hth, NamespaceMTLS: nsmtls, SubServices: serviceOverviews}
 	s.Service = svc
-	s.SetPods(pods)
+	s.SetPods(pods, in.businessLayer.Mesh.IsControlPlane)
 	s.Endpoints = *models.GetEndpointsFromPods(pods)
 	// ServiceDetail will consider if the Service is a External/Federation entry
 	if s.Service.Type == "External" || s.Service.Type == "Federation" {
@@ -877,11 +877,7 @@ func (in *SvcService) GetService(ctx context.Context, cluster, namespace, servic
 }
 
 func (in *SvcService) getServiceValidations(services []core_v1.Service, deployments []apps_v1.Deployment, pods []core_v1.Pod) models.IstioValidations {
-	validations := checkers.ServiceChecker{
-		Services:    services,
-		Deployments: deployments,
-		Pods:        pods,
-	}.Check()
+	validations := checkers.NewServiceChecker("", deployments, in.businessLayer.Mesh.discovery, pods, services).Check()
 
 	return validations
 }
diff --git a/business/workloads.go b/business/workloads.go
index b62d43b43..50555e54b 100644
--- a/business/workloads.go
+++ b/business/workloads.go
@@ -547,7 +547,7 @@ func (in *WorkloadService) GetPod(cluster, namespace, name string) (*models.Pod,
 	}
 
 	pod := models.Pod{}
-	pod.Parse(p)
+	pod.Parse(p, in.businessLayer.Mesh.discovery.IsControlPlane)
 	return &pod, nil
 }
 
@@ -824,6 +824,8 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 
 	ws := models.Workloads{}
 
+	includeIstioWorkloads := cluster != in.conf.KubernetesConfig.ClusterName || !in.conf.Clustering.IgnoreHomeCluster
+
 	// Check if user has access to the namespace (RBAC) in cache scenarios and/or
 	// if namespace is accessible from Kiali (Deployment.AccessibleNamespaces)
 	if _, err := in.businessLayer.Namespace.GetClusterNamespace(ctx, namespace, cluster); err != nil {
@@ -852,19 +854,19 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 
 	podList := &core_v1.PodList{}
 	if err := kubeCache.List(ctx, podList, inNamespaceOpt, selectorOpt); err != nil {
-		return nil, fmt.Errorf("Error fetching Pods per namespace %s: %s", namespace, err)
+		return nil, fmt.Errorf("Error fetching Pods per namespace [%s][%s]: %s", cluster, namespace, err)
 	}
 	pods = podList.Items
 
 	depList := &apps_v1.DeploymentList{}
 	if err := kubeCache.List(ctx, depList, inNamespaceOpt); err != nil {
-		return nil, fmt.Errorf("Error fetching Deployments per namespace %s: %s", namespace, err)
+		return nil, fmt.Errorf("Error fetching Deployments per namespace [%s][%s]: %s", cluster, namespace, err)
 	}
 	dep = depList.Items
 
 	repList := &apps_v1.ReplicaSetList{}
 	if err := kubeCache.List(ctx, repList, inNamespaceOpt); err != nil {
-		return nil, fmt.Errorf("Error fetching ReplicaSets per namespace %s: %s", namespace, err)
+		return nil, fmt.Errorf("Error fetching ReplicaSets per namespace [%s][%s]: %s", cluster, namespace, err)
 	}
 	repset = repList.Items
 
@@ -878,7 +880,7 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 			// No Cache for ReplicationControllers
 			repcon, err = client.GetReplicationControllers(namespace)
 			if err != nil {
-				log.Errorf("Error fetching GetReplicationControllers per namespace %s: %s", namespace, err)
+				log.Errorf("Error fetching GetReplicationControllers per namespace [%s][%s]: %s", cluster, namespace, err)
 				errChan <- err
 			}
 		}
@@ -894,7 +896,7 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 			// No cache for DeploymentConfigs
 			depcon, err = client.GetDeploymentConfigs(ctx, namespace)
 			if err != nil {
-				log.Errorf("Error fetching DeploymentConfigs per namespace %s: %s", namespace, err)
+				log.Errorf("Error fetching DeploymentConfigs per namespace [%s][%s]: %s", cluster, namespace, err)
 				errChan <- err
 			}
 		}
@@ -904,7 +906,7 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 	if in.isWorkloadIncluded(kubernetes.StatefulSetType) {
 		setList := &apps_v1.StatefulSetList{}
 		if err := kubeCache.List(ctx, setList, inNamespaceOpt); err != nil {
-			return nil, fmt.Errorf("Error fetching StatefulSets per namespace %s: %s", namespace, err)
+			return nil, fmt.Errorf("Error fetching StatefulSets per namespace [%s][%s]: %s", cluster, namespace, err)
 		}
 		fulset = setList.Items
 	}
@@ -919,7 +921,7 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 			// No cache for Cronjobs
 			conjbs, err = client.GetCronJobs(namespace)
 			if err != nil {
-				log.Errorf("Error fetching CronJobs per namespace %s: %s", namespace, err)
+				log.Errorf("Error fetching CronJobs per namespace [%s][%s]: %s", cluster, namespace, err)
 				errChan <- err
 			}
 		}
@@ -935,7 +937,7 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 			// No cache for Jobs
 			jbs, err = client.GetJobs(namespace)
 			if err != nil {
-				log.Errorf("Error fetching Jobs per namespace %s: %s", namespace, err)
+				log.Errorf("Error fetching Jobs per namespace [%s][%s]: %s", cluster, namespace, err)
 				errChan <- err
 			}
 		}
@@ -945,34 +947,34 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 	if in.isWorkloadIncluded(kubernetes.DaemonSetType) {
 		daeList := &apps_v1.DaemonSetList{}
 		if err := kubeCache.List(ctx, daeList, inNamespaceOpt); err != nil {
-			return nil, fmt.Errorf("Error fetching DaemonSets per namespace %s: %s", namespace, err)
+			return nil, fmt.Errorf("Error fetching DaemonSets per namespace [%s][%s]: %s", cluster, namespace, err)
 		}
 		daeset = daeList.Items
 	}
 
 	// WorkloadGroups are fetched only when included
-	if in.isWorkloadIncluded(kubernetes.WorkloadGroupType) {
+	if includeIstioWorkloads && in.isWorkloadIncluded(kubernetes.WorkloadGroupType) {
 		wgroupList := &networking_v1.WorkloadGroupList{}
 		if err := kubeCache.List(ctx, wgroupList, inNamespaceOpt); err != nil {
-			return nil, fmt.Errorf("Error fetching WorkloadGroups per namespace %s: %s", namespace, err)
+			return nil, fmt.Errorf("Error fetching WorkloadGroups per namespace [%s][%s]: %s", cluster, namespace, err)
 		}
 		wgroups = wgroupList.Items
 	}
 
 	// WorkloadEntries are fetched only when included
-	if in.isWorkloadIncluded(kubernetes.WorkloadEntryType) {
+	if includeIstioWorkloads && in.isWorkloadIncluded(kubernetes.WorkloadEntryType) {
 		wentryList := &networking_v1.WorkloadEntryList{}
 		if err := kubeCache.List(ctx, wentryList, inNamespaceOpt); err != nil {
-			return nil, fmt.Errorf("Error fetching WorkloadEntries per namespace %s: %s", namespace, err)
+			return nil, fmt.Errorf("Error fetching WorkloadEntries per namespace [%s][%s]: %s", cluster, namespace, err)
 		}
 		wentries = wentryList.Items
 	}
 
 	// Sidecars are fetched only when included
-	if in.isWorkloadIncluded(kubernetes.SidecarType) {
+	if includeIstioWorkloads && in.isWorkloadIncluded(kubernetes.SidecarType) {
 		sidecarList := &networking_v1.SidecarList{}
 		if err := kubeCache.List(ctx, sidecarList, inNamespaceOpt); err != nil {
-			return nil, fmt.Errorf("Error fetching Sidecars per namespace %s: %s", namespace, err)
+			return nil, fmt.Errorf("Error fetching Sidecars per namespace [%s][%s]: %s", cluster, namespace, err)
 		}
 		sidecars = sidecarList.Items
 	}
@@ -1223,7 +1225,7 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 			}
 			if found {
 				selector := labels.Set(dep[iFound].Spec.Template.Labels).AsSelector()
-				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods))
+				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods), in.businessLayer.Mesh.IsControlPlane)
 				w.ParseDeployment(&dep[iFound])
 			} else {
 				log.Errorf("Workload %s is not found as Deployment", controllerName)
@@ -1241,7 +1243,7 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 			}
 			if found {
 				selector := labels.Set(repset[iFound].Spec.Template.Labels).AsSelector()
-				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods))
+				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods), in.businessLayer.Mesh.IsControlPlane)
 				w.ParseReplicaSet(&repset[iFound])
 			} else {
 				log.Errorf("Workload %s is not found as ReplicaSet", controllerName)
@@ -1259,7 +1261,7 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 			}
 			if found {
 				selector := labels.Set(repcon[iFound].Spec.Template.Labels).AsSelector()
-				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods))
+				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods), in.businessLayer.Mesh.IsControlPlane)
 				w.ParseReplicationController(&repcon[iFound])
 			} else {
 				log.Errorf("Workload %s is not found as ReplicationController", controllerName)
@@ -1277,7 +1279,7 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 			}
 			if found {
 				selector := labels.Set(depcon[iFound].Spec.Template.Labels).AsSelector()
-				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods))
+				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods), in.businessLayer.Mesh.IsControlPlane)
 				w.ParseDeploymentConfig(&depcon[iFound])
 			} else {
 				log.Errorf("Workload %s is not found as DeploymentConfig", controllerName)
@@ -1295,7 +1297,7 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 			}
 			if found {
 				selector := labels.Set(fulset[iFound].Spec.Template.Labels).AsSelector()
-				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods))
+				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods), in.businessLayer.Mesh.IsControlPlane)
 				w.ParseStatefulSet(&fulset[iFound])
 			} else {
 				log.Errorf("Workload %s is not found as StatefulSet", controllerName)
@@ -1312,7 +1314,7 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 				}
 			}
 			if found {
-				w.SetPods([]core_v1.Pod{pods[iFound]})
+				w.SetPods([]core_v1.Pod{pods[iFound]}, in.businessLayer.Mesh.IsControlPlane)
 				w.ParsePod(&pods[iFound])
 			} else {
 				log.Errorf("Workload %s is not found as Pod", controllerName)
@@ -1330,7 +1332,7 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 			}
 			if found {
 				selector := labels.Set(jbs[iFound].Spec.Template.Labels).AsSelector()
-				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods))
+				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods), in.businessLayer.Mesh.IsControlPlane)
 				w.ParseJob(&jbs[iFound])
 			} else {
 				log.Errorf("Workload %s is not found as Job", controllerName)
@@ -1348,7 +1350,7 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 			}
 			if found {
 				selector := labels.Set(conjbs[iFound].Spec.JobTemplate.Spec.Template.Labels).AsSelector()
-				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods))
+				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods), in.businessLayer.Mesh.IsControlPlane)
 				w.ParseCronJob(&conjbs[iFound])
 			} else {
 				log.Warningf("Workload %s is not found as CronJob (CronJob could be deleted but children are still in the namespace)", controllerName)
@@ -1366,7 +1368,7 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 			}
 			if found {
 				selector := labels.Set(daeset[iFound].Spec.Template.Labels).AsSelector()
-				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods))
+				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods), in.businessLayer.Mesh.IsControlPlane)
 				w.ParseDaemonSet(&daeset[iFound])
 			} else {
 				log.Errorf("Workload %s is not found as DaemonSet", controllerName)
@@ -1423,7 +1425,7 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 					log.Debugf("Workload %s of type %s has not a ReplicaSet as a child controller, it may need a revisit", controllerName, controllerGVK.Kind)
 				}
 			}
-			w.SetPods(cPods)
+			w.SetPods(cPods, in.businessLayer.Mesh.IsControlPlane)
 		}
 
 		if cnFound {
@@ -1898,7 +1900,7 @@ func (in *WorkloadService) fetchWorkload(ctx context.Context, criteria WorkloadC
 		case kubernetes.Deployments:
 			if dep != nil && dep.Name == criteria.WorkloadName {
 				selector := labels.Set(dep.Spec.Template.Labels).AsSelector()
-				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods))
+				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods), in.businessLayer.Mesh.IsControlPlane)
 				w.ParseDeployment(dep)
 			} else {
 				log.Errorf("Workload %s is not found as Deployment", criteria.WorkloadName)
@@ -1916,7 +1918,7 @@ func (in *WorkloadService) fetchWorkload(ctx context.Context, criteria WorkloadC
 			}
 			if found {
 				selector := labels.Set(repset[iFound].Spec.Template.Labels).AsSelector()
-				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods))
+				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods), in.businessLayer.Mesh.IsControlPlane)
 				w.ParseReplicaSet(&repset[iFound])
 			} else {
 				log.Errorf("Workload %s is not found as ReplicaSet", criteria.WorkloadName)
@@ -1934,7 +1936,7 @@ func (in *WorkloadService) fetchWorkload(ctx context.Context, criteria WorkloadC
 			}
 			if found {
 				selector := labels.Set(repcon[iFound].Spec.Template.Labels).AsSelector()
-				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods))
+				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods), in.businessLayer.Mesh.IsControlPlane)
 				w.ParseReplicationController(&repcon[iFound])
 			} else {
 				log.Errorf("Workload %s is not found as ReplicationController", criteria.WorkloadName)
@@ -1943,7 +1945,7 @@ func (in *WorkloadService) fetchWorkload(ctx context.Context, criteria WorkloadC
 		case kubernetes.DeploymentConfigs:
 			if depcon != nil && depcon.Name == criteria.WorkloadName {
 				selector := labels.Set(depcon.Spec.Template.Labels).AsSelector()
-				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods))
+				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods), in.businessLayer.Mesh.IsControlPlane)
 				w.ParseDeploymentConfig(depcon)
 			} else {
 				log.Errorf("Workload %s is not found as DeploymentConfig", criteria.WorkloadName)
@@ -1952,7 +1954,7 @@ func (in *WorkloadService) fetchWorkload(ctx context.Context, criteria WorkloadC
 		case kubernetes.StatefulSets:
 			if fulset != nil && fulset.Name == criteria.WorkloadName {
 				selector := labels.Set(fulset.Spec.Template.Labels).AsSelector()
-				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods))
+				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods), in.businessLayer.Mesh.IsControlPlane)
 				w.ParseStatefulSet(fulset)
 			} else {
 				log.Errorf("Workload %s is not found as StatefulSet", criteria.WorkloadName)
@@ -1969,7 +1971,7 @@ func (in *WorkloadService) fetchWorkload(ctx context.Context, criteria WorkloadC
 				}
 			}
 			if found {
-				w.SetPods([]core_v1.Pod{pods[iFound]})
+				w.SetPods([]core_v1.Pod{pods[iFound]}, in.businessLayer.Mesh.IsControlPlane)
 				w.ParsePod(&pods[iFound])
 			} else {
 				log.Errorf("Workload %s is not found as Pod", criteria.WorkloadName)
@@ -1987,7 +1989,7 @@ func (in *WorkloadService) fetchWorkload(ctx context.Context, criteria WorkloadC
 			}
 			if found {
 				selector := labels.Set(jbs[iFound].Spec.Template.Labels).AsSelector()
-				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods))
+				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods), in.businessLayer.Mesh.IsControlPlane)
 				w.ParseJob(&jbs[iFound])
 			} else {
 				log.Errorf("Workload %s is not found as Job", criteria.WorkloadName)
@@ -2005,7 +2007,7 @@ func (in *WorkloadService) fetchWorkload(ctx context.Context, criteria WorkloadC
 			}
 			if found {
 				selector := labels.Set(conjbs[iFound].Spec.JobTemplate.Spec.Template.Labels).AsSelector()
-				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods))
+				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods), in.businessLayer.Mesh.IsControlPlane)
 				w.ParseCronJob(&conjbs[iFound])
 			} else {
 				log.Warningf("Workload %s is not found as CronJob (CronJob could be deleted but children are still in the namespace)", criteria.WorkloadName)
@@ -2014,7 +2016,7 @@ func (in *WorkloadService) fetchWorkload(ctx context.Context, criteria WorkloadC
 		case kubernetes.DaemonSets:
 			if ds != nil && ds.Name == criteria.WorkloadName {
 				selector := labels.Set(ds.Spec.Template.Labels).AsSelector()
-				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods))
+				w.SetPods(kubernetes.FilterPodsBySelector(selector, pods), in.businessLayer.Mesh.IsControlPlane)
 				w.ParseDaemonSet(ds)
 			} else {
 				log.Errorf("Workload %s is not found as DaemonSet", criteria.WorkloadName)
@@ -2062,7 +2064,7 @@ func (in *WorkloadService) fetchWorkload(ctx context.Context, criteria WorkloadC
 				}
 			}
 
-			w.SetPods(cPods)
+			w.SetPods(cPods, in.businessLayer.Mesh.IsControlPlane)
 		}
 
 		w.WorkloadListItem.IsGateway = w.IsGateway()
diff --git a/cache/ztunnel_dump.go b/cache/ztunnel_dump.go
index 5f3114b2b..b41a3039b 100644
--- a/cache/ztunnel_dump.go
+++ b/cache/ztunnel_dump.go
@@ -39,7 +39,7 @@ func (c *kialiCacheImpl) GetZtunnelDump(cluster, namespace, pod string) *kuberne
 	zl := c.zl
 	client, found := c.clients[cluster]
 	if !found {
-		zl.Error().Msgf("[GetZtunnelDump] Kiali Service Account client not found for cluster %s", cluster)
+		zl.Error().Msgf("[GetZtunnelDump] Kiali Service Account client not found for cluster [%s]", cluster)
 		return nil
 	}
 
diff --git a/config/config.go b/config/config.go
index 0f21cc45e..c12694db0 100644
--- a/config/config.go
+++ b/config/config.go
@@ -60,6 +60,7 @@ const (
 
 const (
 	IstioMultiClusterHostSuffix = "global"
+	IstioNamespaceDefault       = "istio-system"
 	OidcClientSecretFile        = "/kiali-secret/oidc-secret"
 )
 
@@ -318,20 +319,27 @@ type RegistryConfig struct {
 	// TODO: Support auth options
 }
 
-// IstioConfig describes configuration used for istio links
+// IstioConfig describes configuration used for istio links.
+// IMPORTANT: Values set here MUST apply to ALL Istio control planes being monitored by the Kiali
+//
+//	instance. Otherwise Kiali will do it's best to auto-detect differences.
+//
+// TODO: Go through this list and remove anything that requires auto-detection or does not make sense for
+//
+//	a multi-control-plane deployment.
 type IstioConfig struct {
-	ComponentStatuses                 ComponentStatuses `yaml:"component_status,omitempty" json:"componentStatuses,omitempty"`
-	ConfigMapName                     string            `yaml:"config_map_name,omitempty" json:"configMapName,omitempty"`
-	EnvoyAdminLocalPort               int               `yaml:"envoy_admin_local_port,omitempty" json:"envoyAdminLocalPort,omitempty"`
-	GatewayAPIClasses                 []GatewayAPIClass `yaml:"gateway_api_classes,omitempty" json:"gatewayApiClasses,omitempty"`
-	GatewayAPIClassesLabelSelector    string            `yaml:"gateway_api_classes_label_selector,omitempty" json:"gatewayApiClassesLabelSelector,omitempty"`
-	IstioAPIEnabled                   bool              `yaml:"istio_api_enabled" json:"istioApiEnabled"`
-	IstioIdentityDomain               string            `yaml:"istio_identity_domain,omitempty" json:"istioIdentityDomain,omitempty"`
-	IstioInjectionAnnotation          string            `yaml:"istio_injection_annotation,omitempty" json:"istioInjectionAnnotation,omitempty"`
-	IstioSidecarInjectorConfigMapName string            `yaml:"istio_sidecar_injector_config_map_name,omitempty" json:"istioSidecarInjectorConfigMapName,omitempty"`
-	IstioSidecarAnnotation            string            `yaml:"istio_sidecar_annotation,omitempty" json:"istioSidecarAnnotation,omitempty"`
-	IstiodDeploymentName              string            `yaml:"istiod_deployment_name,omitempty" json:"istiodDeploymentName,omitempty"`
-	IstiodPodMonitoringPort           int               `yaml:"istiod_pod_monitoring_port,omitempty" json:"istiodPodMonitoringPort,omitempty"`
+	ComponentStatuses                 ComponentStatuses `yaml:"component_status,omitempty"`
+	ConfigMapName                     string            `yaml:"config_map_name,omitempty"`
+	EnvoyAdminLocalPort               int               `yaml:"envoy_admin_local_port,omitempty"`
+	GatewayAPIClasses                 []GatewayAPIClass `yaml:"gateway_api_classes,omitempty"`
+	GatewayAPIClassesLabelSelector    string            `yaml:"gateway_api_classes_label_selector,omitempty"`
+	IstioAPIEnabled                   bool              `yaml:"istio_api_enabled"`
+	IstioIdentityDomain               string            `yaml:"istio_identity_domain,omitempty"`
+	IstioInjectionAnnotation          string            `yaml:"istio_injection_annotation,omitempty"`
+	IstioSidecarInjectorConfigMapName string            `yaml:"istio_sidecar_injector_config_map_name,omitempty"`
+	IstioSidecarAnnotation            string            `yaml:"istio_sidecar_annotation,omitempty"`
+	IstiodDeploymentName              string            `yaml:"istiod_deployment_name,omitempty"`
+	IstiodPodMonitoringPort           int               `yaml:"istiod_pod_monitoring_port,omitempty"`
 	// IstiodPollingIntervalSeconds is how often in seconds Kiali will poll istiod(s) for
 	// proxy status and registry services. Polling is not performed if IstioAPIEnabled is false.
 	IstiodPollingIntervalSeconds     int             `yaml:"istiod_polling_interval_seconds,omitempty" json:"istiodPollingIntervalSeconds,omitempty"`
@@ -485,10 +493,6 @@ type DeploymentConfig struct {
 	InstanceName         string                   `yaml:"instance_name"`
 	Namespace            string                   `yaml:"namespace,omitempty"` // Kiali deployment namespace
 	ViewOnlyMode         bool                     `yaml:"view_only_mode,omitempty"`
-	// RemoteSecretPath is used to identify the remote cluster Kiali will connect to as its "local cluster".
-	// This is to support installing Kiali in the control plane, but observing only the data plane in the remote cluster.
-	// Experimental feature. See: https://github.com/kiali/kiali/issues/3002
-	RemoteSecretPath string `yaml:"remote_secret_path,omitempty"`
 }
 
 // we need to play games with a custom unmarshaller/marshaller for metav1.LabelSelector because it has no yaml struct tags so
@@ -631,8 +635,9 @@ type Clustering struct {
 	// Clusters is a list of clusters that cannot be autodetected by the Kiali Server.
 	// Remote clusters are specified here if ‘autodetect_secrets.enabled’ is false or
 	// if the Kiali Server does not have access to the remote cluster’s secret.
-	Clusters  []Cluster  `yaml:"clusters" json:"clusters"`
-	KialiURLs []KialiURL `yaml:"kiali_urls" json:"kiali_urls"`
+	Clusters          []Cluster  `yaml:"clusters" json:"clusters"`
+	IgnoreHomeCluster bool       `yaml:"ignore_home_cluster" json:"ignoreHomeCluster"`
+	KialiURLs         []KialiURL `yaml:"kiali_urls" json:"kialiUrls"`
 }
 
 // IsZero implements: https://pkg.go.dev/gopkg.in/yaml.v2#IsZeroer so that
@@ -703,10 +708,8 @@ type Config struct {
 	ExternalServices         ExternalServices                    `yaml:"external_services,omitempty"`
 	HealthConfig             HealthConfig                        `yaml:"health_config,omitempty" json:"healthConfig,omitempty"`
 	Identity                 security.Identity                   `yaml:",omitempty"`
-	InCluster                bool                                `yaml:"in_cluster,omitempty"`
 	InstallationTag          string                              `yaml:"installation_tag,omitempty"`
 	IstioLabels              IstioLabels                         `yaml:"istio_labels,omitempty"`
-	IstioNamespace           string                              `yaml:"istio_namespace,omitempty"` // default component namespace
 	KialiFeatureFlags        KialiFeatureFlags                   `yaml:"kiali_feature_flags,omitempty"`
 	KialiInternal            KialiInternalConfig                 `yaml:"kiali_internal,omitempty"`
 	KubernetesConfig         KubernetesConfig                    `yaml:"kubernetes_config,omitempty"`
@@ -717,8 +720,6 @@ type Config struct {
 // NewConfig creates a default Config struct
 func NewConfig() (c *Config) {
 	c = &Config{
-		InCluster:      true,
-		IstioNamespace: "istio-system",
 		Auth: AuthConfig{
 			Strategy: AuthStrategyToken,
 			OpenId: OpenIdConfig{
@@ -741,14 +742,16 @@ func NewConfig() (c *Config) {
 				InsecureSkipVerifyTLS: false,
 			},
 		},
+		Clustering: Clustering{
+			IgnoreHomeCluster: false,
+		},
 		CustomDashboards: dashboards.GetBuiltInMonitoringDashboards(),
 		Deployment: DeploymentConfig{
 			certPool:           x509.NewCertPool(),
 			ClusterWideAccess:  true,
 			DiscoverySelectors: DiscoverySelectorsConfig{Default: nil, Overrides: nil},
 			InstanceName:       "kiali",
-			Namespace:          "istio-system",
-			RemoteSecretPath:   "/kiali-remote-secret/kiali",
+			Namespace:          IstioNamespaceDefault,
 			ViewOnlyMode:       false,
 		},
 		ExternalServices: ExternalServices{
@@ -790,7 +793,7 @@ func NewConfig() (c *Config) {
 				IstiodDeploymentName:              "",
 				IstiodPodMonitoringPort:           15014,
 				IstiodPollingIntervalSeconds:      20,
-				RootNamespace:                     "istio-system",
+				RootNamespace:                     IstioNamespaceDefault,
 				UrlServiceVersion:                 "",
 				ValidationChangeDetectionEnabled:  true,
 				ValidationReconcileInterval:       util.AsPtr(time.Minute),
@@ -1346,11 +1349,6 @@ func SaveToFile(filename string, conf *Config) (err error) {
 	return
 }
 
-// IsIstioNamespace returns true if the namespace is the default istio namespace
-func IsIstioNamespace(namespace string) bool {
-	return namespace == configuration.IstioNamespace
-}
-
 // IsRootNamespace returns true if the namespace is the root namespace
 func IsRootNamespace(namespace string) bool {
 	return namespace == configuration.ExternalServices.Istio.RootNamespace
@@ -1457,7 +1455,7 @@ func (conf Config) IsValidationsEnabled() bool {
 
 // Validate will ensure the config is valid. This should be called after the config
 // is initialized and before the config is used.
-func Validate(conf Config) error {
+func Validate(conf *Config) error {
 	if conf.Server.Port < 0 {
 		return fmt.Errorf("server port is negative: %v", conf.Server.Port)
 	}
diff --git a/controller/validations.go b/controller/validations.go
index 360667214..fa890e495 100644
--- a/controller/validations.go
+++ b/controller/validations.go
@@ -140,7 +140,7 @@ func (r *ValidationsReconciler) Reconcile(ctx context.Context, req ctrl.Request)
 	for _, cluster := range r.clusters {
 		validationPerformed, clusterValidations, err := r.validationsService.Validate(ctx, cluster, vInfo)
 		if err != nil {
-			log.Errorf("[ValidationsReconciler] Error performing validation for cluster %s: %s", cluster, err)
+			log.Errorf("[ValidationsReconciler] Error performing validation for cluster [%s]: %s", cluster, err)
 			return ctrl.Result{}, err
 		}
 
diff --git a/frontend/cypress/integration/featureFiles/overview.feature b/frontend/cypress/integration/featureFiles/overview.feature
index d2b474447..66df103de 100644
--- a/frontend/cypress/integration/featureFiles/overview.feature
+++ b/frontend/cypress/integration/featureFiles/overview.feature
@@ -114,12 +114,19 @@ Feature: Kiali Overview page
   Scenario: The badge for local cluster should be visible
     Then user sees the "east" cluster badge in the Kiali header
 
-  @multi-cluster
-  Scenario: Istio panels for both clusters should be visible and have the control panel label
+  @multi-primary
+  Scenario: Istio panels for both clusters should be visible and have the control plane label
     Then user sees the "istio-system" namespace card in cluster "east"
     And user sees the "istio-system" namespace card in cluster "west"
     And user sees the "Control plane" label in both "istio-system" namespace cards
     And the toggle on the right side of both "istio-system" namespace cards exists
+  
+  @multi-cluster
+  Scenario: Istio panel for east cluster should be visible and have the control plane label
+    Then user sees the "istio-system" namespace card in cluster "east"
+    And user sees the "istio-system" namespace card in cluster "west"
+    And user sees the "Control plane" label in the "east" "istio-system" namespace card
+    And the toggle on the right side of both "istio-system" namespace cards exists
     And Istio config should not be available for the "west" "istio-system"
     And health should be different for "east" and "west" "istio-system"
 
@@ -167,7 +174,6 @@ Feature: Kiali Overview page
     And user sorts by column "Cluster" desc
     Then the list is sorted by "Cluster" desc
 
-  @multi-cluster
   @multi-primary
   Scenario: There should be two control plane cards for each cluster
     Then user sees the "Control plane" label in the "east" "istio-system" namespace card
diff --git a/frontend/package.json b/frontend/package.json
index 121ce17e9..826677bf6 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -38,7 +38,7 @@
     "build:dev": "sh -ac '. ./.env.upstream; npm run lint && npm run build:kiali'",
     "build:kiali": "npm run i18n && REACT_APP_VERSION=$npm_package_version REACT_APP_NAME=$npm_package_name GENERATE_SOURCEMAP=false EXTEND_ESLINT=true react-scripts build --profile",
     "build:prod": "sh -ac '. ./.env.downstream; npm run build:kiali'",
-    "cypress": "cypress open -e TAGS=\"not @multi-cluster and not @ambient and not @waypoint and (not @waypoint-tracing or @tracing)\"",
+    "cypress": "cypress open -e TAGS=\"not @multi-cluster and not @multi-primary and not @ambient and not @waypoint and (not @waypoint-tracing or @tracing)\"",
     "cypress:ambient": "cypress open -e TAGS=\"@ambient\"",
     "cypress:waypoint": "cypress open -e TAGS=\"@waypoint or @waypoint-tracing\"",
     "cypress:combine:reports": "jrm cypress/results/combined-report.xml \"cypress/results/*.xml\"",
@@ -46,10 +46,10 @@
     "cypress:multi-cluster": "cypress open -e TAGS=\"@multi-cluster and not @multi-primary\"",
     "cypress:multi-primary": "cypress open -e TAGS=\"@multi-cluster and @multi-primary\"",
     "cypress:perf": "cypress open --config-file cypress-perf.config.ts",
-    "cypress:run": "cypress run -e TAGS=\"@smoke\" && cypress run -e TAGS=\"not @crd-validation and not @multi-cluster and not @smoke and not @ambient and not @waypoint and (not @waypoint-tracing or @tracing)\" && cypress run -e TAGS=\"@crd-validation\"",
+    "cypress:run": "cypress run -e TAGS=\"@smoke\" && cypress run -e TAGS=\"not @crd-validation and not @multi-cluster and not @multi-primary and not @smoke and not @ambient and not @waypoint and (not @waypoint-tracing or @tracing)\" && cypress run -e TAGS=\"@crd-validation\"",
     "cypress:run:ambient": "cypress run -e TAGS=\"@ambient\" && cypress run -e TAGS=\"@waypoint\" && cypress run -e TAGS=\"@waypoint-tracing\"",
     "cypress:run:ambient123": "cypress run -e TAGS=\"@ambient\" && cypress run -e TAGS=\"@waypoint and not @skip-istio-1-23\" && cypress run -e TAGS=\"@waypoint-tracing\"",
-    "cypress:run:junit": "cypress run --reporter cypress-multi-reporters --reporter-options configFile=reporter-config.json -e TAGS=\"not @crd-validation and not @multi-cluster and not @ambient and not @waypoint and (not @waypoint-tracing or @tracing)\" && cypress run --reporter cypress-multi-reporters --reporter-options configFile=reporter-config.json -e TAGS=\"@crd-validation\"",
+    "cypress:run:junit": "cypress run --reporter cypress-multi-reporters --reporter-options configFile=reporter-config.json -e TAGS=\"not @crd-validation and not @multi-cluster and not @multi-primary and not @ambient and not @waypoint and (not @waypoint-tracing or @tracing)\" && cypress run --reporter cypress-multi-reporters --reporter-options configFile=reporter-config.json -e TAGS=\"@crd-validation\"",
     "cypress:run:multi-cluster": "cypress run -e TAGS=\"@multi-cluster and not @multi-primary\"",
     "cypress:run:multi-primary": "cypress run -e TAGS=\"@multi-cluster and @multi-primary\"",
     "cypress:run:perf": "cypress run --config-file cypress-perf.config.ts",
diff --git a/frontend/src/components/IstioStatus/IstioComponentStatus.tsx b/frontend/src/components/IstioStatus/IstioComponentStatus.tsx
index 5dd40f46f..da7583c4b 100644
--- a/frontend/src/components/IstioStatus/IstioComponentStatus.tsx
+++ b/frontend/src/components/IstioStatus/IstioComponentStatus.tsx
@@ -70,7 +70,9 @@ export const IstioComponentStatus: React.FC<Props> = (props: Props) => {
 
     return [
       <Split key={`cell-status-icon-${comp.name}`} hasGutter={true} className={splitItemStyle}>
-        <SplitItem>{renderIcon(props.componentStatus.status, props.componentStatus.isCore)}</SplitItem>
+        <SplitItem style={{ paddingLeft: '0.5rem' }}>
+          {renderIcon(props.componentStatus.status, props.componentStatus.isCore)}
+        </SplitItem>
         <SplitItem isFilled={true}>{comp.name}</SplitItem>
         <SplitItem>{t(statusMsg[comp.status])}</SplitItem>
       </Split>
diff --git a/frontend/src/components/IstioStatus/__tests__/__snapshots__/IstioComponentStatus.test.tsx.snap b/frontend/src/components/IstioStatus/__tests__/__snapshots__/IstioComponentStatus.test.tsx.snap
index c9e9ea4e9..0d617ff13 100644
--- a/frontend/src/components/IstioStatus/__tests__/__snapshots__/IstioComponentStatus.test.tsx.snap
+++ b/frontend/src/components/IstioStatus/__tests__/__snapshots__/IstioComponentStatus.test.tsx.snap
@@ -7,7 +7,13 @@ exports[`IstioComponentStatus renders error icon when core component is not foun
     hasGutter={true}
     key="cell-status-icon-isito-ingress"
   >
-    <SplitItem>
+    <SplitItem
+      style={
+        Object {
+          "paddingLeft": "0.5rem",
+        }
+      }
+    >
       <Icon
         className="kiali_fi2mdky kiali_f1h37jb8"
       >
@@ -33,7 +39,13 @@ exports[`IstioComponentStatus renders error icon when core component is not runn
     hasGutter={true}
     key="cell-status-icon-isito-ingress"
   >
-    <SplitItem>
+    <SplitItem
+      style={
+        Object {
+          "paddingLeft": "0.5rem",
+        }
+      }
+    >
       <Icon
         className="kiali_fi2mdky kiali_f1h37jb8"
       >
@@ -59,7 +71,13 @@ exports[`IstioComponentStatus renders minus icon when addon component is not rea
     hasGutter={true}
     key="cell-status-icon-addon"
   >
-    <SplitItem>
+    <SplitItem
+      style={
+        Object {
+          "paddingLeft": "0.5rem",
+        }
+      }
+    >
       <Icon
         className="kiali_fi2mdky kiali_f1elceoh"
       >
@@ -85,7 +103,13 @@ exports[`IstioComponentStatus renders minus icon when core component is not read
     hasGutter={true}
     key="cell-status-icon-core"
   >
-    <SplitItem>
+    <SplitItem
+      style={
+        Object {
+          "paddingLeft": "0.5rem",
+        }
+      }
+    >
       <Icon
         className="kiali_fi2mdky kiali_f1elceoh"
       >
@@ -111,7 +135,13 @@ exports[`IstioComponentStatus renders success icon when core component is runnin
     hasGutter={true}
     key="cell-status-icon-isito-ingress"
   >
-    <SplitItem>
+    <SplitItem
+      style={
+        Object {
+          "paddingLeft": "0.5rem",
+        }
+      }
+    >
       <Icon
         className="kiali_fi2mdky kiali_f1r5cf3w"
       >
@@ -137,7 +167,13 @@ exports[`IstioComponentStatus renders success icon when core component is runnin
     hasGutter={true}
     key="cell-status-icon-prometheus"
   >
-    <SplitItem>
+    <SplitItem
+      style={
+        Object {
+          "paddingLeft": "0.5rem",
+        }
+      }
+    >
       <Icon
         className="kiali_fi2mdky kiali_f1r5cf3w"
       >
@@ -163,7 +199,13 @@ exports[`IstioComponentStatus renders warning icon when core component is not fo
     hasGutter={true}
     key="cell-status-icon-prometheus"
   >
-    <SplitItem>
+    <SplitItem
+      style={
+        Object {
+          "paddingLeft": "0.5rem",
+        }
+      }
+    >
       <Icon
         className="kiali_fi2mdky kiali_frkkrth"
       >
@@ -189,7 +231,13 @@ exports[`IstioComponentStatus renders warning icon when core component is not ru
     hasGutter={true}
     key="cell-status-icon-prometheus"
   >
-    <SplitItem>
+    <SplitItem
+      style={
+        Object {
+          "paddingLeft": "0.5rem",
+        }
+      }
+    >
       <Icon
         className="kiali_fi2mdky kiali_frkkrth"
       >
diff --git a/frontend/src/components/IstioWizards/WizardActions.ts b/frontend/src/components/IstioWizards/WizardActions.ts
index af0f971e4..717e902fc 100644
--- a/frontend/src/components/IstioWizards/WizardActions.ts
+++ b/frontend/src/components/IstioWizards/WizardActions.ts
@@ -70,7 +70,7 @@ import { ANYTHING, PRESENCE } from './RequestRouting/MatchBuilder';
 import { t } from 'utils/I18nUtils';
 import { defaultGatewayLabel, defaultGatewayLabelValue } from 'config/Constants';
 import { dicTypeToGVK, gvkType } from '../../types/IstioConfigList';
-import { getAppLabelName, getVersionLabelName } from 'config/ServerConfig';
+import { getAppLabelName, getVersionLabelName, istioNamespaces } from 'config/ServerConfig';
 
 export const WIZARD_TRAFFIC_SHIFTING = 'traffic_shifting';
 export const WIZARD_TCP_TRAFFIC_SHIFTING = 'tcp_traffic_shifting';
@@ -1956,7 +1956,7 @@ export const buildGraphSidecars = (namespace: string, graph: GraphDefinition): S
             },
             egress: [
               {
-                hosts: [`${serverConfig.istioNamespace}/*`]
+                hosts: istioNamespaces().map(ns => `${ns}/*`)
               }
             ]
           }
diff --git a/frontend/src/components/VirtualList/Renderers.tsx b/frontend/src/components/VirtualList/Renderers.tsx
index d903de89c..a3fe9c658 100644
--- a/frontend/src/components/VirtualList/Renderers.tsx
+++ b/frontend/src/components/VirtualList/Renderers.tsx
@@ -36,7 +36,7 @@ import {
   kindToStringIncludeK8s
 } from 'utils/IstioConfigUtils';
 import { Label } from 'components/Label/Label';
-import { isMultiCluster, serverConfig } from 'config/ServerConfig';
+import { isIstioNamespace, isMultiCluster } from 'config/ServerConfig';
 import { ControlPlaneBadge } from 'pages/Overview/ControlPlaneBadge';
 import { NamespaceStatuses } from 'pages/Overview/NamespaceStatuses';
 import { KialiIcon } from '../../config/KialiIcon';
@@ -260,7 +260,7 @@ export const nsItem: Renderer<NamespaceInfo> = (ns: NamespaceInfo, _config: Reso
     >
       <PFBadge badge={badge} />
       {ns.name}
-      {ns.name === serverConfig.istioNamespace && <ControlPlaneBadge />}
+      {isIstioNamespace(ns.name) && <ControlPlaneBadge />}
     </Td>
   );
 };
diff --git a/frontend/src/config/ServerConfig.ts b/frontend/src/config/ServerConfig.ts
index c816c31a6..e2498fafd 100644
--- a/frontend/src/config/ServerConfig.ts
+++ b/frontend/src/config/ServerConfig.ts
@@ -59,6 +59,7 @@ const defaultServerConfig: ComputedServerConfig = {
   authStrategy: '',
   clusters: {},
   clusterWideAccess: true,
+  controlPlanes: { Kubernetes: 'istio-system' },
   durations: {},
   gatewayAPIClasses: [],
   gatewayAPIEnabled: false,
@@ -69,6 +70,7 @@ const defaultServerConfig: ComputedServerConfig = {
   deployment: {
     viewOnlyMode: false
   },
+  ignoreHomeCluster: false,
   installationTag: 'Kiali Console',
   istioAnnotations: {
     ambientAnnotation: 'ambient.istio.io/redirection',
@@ -76,7 +78,6 @@ const defaultServerConfig: ComputedServerConfig = {
     istioInjectionAnnotation: 'sidecar.istio.io/inject'
   },
   istioIdentityDomain: 'svc.cluster.local',
-  istioNamespace: 'istio-system',
   istioLabels: {
     ambientNamespaceLabel: 'istio.io/dataplane-mode',
     ambientNamespaceLabelValue: 'ambient',
@@ -183,11 +184,17 @@ export const setServerConfig = (cfg: ServerConfig): void => {
   }
 };
 
+export const isIstioControlPlane = (cluster: string, namespace: string): boolean => {
+  return serverConfig.controlPlanes[cluster] === namespace;
+};
+
 export const isIstioNamespace = (namespace: string): boolean => {
-  if (namespace === serverConfig.istioNamespace) {
-    return true;
-  }
-  return false;
+  return Object.values(serverConfig.controlPlanes).some(cpNamespace => cpNamespace === namespace);
+};
+
+export const istioNamespaces = (): string[] => {
+  const cpNamespaces = Object.values(serverConfig.controlPlanes);
+  return [...new Set(cpNamespaces)];
 };
 
 export const isHomeCluster = (cluster: string): boolean => {
diff --git a/frontend/src/pages/IstioConfigNew/IstioConfigNewPage.tsx b/frontend/src/pages/IstioConfigNew/IstioConfigNewPage.tsx
index 6f62afba7..5064c3f25 100644
--- a/frontend/src/pages/IstioConfigNew/IstioConfigNewPage.tsx
+++ b/frontend/src/pages/IstioConfigNew/IstioConfigNewPage.tsx
@@ -77,6 +77,7 @@ import { getGVKTypeString } from '../../utils/IstioConfigUtils';
 import { GroupVersionKind } from '../../types/IstioObjects';
 import { useKialiTranslation } from 'utils/I18nUtils';
 import { usePreviousValue } from 'utils/ReactUtils';
+import { istioNamespaces } from 'config/ServerConfig';
 
 type ReduxProps = {
   activeClusters: MeshCluster[];
@@ -139,7 +140,7 @@ const IstioConfigNewPageComponent: React.FC<Props> = (props: Props) => {
   const [showAnnotationsWizard, setShowAnnotationsWizard] = React.useState<boolean>(false);
   const [showLabelsWizard, setShowLabelsWizard] = React.useState<boolean>(false);
   const [showPreview, setShowPreview] = React.useState<boolean>(false);
-  const [sidecar, setSidecar] = React.useState<SidecarState>(initSidecar(`${serverConfig.istioNamespace}/*`));
+  const [sidecar, setSidecar] = React.useState<SidecarState>(initSidecar(`${istioNamespaces()[0]}/*`));
 
   const { t } = useKialiTranslation();
 
diff --git a/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx b/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx
index ffc376877..43417f78e 100644
--- a/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx
+++ b/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx
@@ -39,6 +39,7 @@ import { ConfigSource, ControlPlane, IstiodNodeData, NodeTarget } from 'types/Me
 import { SimpleTabs } from 'components/Tab/SimpleTabs';
 import { HelpIcon } from '@patternfly/react-icons';
 import { OutboundTrafficPolicy } from 'types/IstioObjects';
+import { isIstioNamespace } from 'config/ServerConfig';
 
 type TargetPanelControlPlaneProps = TargetPanelCommonProps & {
   meshStatus: string;
@@ -416,7 +417,7 @@ export class TargetPanelControlPlane extends React.Component<
 
   private isControlPlane = (): boolean => {
     const data = this.state.controlPlaneNode!.getData()!;
-    return data.namespace === serverConfig.istioNamespace;
+    return isIstioNamespace(data.namespace);
   };
 
   private handleApiError = (message: string, error: ApiError): void => {
diff --git a/frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx b/frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx
index b7f2c05da..f8d303700 100644
--- a/frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx
+++ b/frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx
@@ -8,9 +8,9 @@ import { Title, TitleSizes } from '@patternfly/react-core';
 import { ExpandableRowContent, Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
 import { NamespaceInfo } from 'types/NamespaceInfo';
 import { TargetPanelDataPlaneNamespace } from './TargetPanelDataPlaneNamespace';
-import { serverConfig } from 'config';
 import { useKialiTranslation } from 'utils/I18nUtils';
 import { ControlPlaneVersionBadge } from 'pages/Overview/ControlPlaneVersionBadge';
+import { isIstioNamespace } from 'config/ServerConfig';
 
 type TargetPanelDataPlaneProps = TargetPanelCommonProps & {
   target: NodeTarget<DataPlaneNodeData>;
@@ -72,7 +72,7 @@ export const TargetPanelDataPlane: React.FC<TargetPanelDataPlaneProps> = props =
           </Thead>
 
           {data.infraData
-            .filter(ns => ns.name !== serverConfig.istioNamespace)
+            .filter(ns => !isIstioNamespace(ns.name))
             .sort((ns1, ns2) => (ns1.name < ns2.name ? -1 : 1))
             .map((ns, i) => {
               const namespaceData = data.infraData.find((id: NamespaceInfo) => id.name === ns.name);
diff --git a/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx b/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx
index 293a44403..ba64041b8 100644
--- a/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx
+++ b/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx
@@ -46,6 +46,7 @@ import { panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle';
 import { isRemoteCluster } from './TargetPanelControlPlane';
 import { BoxTarget, ControlPlane, NamespaceNodeData } from 'types/Mesh';
 import { MeshInfraType } from 'types/Mesh';
+import { isIstioNamespace } from 'config/ServerConfig';
 
 type TargetPanelNamespaceProps = TargetPanelCommonProps & {
   target: BoxTarget<NamespaceNodeData>;
@@ -365,7 +366,7 @@ export class TargetPanelNamespace extends React.Component<TargetPanelNamespacePr
     const isControlPlane = this.isControlPlane();
     return (
       <>
-        {isControlPlane && ns.name !== serverConfig.istioNamespace && (
+        {isControlPlane && !isIstioNamespace(ns.name) && (
           <ControlPlaneVersionBadge version={ns.labels ? ns.labels['istio.io/rev'] : ''} />
         )}
 
@@ -537,7 +538,7 @@ export class TargetPanelNamespace extends React.Component<TargetPanelNamespacePr
 
   private isControlPlane = (): boolean => {
     const namespace = this.state.targetNamespace!;
-    return namespace === serverConfig.istioNamespace;
+    return isIstioNamespace(namespace);
   };
 
   private handleApiError = (message: string, error: ApiError): void => {
diff --git a/frontend/src/pages/Overview/OverviewPage.tsx b/frontend/src/pages/Overview/OverviewPage.tsx
index 3267bdff3..03bbacc6b 100644
--- a/frontend/src/pages/Overview/OverviewPage.tsx
+++ b/frontend/src/pages/Overview/OverviewPage.tsx
@@ -75,6 +75,7 @@ import { getGVKTypeString } from '../../utils/IstioConfigUtils';
 import { RefreshIntervalManual, RefreshIntervalPause } from 'config/Config';
 import { EmptyOverview } from './EmptyOverview';
 import { connectRefresh } from 'components/Refresh/connectRefresh';
+import { isIstioControlPlane } from 'config/ServerConfig';
 
 const gridStyleCompact = kialiStyle({
   backgroundColor: PFColors.BackgroundColor200,
@@ -443,7 +444,7 @@ export class OverviewPageComponent extends React.Component<OverviewProps, State>
             nsInfo.errorMetrics = rs.request_error_count;
 
             /*
-            if (nsInfo.name === serverConfig.istioNamespace) {
+            if (isIstioNamespace(nsInfo.name)) {
               nsInfo.controlPlaneMetrics = {
                 istiod_proxy_time: rs.pilot_proxy_convergence_time,
                 istiod_container_cpu: rs.container_cpu_usage_seconds_total,
@@ -775,7 +776,7 @@ export class OverviewPageComponent extends React.Component<OverviewProps, State>
     // then it can use the Istio Injection Actions.
     // RBAC allow more fine granularity but Kiali won't check that in detail.
 
-    if (serverConfig.istioNamespace !== nsInfo.name) {
+    if (!isIstioControlPlane(nsInfo.cluster!, nsInfo.name)) {
       if (
         !(
           serverConfig.ambientEnabled &&
@@ -1319,19 +1320,20 @@ export class OverviewPageComponent extends React.Component<OverviewProps, State>
   };
 
   renderNamespaceBadges = (ns: NamespaceInfo, tooltip: boolean): React.ReactNode => {
+    const isControlPlane = isIstioControlPlane(ns.cluster!, ns.name);
     return (
       <>
-        {ns.name === serverConfig.istioNamespace && <ControlPlaneBadge />}
+        {isControlPlane && <ControlPlaneBadge />}
 
-        {ns.name !== serverConfig.istioNamespace && ns.revision && <ControlPlaneVersionBadge version={ns.revision} />}
+        {!isControlPlane && ns.revision && <ControlPlaneVersionBadge version={ns.revision} />}
 
-        {ns.name === serverConfig.istioNamespace && !this.props.istioAPIEnabled && (
+        {isControlPlane && !this.props.istioAPIEnabled && (
           <Label style={{ marginLeft: '0.5rem' }} color="orange" isCompact>
             Istio API disabled
           </Label>
         )}
 
-        {serverConfig.ambientEnabled && ns.name !== serverConfig.istioNamespace && ns.labels && ns.isAmbient && (
+        {serverConfig.ambientEnabled && !isControlPlane && ns.labels && ns.isAmbient && (
           <AmbientBadge tooltip={tooltip ? 'labeled as part of Ambient Mesh' : undefined}></AmbientBadge>
         )}
       </>
diff --git a/frontend/src/services/Api.ts b/frontend/src/services/Api.ts
index 2c75d981e..9dda571dd 100644
--- a/frontend/src/services/Api.ts
+++ b/frontend/src/services/Api.ts
@@ -303,7 +303,7 @@ export const getClustersMetrics = (
 
 export const getMeshTls = (cluster: string, revision: string): Promise<ApiResponse<TLSStatus>> => {
   const queryParams = {
-    cluster,
+    clusterName: cluster,
     revision
   };
 
diff --git a/frontend/src/types/ErrorRate/__testData__/ErrorRateConfig.ts b/frontend/src/types/ErrorRate/__testData__/ErrorRateConfig.ts
index ad15b7bd5..b85aa394a 100644
--- a/frontend/src/types/ErrorRate/__testData__/ErrorRateConfig.ts
+++ b/frontend/src/types/ErrorRate/__testData__/ErrorRateConfig.ts
@@ -72,10 +72,11 @@ export const serverRateConfig = {
   ambientEnabled: false,
   clusters: {},
   clusterWideAccess: true,
-  controlPlaneClusters: [],
+  controlPlanes: { Kubernetes: 'istio-system' },
   gatewayAPIClasses: [],
   gatewayAPIEnabled: false,
   grafana: {},
+  ignoreHomeCluster: false,
   logLevel: '',
   kialiFeatureFlags: {
     disabledFeatures: [],
diff --git a/frontend/src/types/ServerConfig.ts b/frontend/src/types/ServerConfig.ts
index 5ea51514d..4a879b52f 100644
--- a/frontend/src/types/ServerConfig.ts
+++ b/frontend/src/types/ServerConfig.ts
@@ -139,16 +139,17 @@ export interface ServerConfig {
   ambientEnabled: boolean;
   authStrategy: string;
   clusterWideAccess: boolean;
-  clusters: { [key: string]: MeshCluster };
+  clusters: { [key: string]: MeshCluster }; // cluster => MeshCluster
+  controlPlanes: { [key: string]: string }; // cluster => namespace
   deployment: DeploymentConfig;
   gatewayAPIClasses: GatewayAPIClass[];
   gatewayAPIEnabled: boolean;
   healthConfig: HealthConfig;
+  ignoreHomeCluster: boolean;
   installationTag?: string;
   istioAnnotations: IstioAnnotations;
   istioIdentityDomain: string;
   istioLabels: { [key in IstioLabelKey]: string };
-  istioNamespace: string;
   kialiFeatureFlags: KialiFeatureFlags;
   logLevel: string;
   prometheus: {
diff --git a/frontend/src/types/__testData__/HealthConfig.ts b/frontend/src/types/__testData__/HealthConfig.ts
index 445388ae6..7ae36f557 100644
--- a/frontend/src/types/__testData__/HealthConfig.ts
+++ b/frontend/src/types/__testData__/HealthConfig.ts
@@ -6,10 +6,11 @@ export const healthConfig = {
   ambientEnabled: false,
   clusters: {},
   clusterWideAccess: true,
-  controlPlaneClusters: [],
+  controlPlanes: { Kubernetes: 'istio-system' },
   gatewayAPIClasses: [],
   gatewayAPIEnabled: false,
   grafana: {},
+  ignoreHomeCluster: false,
   logLevel: '',
   kialiFeatureFlags: {
     disabledFeatures: [],
diff --git a/frontend/src/utils/MeshUtils.ts b/frontend/src/utils/MeshUtils.ts
index ab318d83b..f6c49a0d1 100644
--- a/frontend/src/utils/MeshUtils.ts
+++ b/frontend/src/utils/MeshUtils.ts
@@ -1,7 +1,7 @@
 import { store } from '../store/ConfigStore';
-import { serverConfig } from '../config';
+import { isIstioNamespace } from 'config/ServerConfig';
 
-// isControlPlaneAccessible returns true if
+// isControlPlaneAccessible returns true if the user has access to any control plane namespace
 export const isControlPlaneAccessible = (cluster?: string): boolean => {
   const ns = store.getState().namespaces;
 
@@ -9,9 +9,9 @@ export const isControlPlaneAccessible = (cluster?: string): boolean => {
     ns.items !== undefined &&
     ns.items.some(nsItem => {
       if (cluster) {
-        return nsItem.name === serverConfig.istioNamespace && nsItem.cluster === cluster;
+        return isIstioNamespace(nsItem.name) && nsItem.cluster === cluster;
       }
-      return nsItem.name === serverConfig.istioNamespace;
+      return isIstioNamespace(nsItem.name);
     })
   );
 };
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index 9e8775765..bdeb1a886 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -3,7 +3,7 @@
     "baseUrl": "src",
     "outDir": "build/dist",
     "module": "esnext",
-    "target": "es5",
+    "target": "es6",
     "lib": ["es2019", "dom"],
     "sourceMap": true,
     "allowJs": true,
diff --git a/graph/options.go b/graph/options.go
index 4beb4c775..71f19efc1 100644
--- a/graph/options.go
+++ b/graph/options.go
@@ -15,7 +15,6 @@ import (
 	"github.com/prometheus/common/model"
 
 	"github.com/kiali/kiali/business"
-	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/log"
 )
 
@@ -133,7 +132,7 @@ type Options struct {
 	TelemetryOptions
 }
 
-func NewOptions(r *net_http.Request, namespacesService *business.NamespaceService) Options {
+func NewOptions(r *net_http.Request, businessLayer *business.Layer) Options {
 	// path variables (0 or more will be set)
 	vars := mux.Vars(r)
 	aggregate := vars["aggregate"]
@@ -256,7 +255,7 @@ func NewOptions(r *net_http.Request, namespacesService *business.NamespaceServic
 
 	// Process namespaces options:
 	namespaceMap := NewNamespaceInfoMap()
-	accessibleNamespaces := getAccessibleNamespaces(r.Context(), namespacesService)
+	accessibleNamespaces := getAccessibleNamespaces(r.Context(), &businessLayer.Namespace)
 
 	// If path variable is set then it is the only relevant namespace (it's a node graph)
 	// Else if namespaces query param is set it specifies the relevant namespaces
@@ -288,7 +287,7 @@ func NewOptions(r *net_http.Request, namespacesService *business.NamespaceServic
 				Name:      namespaceName,
 				Duration:  getSafeNamespaceDuration(r.Context(), namespaceName, *earliestCreationTimestamp, time.Duration(duration), queryTime),
 				IsAmbient: isAmbient,
-				IsIstio:   config.IsIstioNamespace(namespaceName),
+				IsIstio:   businessLayer.Mesh.IsControlPlane(r.Context(), "", namespaceName),
 			}
 		}
 	}
diff --git a/graph/telemetry/istio/appender/mesh_check.go b/graph/telemetry/istio/appender/mesh_check.go
index 29303b7e2..ca88a6c0d 100644
--- a/graph/telemetry/istio/appender/mesh_check.go
+++ b/graph/telemetry/istio/appender/mesh_check.go
@@ -3,7 +3,6 @@ package appender
 import (
 	"context"
 
-	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/graph"
 )
 
@@ -33,10 +32,10 @@ func (a MeshCheckAppender) AppendGraph(ctx context.Context, trafficMap graph.Tra
 		return
 	}
 
-	a.applyMeshChecks(trafficMap, globalInfo, namespaceInfo)
+	a.applyMeshChecks(ctx, trafficMap, globalInfo, namespaceInfo)
 }
 
-func (a *MeshCheckAppender) applyMeshChecks(trafficMap graph.TrafficMap, globalInfo *graph.GlobalInfo, namespaceInfo *graph.AppenderNamespaceInfo) {
+func (a *MeshCheckAppender) applyMeshChecks(ctx context.Context, trafficMap graph.TrafficMap, globalInfo *graph.GlobalInfo, namespaceInfo *graph.AppenderNamespaceInfo) {
 	for _, n := range trafficMap {
 		// skip if we've already determined the node is out-of-mesh. we may process the same
 		// node multiple times to ensure we check every node (e.g. missing sidecars indicate missing
@@ -51,7 +50,7 @@ func (a *MeshCheckAppender) applyMeshChecks(trafficMap graph.TrafficMap, globalI
 		}
 
 		// We whitelist istio components because they may not report telemetry using injected sidecars.
-		if config.IsIstioNamespace(n.Namespace) {
+		if globalInfo.Business.Mesh.IsControlPlane(ctx, n.Cluster, n.Namespace) {
 			continue
 		}
 
diff --git a/graph/telemetry/istio/util/util.go b/graph/telemetry/istio/util/util.go
index 96356a317..512f247d4 100644
--- a/graph/telemetry/istio/util/util.go
+++ b/graph/telemetry/istio/util/util.go
@@ -43,13 +43,13 @@ func HandleClusters(lSourceCluster model.LabelValue, sourceClusterOk bool, lDest
 // cases.  It should be called after source validation and before destination processing.
 // Returns destSvcNs, destSvcName, destWlNs, destWl, destApp, destVersion, isupdated
 func HandleDestination(sourceCluster, sourceWlNs, sourceWl, destCluster, destSvcNs, destSvc, destSvcName, destWlNs, destWl, destApp, destVer string, conf *config.Config) (string, string, string, string, string, string, string, bool) {
-	// Handle egressgateway (kiali#2999)
+	// Handle egressgateway (kiali#2999), this is pretty weak and only works well using the default control plane namespace, hopefully it's been resolved since it was reported
 	if egressHost == "" {
-		egressHost = fmt.Sprintf("istio-egressgateway.%s.svc.cluster.local", conf.IstioNamespace)
+		egressHost = fmt.Sprintf("istio-egressgateway.%s.svc.cluster.local", config.IstioNamespaceDefault)
 	}
 
 	if destSvc == egressHost && destSvc == destSvcName {
-		istioNs := conf.IstioNamespace
+		istioNs := config.IstioNamespaceDefault
 		log.Debugf("HandleDestination: destCluster=%s, destSvcNs=%s", sourceCluster, istioNs)
 		return sourceCluster, istioNs, "istio-egressgateway", istioNs, "istio-egressgateway", "istio-egressgateway", "latest", true
 	}
diff --git a/hack/istio/install-bookinfo-demo.sh b/hack/istio/install-bookinfo-demo.sh
index e576e2516..f92572471 100755
--- a/hack/istio/install-bookinfo-demo.sh
+++ b/hack/istio/install-bookinfo-demo.sh
@@ -186,6 +186,11 @@ else
   exit 1
 fi
 
+# if we have a valid minikube profile just add it to every kubectl command
+if minikube -p ${MINIKUBE_PROFILE} status > /dev/null 2>&1 ; then
+  CLIENT_EXE="${CLIENT_EXE} --context=${MINIKUBE_PROFILE}"
+fi
+
 IS_OPENSHIFT="false"
 if [[ "${CLIENT_EXE}" = *"oc" ]]; then
   IS_OPENSHIFT="true"
diff --git a/hack/istio/multicluster/deploy-kiali.sh b/hack/istio/multicluster/deploy-kiali.sh
index 4429c465a..d8bd64d27 100755
--- a/hack/istio/multicluster/deploy-kiali.sh
+++ b/hack/istio/multicluster/deploy-kiali.sh
@@ -135,8 +135,7 @@ deploy_kiali() {
       if [ "${MANAGE_KIND}" == "true" ]; then
         remote_url_flag="--remote-cluster-url https://$(${CLIENT_EXE} get nodes ${CLUSTER2_NAME}-control-plane --context ${CLUSTER2_CONTEXT} -o jsonpath='{.status.addresses[?(@.type == "InternalIP")].address}'):6443"
       fi
-      echo "Preparing remote cluster secret for single Kiali install in multicluster mode."
-
+      echo "Preparing remote cluster secret for single Kiali install in multicluster or external mode."
       ${SCRIPT_DIR}/kiali-prepare-remote-cluster.sh -c ${CLIENT_EXE} --remote-cluster-name ${CLUSTER2_NAME} -kcc ${CLUSTER1_CONTEXT} -rcc ${CLUSTER2_CONTEXT} ${remote_url_flag} -vo false ${openshift_flags} -rcns ${ISTIO_NAMESPACE} -kshc ${KIALI_SERVER_HELM_CHARTS}
     else
       echo "Preparing remote cluster secrets for both Kiali installs."
@@ -164,7 +163,41 @@ deploy_kiali() {
     helm_args+=(--set kiali_route_url="${kiali_route_url}")
   fi
 
-  if [ "${TEMPO}" == "true" ]; then
+  # Configure external services if external addresses are available
+  if [ -n "${KIALI_PROMETHEUS_ADDRESS}" ]; then
+    echo "Configuring Kiali to use external Prometheus at: ${KIALI_PROMETHEUS_ADDRESS}"
+    helm_args+=(
+          --set external_services.prometheus.url="http://${KIALI_PROMETHEUS_ADDRESS}:9090"
+        )
+  fi
+
+  if [ -n "${KIALI_GRAFANA_ADDRESS}" ]; then
+    echo "Configuring Kiali to use external Grafana at: ${KIALI_GRAFANA_ADDRESS}"
+    helm_args+=(
+          --set external_services.grafana.external_url="http://${KIALI_GRAFANA_ADDRESS}:3000"
+        )
+  else
+    echo "No external Grafana address provided, using default Grafana address"
+    helm_args+=(
+          --set external_services.grafana.external_url="http://grafana.istio-system:3000"
+        )
+  fi
+
+  if [ -n "${KIALI_TRACING_ADDRESS}" ]; then
+    echo "Configuring Kiali to use external tracing at: ${KIALI_TRACING_ADDRESS}"
+    if [ "${TEMPO}" == "true" ]; then
+      helm_args+=(
+            --set external_services.tracing.external_url="http://${KIALI_TRACING_ADDRESS}:3200"
+            --set external_services.tracing.provider="tempo"
+            --set external_services.tracing.internal_url="http://${KIALI_TRACING_ADDRESS}:3200"
+            --set external_services.tracing.use_grpc="false"
+          )
+    else
+      helm_args+=(
+            --set external_services.tracing.external_url="http://${KIALI_TRACING_ADDRESS}/jaeger"
+          )
+    fi
+  elif [ "${TEMPO}" == "true" ]; then
     helm_args+=(
           --set external_services.tracing.external_url="http://tempo-cr-query-frontend.tempo:3200"
           --set external_services.tracing.provider="tempo"
@@ -179,7 +212,6 @@ deploy_kiali() {
 
   if [ "${CI_CONFIG}" == "true" ]; then
     helm_args+=(
-          --set external_services.grafana.external_url="http://grafana.istio-system:3000"
           --set external_services.grafana.dashboards[0].name="Istio Mesh Dashboard"
           --set external_services.istio.validation_reconcile_interval="5s"
           --set health_config.rate[0].kind="service"
@@ -195,6 +227,13 @@ deploy_kiali() {
         )
   fi
 
+  if [ "${IGNORE_HOME_CLUSTER}" == "true" ]; then
+    helm_args+=(
+          --set clustering.ignore_home_cluster="true"
+          --set kubernetes_config.cluster_name="${CLUSTER1_NAME}"
+        )
+  fi
+
   helm upgrade --install \
     "${helm_args[@]}" \
     --namespace "${ISTIO_NAMESPACE}" \
diff --git a/hack/istio/multicluster/env.sh b/hack/istio/multicluster/env.sh
index c48ea820f..259e1bfbc 100755
--- a/hack/istio/multicluster/env.sh
+++ b/hack/istio/multicluster/env.sh
@@ -72,6 +72,9 @@ ISTIO_DIR=""
 # If the scripts need image registry client, this is it (docker or podman)
 DORP="${DORP:-podman}"
 
+# Set true if the kiali home cluster is out of the mesh (not co-located with an istio control plane)
+IGNORE_HOME_CLUSTER="${IGNORE_HOME_CLUSTER:-false}"
+
 # The namespace where Istio will be found - this namespace must be the same on both clusters
 ISTIO_NAMESPACE="${ISTIO_NAMESPACE:-istio-system}"
 
@@ -172,6 +175,11 @@ KEYCLOAK_ADDRESS="${KEYCLOAK_ADDRESS:-}"
 KEYCLOAK_DB_PASSWORD="${KEYCLOAK_DB_PASSWORD:-keycloak-password}"
 KEYCLOAK_KUBE_CLIENT_SECRET="${KEYCLOAK_KUBE_CLIENT_SECRET:-kube-client-secret}"
 KIALI_USER_PASSWORD="${KIALI_USER_PASSWORD:-kiali}"
+# Optional: set memory limits for Keycloak deployment (passed to keycloak.sh as -slm and -srm)
+# Example: KEYCLOAK_LIMIT_MEMORY=1Gi
+KEYCLOAK_LIMIT_MEMORY="${KEYCLOAK_LIMIT_MEMORY:-}"
+# Example: KEYCLOAK_REQUESTS_MEMORY=1Gi
+KEYCLOAK_REQUESTS_MEMORY="${KEYCLOAK_REQUESTS_MEMORY:-}"
 
 # Path to a Kiali server helm charts tarball
 KIALI_SERVER_HELM_CHARTS="${KIALI_SERVER_HELM_CHARTS:-}"
@@ -185,6 +193,11 @@ KIALI2_WEB_SCHEMA="${KIALI2_WEB_SCHEMA:-}"
 # If true the local dev image of Kiali will be built and used in the Kiali deployment
 KIALI_BUILD_DEV_IMAGE="${KIALI_BUILD_DEV_IMAGE:-false}"
 
+# External service addresses for Kiali configuration
+KIALI_PROMETHEUS_ADDRESS="${KIALI_PROMETHEUS_ADDRESS:-}"
+KIALI_GRAFANA_ADDRESS="${KIALI_GRAFANA_ADDRESS:-}"
+KIALI_TRACING_ADDRESS="${KIALI_TRACING_ADDRESS:-}"
+
 # process command line args
 while [[ $# -gt 0 ]]; do
   key="$1"
@@ -260,6 +273,11 @@ while [[ $# -gt 0 ]]; do
       ISTIO_DIR="$2"
       shift;shift
       ;;
+    -ilc|--ignore-local-cluster)
+      [ "${2:-}" != "true" -a "${2:-}" != "false" ] && echo "--ignore-local-cluster must be 'true' or 'false'" && exit 1
+      IGNORE_HOME_CLUSTER="$2"
+      shift;shift
+      ;;
     -in|--istio-namespace)
       ISTIO_NAMESPACE="$2"
       shift;shift
@@ -300,6 +318,14 @@ while [[ $# -gt 0 ]]; do
       KIALI_ENABLED="$2"
       shift;shift
       ;;
+    -kml|--keycloak-memory-limit)
+      KEYCLOAK_LIMIT_MEMORY="$2"
+      shift;shift
+      ;;
+    -krm|--keycloak-requests-memory)
+      KEYCLOAK_REQUESTS_MEMORY="$2"
+      shift;shift
+      ;;
     -kni|--kind-node-image)
       KIND_NODE_IMAGE="$2"
       shift;shift
@@ -313,6 +339,18 @@ while [[ $# -gt 0 ]]; do
       KIALI_USE_DEV_IMAGE="$2"
       shift;shift
       ;;
+    -kpa|--kiali-prometheus-address)
+      KIALI_PROMETHEUS_ADDRESS="$2"
+      shift;shift
+      ;;
+    -kga|--kiali-grafana-address)
+      KIALI_GRAFANA_ADDRESS="$2"
+      shift;shift
+      ;;
+    -kta|--kiali-tracing-address)
+      KIALI_TRACING_ADDRESS="$2"
+      shift;shift
+      ;;
     -k1wf|--kiali1-web-fqdn)
       KIALI1_WEB_FQDN="$2"
       shift;shift
@@ -419,6 +457,7 @@ Valid command line arguments:
   -ag|--auth-groups <string>: If using Group for authentication, a comma separated groups list. Just for OpenID.
   -gr|--gateway-required <bool>: If a gateway is required to cross between networks, set this to true
   -id|--istio-dir <dir>: Where Istio has already been downloaded. If not found, this script aborts.
+  -ilc|--ignore-local-cluster <bool>: True id the kiali home cluster is not in the mesh (not co-located with an Istio control plane)
   -in|--istio-namespace <name>: Where the Istio control plane is installed (default: istio-system).
   -ih|--istio-hub <hub>: If you want to override the image hub used by istioctl (where the images are found),
                          set this to the hub name, or "default" to use the default image locations.
@@ -440,6 +479,8 @@ Valid command line arguments:
   -ke|--kiali-enabled <bool>: If "true" the latest release of Kiali will be installed in both clusters. If you want
                               a different version of Kiali installed, you must set this to "false" and install it yourself.
                               (Default: true)
+  -kml|--keycloak-memory-limit <value>: Value to increase Keycloak memory limit
+  -krm|--keycloak-requests-memory <value>: Value to increase Keycloak requests memory
   -kni|--kind-node-image: Image of the kind cluster. Defaults to latest kind image if not specified.
   -kshc|--kiali-server-helm-charts <path>: If specified, must be the path to a Kiali server helm charts tarball. If not
                                            specified, the latest published helm charts is used.
@@ -448,6 +489,9 @@ Valid command line arguments:
                                that will be the image pushed to the clusters. You can "make container-build-kiali" to build it.
                                Will be ignored if --kiali-enabled is 'false'. (Default: false)
                                CURRENTLY ONLY SUPPORTED WITH MINIKUBE!
+  -kpa|--kiali-prometheus-address <address>: External IP address of Prometheus service for Kiali to connect to.
+  -kga|--kiali-grafana-address <address>: External IP address of Grafana service for Kiali to connect to.
+  -kta|--kiali-tracing-address <address>: External IP address of tracing service for Kiali to connect to.
   -kup|--kiali-user-password <password>: Password for the kiali user in keycloak.
   -mcpu|--minikube-cpu <cpu count>: Number of CPUs to give to each minikube cluster
   -md|--minikube-driver <name>: The driver used by minikube (e.g. virtualbox, kvm2) (Default: kvm2)
@@ -629,6 +673,7 @@ export AUTH_GROUPS \
        CLUSTER2_USER \
        CROSSNETWORK_GATEWAY_REQUIRED \
        DORP \
+       IGNORE_HOME_CLUSTER \
        IS_OPENSHIFT \
        ISTIO_DIR \
        ISTIO_NAMESPACE \
@@ -636,11 +681,16 @@ export AUTH_GROUPS \
        ISTIO_TAG \
        ISTIO_VERSION \
        KEYCLOAK_CERTS_DIR \
+       KEYCLOAK_LIMIT_MEMORY \
+       KEYCLOAK_REQUESTS_MEMORY \
        KIALI_AUTH_STRATEGY \
        KIALI_BUILD_DEV_IMAGE \
        KIALI_CREATE_REMOTE_CLUSTER_SECRETS \
        KIALI_ENABLED \
+       KIALI_GRAFANA_ADDRESS \
+       KIALI_PROMETHEUS_ADDRESS \
        KIALI_SERVER_HELM_CHARTS \
+       KIALI_TRACING_ADDRESS \
        KIALI_USE_DEV_IMAGE \
        KIND_NODE_IMAGE \
        MANAGE_KIND \
@@ -675,6 +725,7 @@ CLUSTER2_PASS=$CLUSTER2_PASS
 CLUSTER2_USER=$CLUSTER2_USER
 CROSSNETWORK_GATEWAY_REQUIRED=$CROSSNETWORK_GATEWAY_REQUIRED
 DORP=$DORP
+IGNORE_HOME_CLUSTER=$IGNORE_HOME_CLUSTER
 IS_OPENSHIFT=$IS_OPENSHIFT
 ISTIO_DIR=$ISTIO_DIR
 ISTIO_NAMESPACE=$ISTIO_NAMESPACE
@@ -682,11 +733,16 @@ ISTIO_HUB=$ISTIO_HUB
 ISTIO_TAG=$ISTIO_TAG
 ISTIO_VERSION=$ISTIO_VERSION
 KEYCLOAK_CERTS_DIR=$KEYCLOAK_CERTS_DIR
+KEYCLOAK_LIMIT_MEMORY=$KEYCLOAK_LIMIT_MEMORY
+KEYCLOAK_REQUESTS_MEMORY=$KEYCLOAK_REQUESTS_MEMORY
 KIALI_AUTH_STRATEGY=$KIALI_AUTH_STRATEGY
 KIALI_BUILD_DEV_IMAGE=$KIALI_BUILD_DEV_IMAGE
 KIALI_CREATE_REMOTE_CLUSTER_SECRETS=$KIALI_CREATE_REMOTE_CLUSTER_SECRETS
 KIALI_ENABLED=$KIALI_ENABLED
+KIALI_GRAFANA_ADDRESS=$KIALI_GRAFANA_ADDRESS
+KIALI_PROMETHEUS_ADDRESS=$KIALI_PROMETHEUS_ADDRESS
 KIALI_SERVER_HELM_CHARTS=$KIALI_SERVER_HELM_CHARTS
+KIALI_TRACING_ADDRESS=$KIALI_TRACING_ADDRESS
 KIALI_USE_DEV_IMAGE=$KIALI_USE_DEV_IMAGE
 KIALI1_WEB_FQDN=$KIALI1_WEB_FQDN
 KIALI1_WEB_SCHEMA=$KIALI1_WEB_SCHEMA
diff --git a/hack/istio/multicluster/install-external-kiali.sh b/hack/istio/multicluster/install-external-kiali.sh
new file mode 100755
index 000000000..d2f6a02b8
--- /dev/null
+++ b/hack/istio/multicluster/install-external-kiali.sh
@@ -0,0 +1,263 @@
+#!/bin/bash
+
+##############################################################################
+# install-external-kiali.sh
+#
+# Installs two clusters, one with only Kiali (cluster-1 = "mgmt") amd one with only istio (cluster-2 = "mesh").
+#
+# See --help for more details on options to this script.
+#
+##############################################################################
+
+SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)"
+source ${SCRIPT_DIR}/env.sh $*
+
+# The names of each cluster
+if [ "${CLUSTER1_NAME}" == "east" ]; then
+  CLUSTER1_NAME="mgmt"
+fi
+if [ "${CLUSTER2_NAME}" == "west" ]; then
+  CLUSTER2_NAME="mesh"
+fi
+
+# Only install Kiali on cluster-1
+CLUSTER1_CONTEXT="${CLUSTER1_NAME}"
+CLUSTER2_CONTEXT="${CLUSTER2_NAME}"
+IGNORE_HOME_CLUSTER="true"
+SINGLE_KIALI="true"
+
+# TODO: just use anonymous auth until we have this working...
+KIALI_AUTH_STRATEGY="anonymous"
+
+create_crossnetwork_gateway() {
+  local clustername="${1}"
+  local network="${2}"
+
+  # create the gateway
+  local image_hub_arg="--set hub=gcr.io/istio-release"
+  if [ ! -z "${ISTIO_HUB}" -a "${ISTIO_HUB}" != "default" ]; then
+    image_hub_arg="--set hub=${ISTIO_HUB}"
+  fi
+  if [ ! -z "${ISTIO_TAG}" ]; then
+    local image_tag_arg="--set tag=${ISTIO_TAG}"
+  fi
+  
+  local gateway_yaml="$("${GEN_GATEWAY_SCRIPT}" --mesh "${MESH_ID}" --cluster "${clustername}" --network "${network}")"
+  local profile_flag=""
+  if [ "${IS_OPENSHIFT}" == "true" ] || [ "${KIALI_AUTH_STRATEGY}" == "openshift" ]; then
+    profile_flag="--set profile=openshift"
+  fi
+
+  printf "%s" "${gateway_yaml}" | "${ISTIOCTL}" install ${profile_flag} ${image_hub_arg} ${image_tag_arg:-} -y -f -
+  if [ "$?" != "0" ]; then
+    echo "Failed to install crossnetwork gateway on cluster [${clustername}]"
+    exit 1
+  fi
+
+  # expose services
+  ${CLIENT_EXE} apply -n ${ISTIO_NAMESPACE} -f "${EXPOSE_SERVICES_YAML}"
+  if [ "$?" != "0" ]; then
+    echo "Failed to expose services on cluster [${clustername}]"
+    exit 1
+  fi
+}
+
+create_remote_secret() {
+  local clustername="${1}"
+  local secretcount="$(${CLIENT_EXE} get sa -n ${ISTIO_NAMESPACE} istio-reader-service-account --no-headers | tr -s ' ' | cut -d ' ' -f 2)"
+  local secretname=""
+  if [ "${secretcount}" -gt 1 ]; then
+    # find the service account token secret (the word "token" will be in the secret name)
+    secretname="--secret-name $(${CLIENT_EXE} get sa -n ${ISTIO_NAMESPACE} istio-reader-service-account -o jsonpath='{.secrets[0].name}')"
+    if ! echo ${secretname} | grep "token"; then
+      secretname="--secret-name $(${CLIENT_EXE} get sa -n ${ISTIO_NAMESPACE} istio-reader-service-account -o jsonpath='{.secrets[1].name}')"
+      if ! echo ${secretname} | grep "token"; then
+        echo "Failed to find the sa token secret"
+        exit 1
+      fi
+    fi
+    echo "Choosing to use: [${secretname}]"
+  fi
+  REMOTE_SECRET="$("${ISTIOCTL}" create-remote-secret --name "${clustername}" ${secretname})"
+  if [ "$?" != "0" ]; then
+    echo "Failed to generate remote secret for cluster [${clustername}]"
+    exit 1
+  fi
+
+  # if kind, then we have to make sure the remote secret has the external IP to the API server
+  if [ "${MANAGE_KIND}" == "true" ]; then
+    local kind_ip=$(${DORP} inspect ${clustername}-control-plane --format "{{ .NetworkSettings.Networks.kind.IPAddress }}")
+    REMOTE_SECRET="$(printf '%s' "${REMOTE_SECRET}" | sed -E 's!server:.*!server: https://'"${kind_ip}"':6443!')"
+    echo "Updating remote secret for kind cluster [${clustername}] to use API IP [${kind_ip}]"
+  fi
+}
+
+MC_MESH_YAML=$(mktemp)
+cat <<EOF > "$MC_MESH_YAML"
+spec:
+  values:
+    global:
+      meshID: ${MESH_ID}
+      multiCluster:
+        clusterName: ${CLUSTER2_NAME}
+      network: ${NETWORK2_ID}
+EOF
+
+# Start up two minikube instances if requested
+if [ "${MANAGE_MINIKUBE}" == "true" ]; then
+  echo "Starting minikube instances"
+  source ${SCRIPT_DIR}/start-minikube.sh
+fi
+
+# Start up two kind instances if requested
+if [ "${MANAGE_KIND}" == "true" ]; then
+  echo "Starting kind instances"
+
+  if [ "${KIALI_AUTH_STRATEGY}" == "openid" ]; then
+    "${SCRIPT_DIR}/../../keycloak.sh" -kcd "${KEYCLOAK_CERTS_DIR}" create-ca
+
+    docker network create kind || true
+
+    # Given: 172.18.0.0/16 this should return 172.18
+    subnet=""
+
+    # we always use docker today, but we'll leave this here just in case in the future Kind and podman play nice
+    if [ "${DORP}" == "docker" ]; then
+      # loop through all known subnets in the kind network and pick out the IPv4 subnet, ignoring any IPv6 that might be in the list
+      subnets_count="$(docker network inspect kind | jq '.[0].IPAM.Config | length')"
+      infomsg "There are [$subnets_count] subnets in the kind network"
+      for ((i=0; i<subnets_count; i++)); do
+        subnet=$(docker network inspect kind --format '{{(index .IPAM.Config '$i').Subnet}}' 2> /dev/null)
+        if [[ -n $subnet && $subnet != *:* && $subnet == *\.* ]]; then
+          infomsg "Using subnet [$subnet]"
+          break
+        else
+          infomsg "Ignoring subnet [$subnet]"
+          subnet=""
+        fi
+      done
+      if [ -z "$subnet" ]; then
+        infomsg "No subnets found in the expected docker network list. Maybe this is a podman network - let's check"
+        subnet=$(docker network inspect kind | jq -r '.[0].subnets[] | select(.subnet | test("^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+/")) | .subnet' 2>/dev/null)
+      fi
+    else
+      subnet=$(podman network inspect kind | jq -r '.[0].subnets[] | select(.subnet | test("^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+/")) | .subnet' 2>/dev/null)
+    fi
+
+    if [ -z "$subnet" ]; then
+      infomsg "There does not appear to be any IPv4 subnets configured"
+      exit 1
+    fi
+
+    beginning_subnet_octets=$(echo $subnet | cut -d'.' -f1,2 2>/dev/null)
+    lb_range_start="255.70"
+    lb_range_end="255.84"
+
+    KEYCLOAK_ADDRESS="${beginning_subnet_octets}.${lb_range_start}"
+
+    echo "==== START KIND FOR CLUSTER #1 [${CLUSTER1_NAME}] - ${CLUSTER1_CONTEXT}"
+    "${SCRIPT_DIR}"/../../start-kind.sh \
+      --name "${CLUSTER1_NAME}" \
+      --load-balancer-range "${lb_range_start}-${lb_range_end}" \
+      --enable-keycloak true \
+      --keycloak-certs-dir "${KEYCLOAK_CERTS_DIR}" \
+      --keycloak-issuer-uri https://"${KEYCLOAK_ADDRESS}"/realms/kube \
+      --image "${KIND_NODE_IMAGE}"
+
+    "${SCRIPT_DIR}/../../keycloak.sh" -kcd "${KEYCLOAK_CERTS_DIR}" -kip "${KEYCLOAK_ADDRESS}" deploy
+
+    echo "==== START KIND FOR CLUSTER #2 [${CLUSTER2_NAME}] - ${CLUSTER2_CONTEXT}"
+    "${SCRIPT_DIR}"/../../start-kind.sh \
+      --name "${CLUSTER2_NAME}" \
+      --load-balancer-range "255.85-255.98" \
+      --enable-keycloak true \
+      --keycloak-certs-dir "${KEYCLOAK_CERTS_DIR}" \
+      --keycloak-issuer-uri https://"${KEYCLOAK_ADDRESS}"/realms/kube \
+      --image "${KIND_NODE_IMAGE}"
+  else
+    echo "==== START KIND FOR CLUSTER #1 [${CLUSTER1_NAME}] - ${CLUSTER1_CONTEXT}"
+    "${SCRIPT_DIR}"/../../start-kind.sh \
+      --name "${CLUSTER1_NAME}" \
+      --load-balancer-range "255.70-255.84" \
+      --image "${KIND_NODE_IMAGE}"
+
+    echo "==== START KIND FOR CLUSTER #2 [${CLUSTER2_NAME}] - ${CLUSTER2_CONTEXT}"
+    "${SCRIPT_DIR}"/../../start-kind.sh \
+      --name "${CLUSTER2_NAME}" \
+      --load-balancer-range "255.85-255.98" \
+      --image "${KIND_NODE_IMAGE}"
+  fi
+fi
+
+# Setup the certificates
+source ${SCRIPT_DIR}/setup-ca.sh
+
+echo "==== INSTALL ISTIO ON CLUSTER #2 [${CLUSTER2_NAME}] - ${CLUSTER2_CONTEXT}"
+switch_cluster "${CLUSTER2_CONTEXT}" "${CLUSTER2_USER}" "${CLUSTER2_PASS}"
+install_istio --patch-file "${MC_MESH_YAML}"
+
+# Expose Prometheus to the outside world
+${CLIENT_EXE} patch svc prometheus -n ${ISTIO_NAMESPACE} --context ${CLUSTER2_CONTEXT} -p "{\"spec\": {\"type\": \"LoadBalancer\"}}"
+
+MGMT_PROMETHEUS_ADDRESS=$(${CLIENT_EXE} --context=${CLUSTER2_CONTEXT} -n ${ISTIO_NAMESPACE} get svc prometheus -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
+if [ -z "${MGMT_PROMETHEUS_ADDRESS}" ]; then
+  echo "WARNING! Prometheus not updated - cannot determine the west prometheus load balancer ingress IP"
+else
+#  ## TODO: prometheus.yaml has CLUSTER_NAME in it that should also be searched-replaced, but that is not done here
+#  cat ${SCRIPT_DIR}/prometheus.yaml | sed -e "s/MGMT_PROMETHEUS_ADDRESS/$MGMT_PROMETHEUS_ADDRESS/g" | ${CLIENT_EXE} apply -n ${ISTIO_NAMESPACE} --context ${CLUSTER1_CONTEXT} -f -
+  echo "Prometheus external IP: ${MGMT_PROMETHEUS_ADDRESS}"
+fi
+
+# Expose Grafana to the outside world if it exists
+MGMT_GRAFANA_ADDRESS=""
+if ${CLIENT_EXE} get svc grafana -n ${ISTIO_NAMESPACE} --context ${CLUSTER2_CONTEXT} >/dev/null 2>&1; then
+  ${CLIENT_EXE} patch svc grafana -n ${ISTIO_NAMESPACE} --context ${CLUSTER2_CONTEXT} -p "{\"spec\": {\"type\": \"LoadBalancer\"}}"
+
+  MGMT_GRAFANA_ADDRESS=$(${CLIENT_EXE} --context=${CLUSTER2_CONTEXT} -n ${ISTIO_NAMESPACE} get svc grafana -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
+  if [ -z "${MGMT_GRAFANA_ADDRESS}" ]; then
+    echo "WARNING! Grafana not updated - cannot determine the grafana load balancer ingress IP"
+  else
+    echo "Grafana external IP: ${MGMT_GRAFANA_ADDRESS}"
+  fi
+else
+  echo "Grafana service not found - skipping grafana exposure"
+fi
+
+# Expose tracing service to the outside world if it exists
+MGMT_TRACING_ADDRESS=""
+if ${CLIENT_EXE} get svc tracing -n ${ISTIO_NAMESPACE} --context ${CLUSTER2_CONTEXT} >/dev/null 2>&1; then
+  ${CLIENT_EXE} patch svc tracing -n ${ISTIO_NAMESPACE} --context ${CLUSTER2_CONTEXT} -p "{\"spec\": {\"type\": \"LoadBalancer\"}}"
+
+  MGMT_TRACING_ADDRESS=$(${CLIENT_EXE} --context=${CLUSTER2_CONTEXT} -n ${ISTIO_NAMESPACE} get svc tracing -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
+  if [ -z "${MGMT_TRACING_ADDRESS}" ]; then
+    echo "WARNING! Tracing not updated - cannot determine the tracing load balancer ingress IP"
+  else
+    echo "Tracing external IP: ${MGMT_TRACING_ADDRESS}"
+  fi
+else
+  echo "Tracing service not found - skipping tracing exposure"
+fi
+
+# TODO Anything to do for this? Configure Tracing "federation"
+#source ${SCRIPT_DIR}/setup-tracing.sh
+
+# Install Kiali if enabled
+if [ "${KIALI_ENABLED}" == "true" ]; then
+  if [ -z "${KEYCLOAK_ADDRESS}" ] && [ "${MANAGE_MINIKUBE}" == "true" ]; then
+    echo "Keycloak is not available for this cluster setup. Switching Kiali to 'anonymous' mode."
+    export KIALI_AUTH_STRATEGY="anonymous"
+  fi
+
+  # Set external service addresses for Kiali configuration
+  if [ -n "${MGMT_PROMETHEUS_ADDRESS}" ]; then
+    export KIALI_PROMETHEUS_ADDRESS="${MGMT_PROMETHEUS_ADDRESS}"
+  fi
+  if [ -n "${MGMT_GRAFANA_ADDRESS}" ]; then
+    export KIALI_GRAFANA_ADDRESS="${MGMT_GRAFANA_ADDRESS}"
+  fi
+  if [ -n "${MGMT_TRACING_ADDRESS}" ]; then
+    export KIALI_TRACING_ADDRESS="${MGMT_TRACING_ADDRESS}"
+  fi
+
+  source ${SCRIPT_DIR}/deploy-kiali.sh
+fi
diff --git a/hack/istio/multicluster/install-multi-primary.sh b/hack/istio/multicluster/install-multi-primary.sh
index f000e1786..d53a6e9a1 100755
--- a/hack/istio/multicluster/install-multi-primary.sh
+++ b/hack/istio/multicluster/install-multi-primary.sh
@@ -1,7 +1,7 @@
 #!/bin/bash
 
 ##############################################################################
-# install-everything.sh
+# install-multi-primary.sh
 #
 # Installs Istio across two clusters using the "multi-primary" model.
 #
@@ -178,7 +178,21 @@ if [ "${MANAGE_KIND}" == "true" ]; then
       --keycloak-issuer-uri https://"${KEYCLOAK_ADDRESS}"/realms/kube \
       --image "${KIND_NODE_IMAGE}"
 
-    "${SCRIPT_DIR}/../../keycloak.sh" -kcd "${KEYCLOAK_CERTS_DIR}" -kip "${KEYCLOAK_ADDRESS}" deploy
+    
+    # Optional: keycloak memory limits
+    KEYCLOAK_LIMIT_MEMORY="${KEYCLOAK_LIMIT_MEMORY:-}"
+    KEYCLOAK_REQUESTS_MEMORY="${KEYCLOAK_REQUESTS_MEMORY:-}"
+    if [ -n "$KEYCLOAK_LIMIT_MEMORY" ]; then
+      MEMORY_LIMIT_ARG="-slm $KEYCLOAK_LIMIT_MEMORY"
+    else
+      MEMORY_LIMIT_ARG=""
+    fi
+    if [ -n "$KEYCLOAK_REQUESTS_MEMORY" ]; then
+      MEMORY_REQUEST_ARG="-srm $KEYCLOAK_REQUESTS_MEMORY"
+    else
+      MEMORY_REQUEST_ARG=""
+    fi
+    "${SCRIPT_DIR}/../../keycloak.sh" -kcd "${KEYCLOAK_CERTS_DIR}" -kip "${KEYCLOAK_ADDRESS}" $MEMORY_LIMIT_ARG $MEMORY_REQUEST_ARG deploy
 
     echo "==== START KIND FOR CLUSTER #2 [${CLUSTER2_NAME}] - ${CLUSTER2_CONTEXT}"
     "${SCRIPT_DIR}"/../../start-kind.sh \
diff --git a/hack/keycloak.sh b/hack/keycloak.sh
index e50441e25..9aa295153 100755
--- a/hack/keycloak.sh
+++ b/hack/keycloak.sh
@@ -6,6 +6,8 @@ set -e
 
 KEYCLOAK_CERTS_DIR=""
 KEYCLOAK_EXTERNAL_IP=""
+SET_LIMIT_MEMORY=""
+SET_REQUESTS_MEMORY=""
 
 infomsg() {
   echo "[INFO] ${1}"
@@ -19,6 +21,10 @@ while [[ $# -gt 0 ]]; do
     deploy) _CMD="deploy"; shift ;;
     -kcd|--keycloak-certs-dir)    KEYCLOAK_CERTS_DIR="$2";    shift;shift; ;;
     -kip|--keycloak-external-ip)    KEYCLOAK_EXTERNAL_IP="$2";    shift;shift; ;;
+    -slm|--set-limit-memory)
+      SET_LIMIT_MEMORY="$2"; shift; shift ;;
+    -srm|--set-requests-memory)
+      SET_REQUESTS_MEMORY="$2"; shift; shift ;;
     -h|--help)
       cat <<HELPMSG
 
@@ -31,6 +37,10 @@ Valid options:
   -kip|--keycloak-external-ip
       External IP address for the keycloak service.
       Required for the 'deploy' command.
+  -slm|--set-limit-memory
+      Add --set resources.limits.memory <value> to the helm command. Ex. resources.limits.memory=1Gi
+  -srm|--set-requests-memory
+      Add --set resources.requests.memory <valor>  to the helm command. Ex. resources.requests.memory=1Gi
 
 The command must be one of:
   create-ca:        create the root CA for keycloak.
@@ -108,10 +118,17 @@ EOF
   # create kube secret from the certs
   kubectl create secret tls keycloak-tls --cert="${KEYCLOAK_CERTS_DIR}"/cert.pem --key="${KEYCLOAK_CERTS_DIR}"/key.pem -n keycloak
 
-  echo "Creating keycloak deployment"
+  HELM_MEMORY_ARGS=""
+  if [ -n "$SET_LIMIT_MEMORY" ]; then
+    HELM_MEMORY_ARGS="$HELM_MEMORY_ARGS --set resources.limits.memory=$SET_LIMIT_MEMORY"
+  fi
+  if [ -n "$SET_REQUESTS_MEMORY" ]; then
+    HELM_MEMORY_ARGS="$HELM_MEMORY_ARGS --set resources.requests.memory=$SET_REQUESTS_MEMORY"
+  fi
   helm upgrade --install --wait --timeout 15m \
   --namespace keycloak \
-   keycloak oci://registry-1.docker.io/bitnamicharts/keycloak --version 24.3.2 \
+  keycloak oci://registry-1.docker.io/bitnamicharts/keycloak --version 24.3.2 \
+  $HELM_MEMORY_ARGS \
   --reuse-values --values - <<EOF
 auth:
   createAdminUser: true
diff --git a/hack/run-integration-tests.sh b/hack/run-integration-tests.sh
index 4a04dd94c..89717ef31 100755
--- a/hack/run-integration-tests.sh
+++ b/hack/run-integration-tests.sh
@@ -14,6 +14,8 @@ FRONTEND_MULTI_PRIMARY="frontend-multi-primary"
 FRONTEND_TEMPO="frontend-tempo"
 HELM_CHARTS_DIR=""
 ISTIO_VERSION=""
+KEYCLOAK_LIMIT_MEMORY=""
+KEYCLOAK_REQUESTS_MEMORY=""
 SETUP_ONLY="false"
 STERN="false"
 TEMPO="false"
@@ -33,6 +35,14 @@ while [[ $# -gt 0 ]]; do
       ISTIO_VERSION="${2}"
       shift;shift
       ;;
+    -klm|--keycloak-limit-memory)
+      KEYCLOAK_LIMIT_MEMORY="${2}"
+      shift;shift
+      ;;
+    -krm|--keycloak-requests-memory)
+      KEYCLOAK_REQUESTS_MEMORY="${2}"
+      shift;shift
+      ;;
     -so|--setup-only)
       SETUP_ONLY="${2}"
       if [ "${SETUP_ONLY}" != "true" -a "${SETUP_ONLY}" != "false" ]; then
@@ -85,6 +95,10 @@ Valid command line arguments:
   -iv|--istio-version <version>
     Which Istio version to test with. For releases, specify "#.#.#". For dev builds, specify in the form "#.#-dev"
     Default: The latest release
+  -klm|--keycloak-limit-memory <value>
+    Set the keycloak resources limit memory in the keycloak helm charts. Ex. 1Gi
+  -krm|--keycloak-requests-memory <value>
+    Set the keycloak resources requests memory in the keycloak helm charts. Ex. 1Gi
   -so|--setup-only <true|false>
     If true, only setup the test environment and exit without running the tests.
     Default: false
@@ -132,6 +146,8 @@ cat <<EOM
 === SETTINGS ===
 HELM_CHARTS_DIR=$HELM_CHARTS_DIR
 ISTIO_VERSION=$ISTIO_VERSION
+KEYCLOAK_LIMIT_MEMORY=$KEYCLOAK_LIMIT_MEMORY
+KEYCLOAK_REQUESTS_MEMORY=$KEYCLOAK_LIMIT_MEMORY
 SETUP_ONLY=$SETUP_ONLY
 STERN=$STERN
 TESTS_ONLY=$TESTS_ONLY
@@ -466,8 +482,19 @@ elif [ "${TEST_SUITE}" == "${FRONTEND_PRIMARY_REMOTE}" ]; then
 elif [ "${TEST_SUITE}" == "${FRONTEND_MULTI_PRIMARY}" ]; then
   ensureCypressInstalled
 
+  if [ -n "$KEYCLOAK_LIMIT_MEMORY" ]; then
+      MEMORY_LIMIT_ARG="-klm $KEYCLOAK_LIMIT_MEMORY"
+  else
+      MEMORY_LIMIT_ARG=""
+  fi
+  if [ -n "$KEYCLOAK_REQUESTS_MEMORY" ]; then
+     MEMORY_REQUEST_ARG="-krm $KEYCLOAK_REQUESTS_MEMORY"
+  else
+     MEMORY_REQUEST_ARG=""
+  fi
+
   if [ "${TESTS_ONLY}" == "false" ]; then
-    "${SCRIPT_DIR}"/setup-kind-in-ci.sh --multicluster "multi-primary" ${ISTIO_VERSION_ARG} --auth-strategy openid ${HELM_CHARTS_DIR_ARG}
+    "${SCRIPT_DIR}"/setup-kind-in-ci.sh --multicluster "multi-primary" ${ISTIO_VERSION_ARG} --auth-strategy openid ${HELM_CHARTS_DIR_ARG} $MEMORY_LIMIT_ARG $MEMORY_REQUEST_ARG
   fi
   
   ensureKialiServerReady
diff --git a/hack/run-kiali-config-template.yaml b/hack/run-kiali-config-template.yaml
index ffd459ffe..12e17f2d1 100644
--- a/hack/run-kiali-config-template.yaml
+++ b/hack/run-kiali-config-template.yaml
@@ -13,14 +13,16 @@
 #
 ###################################################################
 
-istio_namespace: "${ISTIO_NAMESPACE}"
-
 auth:
   strategy: "anonymous"
 
+clustering:
+  # run-kiali is, by definition, running outside of a cluster so you must ignore home cluster
+  # when ignore_home_cluster=true, use run-kiali.sh -cn option if the context name != cluster name.
+  ignore_home_cluster: true
+
 deployment:
   cluster_wide_access: true
-  remote_secret_path: "${REMOTE_SECRET_PATH}"
 
 external_services:
   custom_dashboards:
@@ -46,6 +48,3 @@ external_services:
 
 login_token:
   signing_key: "notsecure"
-
-# in_cluster must be false - do not change
-in_cluster: false
diff --git a/hack/run-kiali.sh b/hack/run-kiali.sh
index d1f9d6b8e..719287917 100755
--- a/hack/run-kiali.sh
+++ b/hack/run-kiali.sh
@@ -103,6 +103,7 @@ while [[ $# -gt 0 ]]; do
     -cn|--cluster-name)          CLUSTER_NAME="$2";                  shift;shift ;;
     -es|--enable-server)         ENABLE_SERVER="$2";                 shift;shift ;;
     -gu|--grafana-url)           GRAFANA_URL="$2";                   shift;shift ;;
+    -hkc|--home-kube-context)    HOME_KUBE_CONTEXT="$2";             shift;shift ;;
     -in|--istio-namespace)       ISTIO_NAMESPACE="$2";               shift;shift ;;
     -isn|--istiod-service-name)  ISTIOD_SERVICE_NAME="$2";           shift;shift ;;
     -iu|--istiod-url)            ISTIOD_URL="$2";                    shift;shift ;;
@@ -152,7 +153,8 @@ Valid options:
       Cluster client executable - must refer to 'oc' or 'kubectl'.
       Default: ${DEFAULT_CLIENT_EXE}
   -cn|--cluster-name)
-      The name of the cluster as defined by the Istio infrastructure.
+      The name of the Kiali home cluster. This option must be specified if the home cluster name is
+      not the same as the active home context.
       Default: <not defined>
   -es|--enable-server
       When 'true', this script will start the server and manage its lifecycle.
@@ -167,6 +169,11 @@ Valid options:
       to external clients outside of the cluster - that external URL is what this value should be.
       For example, for OpenShift clusters, this should be the Grafana Route URL.
       Default: <will be auto-discovered>
+  -hkc|--home-kube-context
+      The kubernetes context to use specifically when talking to the Kiali pod to download secrets.
+      For all other operations, the --kube-context value will be used.
+      If not specified, the --kube-context value will be used as the default.
+      Default: <same as --kube-context>
   -in|--istio-namespace
       The name of the control plane namespace - this is where Istio components are installed.
       Default: ${DEFAULT_ISTIO_NAMESPACE}
@@ -267,6 +274,7 @@ ISTIO_NAMESPACE="${ISTIO_NAMESPACE:-${DEFAULT_ISTIO_NAMESPACE}}"
 KIALI_CONFIG_TEMPLATE_FILE="${KIALI_CONFIG_TEMPLATE_FILE:-${DEFAULT_KIALI_CONFIG_TEMPLATE_FILE}}"
 KIALI_EXE="${KIALI_EXE:-${DEFAULT_KIALI_EXE}}"
 KUBE_CONTEXT="${KUBE_CONTEXT:-${DEFAULT_KUBE_CONTEXT}}"
+HOME_KUBE_CONTEXT="${HOME_KUBE_CONTEXT:-${KUBE_CONTEXT}}" # uses KUBE_CONTEXT as default, that's why it appears after KUBE_CONTEXT
 LOCAL_REMOTE_PORTS_GRAFANA="${LOCAL_REMOTE_PORTS_GRAFANA:-${DEFAULT_LOCAL_REMOTE_PORTS_GRAFANA}}"
 LOCAL_REMOTE_PORTS_PROMETHEUS="${LOCAL_REMOTE_PORTS_PROMETHEUS:-${DEFAULT_LOCAL_REMOTE_PORTS_PROMETHEUS}}"
 LOCAL_REMOTE_PORTS_TRACING="${LOCAL_REMOTE_PORTS_TRACING:-${DEFAULT_LOCAL_REMOTE_PORTS_TRACING}}"
@@ -494,6 +502,7 @@ echo "CLUSTER_NAME=$CLUSTER_NAME"
 echo "COPY_CLUSTER_SECRETS=$COPY_CLUSTER_SECRETS"
 echo "ENABLE_SERVER=$ENABLE_SERVER"
 echo "GRAFANA_URL=$GRAFANA_URL"
+echo "HOME_KUBE_CONTEXT=$HOME_KUBE_CONTEXT"
 echo "ISTIO_NAMESPACE=$ISTIO_NAMESPACE"
 echo "ISTIOD_URL=$ISTIOD_URL"
 echo "KIALI_CONFIG_TEMPLATE_FILE=$KIALI_CONFIG_TEMPLATE_FILE"
@@ -528,15 +537,6 @@ if ! echo "${LOG_LEVEL}" | grep -qiE "^(trace|debug|info|warn|error|fatal)$"; th
 [ "${ENABLE_SERVER}" == "false" -a "${REBOOTABLE}" == "true" ] && infomsg "--enable-server was set to false - turning off rebootable flag for you" && REBOOTABLE="false"
 [ "${COPY_CLUSTER_SECRETS}" != "true" -a "${COPY_CLUSTER_SECRETS}" != "false" ] && errormsg "--copy-cluster-secrets must be 'true' or 'false'" && exit 1
 
-REMOTE_SECRET_PATH=""
-if [ "${KUBE_CONTEXT}" == "current" ]; then
-  infomsg "Will use the current context as-is: $(${CLIENT_EXE} config current-context)"
-  # Re-use your current kubeconfig file.
-  REMOTE_SECRET_PATH="${HOME}/.kube/config"
-else
-  REMOTE_SECRET_PATH="${TMP_DIR}/kubeconfig"
-fi
-
 # Build the config file from the template
 
 KIALI_CONFIG_FILE="${TMP_DIR}/run-kiali-config.yaml"
@@ -547,7 +547,6 @@ cat ${KIALI_CONFIG_TEMPLATE_FILE} | \
   GRAFANA_URL=${GRAFANA_URL} \
   TRACING_APP=${TRACING_APP} \
   TRACING_URL=${TRACING_URL} \
-  REMOTE_SECRET_PATH=${REMOTE_SECRET_PATH} \
   envsubst > ${KIALI_CONFIG_FILE}
 
 # Set kubernetes_config.cluster_name only if the user told us to configure a specific cluster name
@@ -566,35 +565,44 @@ cd ${TMP_DIR}
 # If we are told to copy the remote cluster secrets, prepare the local directory
 # and pull the files down from the Kiali pod. If there is no Kiali pod deployed,
 # then spit out a warning but keep going.
+# We always prepare the kiali-remote-cluster-secrets location because we will
+# at minimum put our local cluster kubeconfig there (we do that later in the script).
+
+# Unless this dir already exists, it will most likely fail to be created because
+# creating a directory under the root directory usually requires sudo access.
+REMOTE_CLUSTER_SECRETS_DIR="/kiali-remote-cluster-secrets"
+mkdir -p ${REMOTE_CLUSTER_SECRETS_DIR}
+if [ ! -d ${REMOTE_CLUSTER_SECRETS_DIR} ]; then
+  errormsg "You first must prepare the remote cluster secrets directory: sudo mkdir -p ${REMOTE_CLUSTER_SECRETS_DIR}; sudo chmod ugo+w ${REMOTE_CLUSTER_SECRETS_DIR}"
+  exit 1
+fi
+rm -rf ${REMOTE_CLUSTER_SECRETS_DIR}/*
 
+# remote clusters
 if [ "${COPY_CLUSTER_SECRETS}" == "true" ]; then
   infomsg "Attempting to copy the remote cluster secrets from a Kiali pod deployed in the cluster..."
-  POD_NAME="$(${CLIENT_EXE} -n ${ISTIO_NAMESPACE} get pod -l app.kubernetes.io/name=kiali -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)"
+  # Determine the context option for secret operations
+  if [ "${HOME_KUBE_CONTEXT}" != "current" ]; then
+    HOME_CONTEXT_OPT="--context=${HOME_KUBE_CONTEXT}"
+  else
+    HOME_CONTEXT_OPT=""
+  fi
+  POD_NAME="$(${CLIENT_EXE} ${HOME_CONTEXT_OPT} -n ${ISTIO_NAMESPACE} get pod -l app.kubernetes.io/name=kiali -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)"
   if [ -z "${POD_NAME}" ]; then
     warnmsg "Cannot get the Kiali pod name. Kiali must be deployed in [${ISTIO_NAMESPACE}]. If you do not want to deploy Kiali in the cluster, set '--copy-cluster-secrets' to 'false'."
   else
-    infomsg "Will copy remote cluster secrets from the Kiali pod [${ISTIO_NAMESPACE}/${POD_NAME}]"
-
-    # Unless this dir already exists, it will most likely fail to be created because
-    # creating a directory under the root directory usually requires sudo access.
-    REMOTE_CLUSTER_SECRETS_DIR="/kiali-remote-cluster-secrets"
-    mkdir -p ${REMOTE_CLUSTER_SECRETS_DIR}
-    if [ ! -d ${REMOTE_CLUSTER_SECRETS_DIR} ]; then
-      errormsg "You first must prepare the remote cluster secrets directory: sudo mkdir -p ${REMOTE_CLUSTER_SECRETS_DIR}; sudo chmod ugo+w ${REMOTE_CLUSTER_SECRETS_DIR}"
-      exit 1
-    fi
-    rm -rf ${REMOTE_CLUSTER_SECRETS_DIR}/*
+    infomsg "Will copy remote cluster secrets from the Kiali pod [${HOME_KUBE_CONTEXT}/${ISTIO_NAMESPACE}/${POD_NAME}]"
 
     # if the directory doesn't exist, then no remote secrets are available, so skip everything else
-    ${CLIENT_EXE} exec -n ${ISTIO_NAMESPACE} --stdin --tty pod/${POD_NAME} -- ls -d ${REMOTE_CLUSTER_SECRETS_DIR} >&/dev/null
+    ${CLIENT_EXE} ${HOME_CONTEXT_OPT} exec -n ${ISTIO_NAMESPACE} --stdin --tty pod/${POD_NAME} -- ls -d ${REMOTE_CLUSTER_SECRETS_DIR} >&/dev/null
     if [ "$?" == "0" ]; then
-      pod_remote_secrets_dirs=$(${CLIENT_EXE} exec -n ${ISTIO_NAMESPACE} --stdin --tty pod/${POD_NAME} -- ls -1 ${REMOTE_CLUSTER_SECRETS_DIR} | tr -d '\r')
+      pod_remote_secrets_dirs=$(${CLIENT_EXE} ${HOME_CONTEXT_OPT} exec -n ${ISTIO_NAMESPACE} --stdin --tty pod/${POD_NAME} -- ls -1 ${REMOTE_CLUSTER_SECRETS_DIR} | tr -d '\r')
       for d in $pod_remote_secrets_dirs; do
         mkdir -p "${REMOTE_CLUSTER_SECRETS_DIR}/$d"
-        pod_remote_secrets_files=$(${CLIENT_EXE} exec -n ${ISTIO_NAMESPACE} --stdin --tty pod/${POD_NAME} -- ls -1 ${REMOTE_CLUSTER_SECRETS_DIR}/${d} | tr -d '\r')
+        pod_remote_secrets_files=$(${CLIENT_EXE} ${HOME_CONTEXT_OPT} exec -n ${ISTIO_NAMESPACE} --stdin --tty pod/${POD_NAME} -- ls -1 ${REMOTE_CLUSTER_SECRETS_DIR}/${d} | tr -d '\r')
         for f in $pod_remote_secrets_files; do
           infomsg "Copying remote cluster secret file: ${REMOTE_CLUSTER_SECRETS_DIR}/${d}/${f}"
-          secret_file_content=$(${CLIENT_EXE} exec -n ${ISTIO_NAMESPACE} --stdin --tty pod/${POD_NAME} -- cat ${REMOTE_CLUSTER_SECRETS_DIR}/${d}/${f})
+          secret_file_content=$(${CLIENT_EXE} ${HOME_CONTEXT_OPT} exec -n ${ISTIO_NAMESPACE} --stdin --tty pod/${POD_NAME} -- cat ${REMOTE_CLUSTER_SECRETS_DIR}/${d}/${f})
           echo "${secret_file_content}" > ${REMOTE_CLUSTER_SECRETS_DIR}/${d}/${f}
         done
       done
@@ -611,7 +619,8 @@ fi
 # Kiali will report errors in this case because it will be missing the service account, and
 # the current user may have permissions that are different than the Kiali service account.
 # But the benefit of this is that you do not need to have a Kiali deployment in the cluster.
-if [ "${KUBE_CONTEXT}" != "current" ]; then
+REMOTE_SECRET_PATH="${TMP_DIR}/kubeconfig"
+if [ "${HOME_KUBE_CONTEXT}" != "current" ] && ! ${CLIENT_EXE} config get-contexts "${HOME_KUBE_CONTEXT}" >/dev/null 2>&1; then
   TOKEN_FILE="${TMP_DIR}/token"
   CA_FILE="${TMP_DIR}/ca.crt"
 
@@ -651,8 +660,36 @@ EOM
   ${CLIENT_EXE} config set-credentials ${KUBE_CONTEXT} --kubeconfig="${REMOTE_SECRET_PATH}" --token="$(cat ${TOKEN_FILE})"
   ${CLIENT_EXE} config set-context ${KUBE_CONTEXT} --kubeconfig="${REMOTE_SECRET_PATH}" --user=${KUBE_CONTEXT} --cluster=${KUBE_CONTEXT} --namespace=${ISTIO_NAMESPACE}
   ${CLIENT_EXE} config use-context --kubeconfig="${REMOTE_SECRET_PATH}" ${KUBE_CONTEXT}
+
+  # Set the cluster name for the secret when not specifying -kc current
+  if [ -z "${CLUSTER_NAME}" ]; then
+    CLUSTER_NAME_FOR_SECRET=$KUBE_CONTEXT
+  else
+    CLUSTER_NAME_FOR_SECRET="${CLUSTER_NAME}"
+  fi
+else
+  # we are using an existing context - extract the specified context info and create a new kubeconfig
+  if [ "${HOME_KUBE_CONTEXT}" == "current" ]; then
+    infomsg "Extracting current cluster information from user's kubeconfig: $(${CLIENT_EXE} config current-context)"
+    ${CLIENT_EXE} config view --minify --flatten > "${REMOTE_SECRET_PATH}"
+  else
+    infomsg "Extracting context [${HOME_KUBE_CONTEXT}] information from user's kubeconfig"
+    ${CLIENT_EXE} config view --minify --flatten --context="${HOME_KUBE_CONTEXT}" > "${REMOTE_SECRET_PATH}"
+  fi
+
+  # Set the cluster name for the secret
+  if [ -z "${CLUSTER_NAME}" ]; then
+    CLUSTER_NAME_FOR_SECRET=$(${CLIENT_EXE} config view --kubeconfig="${REMOTE_SECRET_PATH}" -o jsonpath='{.contexts[0].context.cluster}')
+  else
+    CLUSTER_NAME_FOR_SECRET="${CLUSTER_NAME}"
+  fi
 fi
 
+# Set up the local cluster with the local kubeconfig
+CLUSTER_NAME_FOR_SECRET="${CLUSTER_NAME_FOR_SECRET:-run-kiali-cluster}"
+mkdir ${REMOTE_CLUSTER_SECRETS_DIR}/${CLUSTER_NAME_FOR_SECRET}
+cp ${REMOTE_SECRET_PATH} ${REMOTE_CLUSTER_SECRETS_DIR}/${CLUSTER_NAME_FOR_SECRET}/${CLUSTER_NAME_FOR_SECRET}
+
 # Functions that port-forward to components we need
 
 start_port_forward_component() {
diff --git a/hack/setup-kind-in-ci.sh b/hack/setup-kind-in-ci.sh
index 9501f7ed2..e59cbb6ff 100755
--- a/hack/setup-kind-in-ci.sh
+++ b/hack/setup-kind-in-ci.sh
@@ -7,6 +7,8 @@
 PRIMARY_REMOTE="primary-remote"
 MULTI_PRIMARY="multi-primary"
 EXTERNAL_CONTROLPLANE="external-controlplane"
+KEYCLOAK_LIMIT_MEMORY=""
+KEYCLOAK_REQUESTS_MEMORY=""
 
 infomsg() {
   echo "[INFO] ${1}"
@@ -34,6 +36,10 @@ Options:
     This option is ignored if -ii is false.
     If not specified, the latest version of Istio is installed.
     Default: <the latest release>
+-klm|--keycloak-limit-memory
+    The keycloak resources limit memory in the keycloak helm charts
+-krm|--keycloak-requests-memory)
+    The keycloak resources requests memory in the keycloak helm charts.
 -mc|--multicluster <${MULTI_PRIMARY}|${PRIMARY_REMOTE}|${EXTERNAL_CONTROLPLANE}>
     Whether to set up a multicluster environment
     and which kind of multicluster environment to setup.
@@ -63,6 +69,8 @@ while [[ $# -gt 0 ]]; do
     -h|--help)                    helpmsg;                    exit 1       ;;
     -hcd|--helm-charts-dir)       HELM_CHARTS_DIR="$2";       shift;shift; ;;
     -iv|--istio-version)          ISTIO_VERSION="$2";         shift;shift; ;;
+    -klm|--keycloak-limit-memory) KEYCLOAK_LIMIT_MEMORY="$2"; shift;shift; ;;
+    -krm|--keycloak-requests-memory) KEYCLOAK_REQUESTS_MEMORY="$2"; shift;shift; ;;
     -mc|--multicluster)
       MULTICLUSTER="${2}"
       if [ "${MULTICLUSTER}" != "${PRIMARY_REMOTE}" -a "${MULTICLUSTER}" != "${MULTI_PRIMARY}" -a "${MULTICLUSTER}" != "${EXTERNAL_CONTROLPLANE}" ]; then
@@ -142,6 +150,8 @@ AUTH_STRATEGY=$AUTH_STRATEGY
 DORP=$DORP
 HELM_CHARTS_DIR=$HELM_CHARTS_DIR
 ISTIO_VERSION=$ISTIO_VERSION
+KEYCLOAK_LIMIT_MEMORY=$KEYCLOAK_LIMIT_MEMORY
+KEYCLOAK_REQUESTS_MEMORY=$KEYCLOAK_REQUESTS_MEMORY
 KIND_NODE_IMAGE=$KIND_NODE_IMAGE
 MULTICLUSTER=$MULTICLUSTER
 SAIL=$SAIL
@@ -201,7 +211,20 @@ setup_kind_singlecluster() {
             --enable-keycloak true \
             --keycloak-certs-dir "${KEYCLOAK_CERTS_DIR}" \
             --keycloak-issuer-uri "https://${KEYCLOAK_ADDRESS}/realms/kube"
-      "${SCRIPT_DIR}/keycloak.sh" -kcd "${KEYCLOAK_CERTS_DIR}" -kip "${KEYCLOAK_ADDRESS}" deploy
+
+      # Optional: keycloak memory limits
+      if [ -n "$KEYCLOAK_LIMIT_MEMORY" ]; then
+        MEMORY_LIMIT_ARG="-slm $KEYCLOAK_LIMIT_MEMORY"
+      else
+        MEMORY_LIMIT_ARG=""
+      fi
+      if [ -n "$KEYCLOAK_REQUESTS_MEMORY" ]; then
+        MEMORY_REQUEST_ARG="-srm $KEYCLOAK_REQUESTS_MEMORY"
+      else
+        MEMORY_REQUEST_ARG=""
+      fi
+
+      "${SCRIPT_DIR}/keycloak.sh" -kcd "${KEYCLOAK_CERTS_DIR}" -kip "${KEYCLOAK_ADDRESS}" $MEMORY_LIMIT_ARG $MEMORY_REQUEST_ARG deploy
 
       keycloak_ip_cl=$(kubectl get svc keycloak -n keycloak -o=jsonpath='{.status.loadBalancer.ingress[0].ip}')
             auth_flags+=(--keycloak-address "${keycloak_ip_cl}")
@@ -410,6 +433,17 @@ setup_kind_multicluster() {
     local kind_node_image="--kind-node-image ${KIND_NODE_IMAGE}"
   fi
 
+  if [ -n "$KEYCLOAK_LIMIT_MEMORY" ]; then
+    MEMORY_LIMIT_ARG="-kml $KEYCLOAK_LIMIT_MEMORY"
+  else
+    MEMORY_LIMIT_ARG=""
+  fi
+  if [ -n "$KEYCLOAK_REQUESTS_MEMORY" ]; then
+    MEMORY_REQUEST_ARG="-krm $KEYCLOAK_REQUESTS_MEMORY"
+  else
+    MEMORY_REQUEST_ARG=""
+  fi
+
   local cluster1_context
   local cluster2_context
   local cluster1_name
@@ -422,6 +456,8 @@ setup_kind_multicluster() {
       --certs-dir "${certs_dir}" \
       -dorp docker \
       --istio-dir "${istio_dir}" \
+      ${MEMORY_REQUEST_ARG} \
+      ${MEMORY_LIMIT_ARG} \
       ${kind_node_image:-} \
       ${hub_arg:-} \
       ${istio_version_arg}
diff --git a/handlers/config.go b/handlers/config.go
index ceb62c526..a63781b6c 100644
--- a/handlers/config.go
+++ b/handlers/config.go
@@ -48,16 +48,17 @@ type PublicConfig struct {
 	AmbientEnabled      bool                          `json:"ambientEnabled,omitempty"`
 	Clusters            map[string]models.KubeCluster `json:"clusters,omitempty"`
 	ClusterWideAccess   bool                          `json:"clusterWideAccess,omitempty"`
+	ControlPlanes       map[string]string             `json:"controlPlanes,omitempty"`
 	Deployment          DeploymentConfig              `json:"deployment,omitempty"`
 	GatewayAPIClasses   []config.GatewayAPIClass      `json:"gatewayAPIClasses,omitempty"`
 	GatewayAPIEnabled   bool                          `json:"gatewayAPIEnabled,omitempty"`
 	HealthConfig        config.HealthConfig           `json:"healthConfig,omitempty"`
+	IgnoreHomeCluster   bool                          `json:"ignoreHomeCluster,omitempty"`
 	InstallationTag     string                        `json:"installationTag,omitempty"`
 	IstioAnnotations    IstioAnnotations              `json:"istioAnnotations,omitempty"`
 	IstioConfigMap      string                        `json:"istioConfigMap"`
 	IstioIdentityDomain string                        `json:"istioIdentityDomain,omitempty"`
 	IstioLabels         config.IstioLabels            `json:"istioLabels,omitempty"`
-	IstioNamespace      string                        `json:"istioNamespace,omitempty"`
 	IstioStatusEnabled  bool                          `json:"istioStatusEnabled,omitempty"`
 	KialiFeatureFlags   config.KialiFeatureFlags      `json:"kialiFeatureFlags,omitempty"`
 	LogLevel            string                        `json:"logLevel,omitempty"`
@@ -79,6 +80,7 @@ func Config(conf *config.Config, cache cache.KialiCache, discovery istio.MeshDis
 			AuthStrategy:      conf.Auth.Strategy,
 			Clusters:          make(map[string]models.KubeCluster),
 			ClusterWideAccess: conf.Deployment.ClusterWideAccess,
+			ControlPlanes:     make(map[string]string),
 			Deployment: DeploymentConfig{
 				ViewOnlyMode: conf.Deployment.ViewOnlyMode,
 			},
@@ -89,9 +91,9 @@ func Config(conf *config.Config, cache cache.KialiCache, discovery istio.MeshDis
 				IstioInjectionAnnotation: conf.ExternalServices.Istio.IstioInjectionAnnotation,
 			},
 			HealthConfig:        conf.HealthConfig,
+			IgnoreHomeCluster:   conf.Clustering.IgnoreHomeCluster,
 			IstioStatusEnabled:  conf.ExternalServices.Istio.ComponentStatuses.Enabled,
 			IstioIdentityDomain: conf.ExternalServices.Istio.IstioIdentityDomain,
-			IstioNamespace:      conf.IstioNamespace,
 			IstioLabels:         conf.IstioLabels,
 			IstioConfigMap:      conf.ExternalServices.Istio.ConfigMapName,
 			KialiFeatureFlags:   conf.KialiFeatureFlags,
@@ -108,7 +110,6 @@ func Config(conf *config.Config, cache cache.KialiCache, discovery istio.MeshDis
 			return
 		}
 
-		// @TODO hardcoded home cluster
 		if client := userClients[conf.KubernetesConfig.ClusterName]; client != nil {
 			publicConfig.GatewayAPIEnabled = client.IsGatewayAPI()
 		}
@@ -125,6 +126,16 @@ func Config(conf *config.Config, cache cache.KialiCache, discovery istio.MeshDis
 
 		for _, cluster := range clusters {
 			publicConfig.Clusters[cluster.Name] = cluster
+
+			if client := userClients[cluster.Name]; client != nil {
+				if namespaces, found := cache.GetNamespaces(cluster.Name, client.GetToken()); found {
+					for _, ns := range namespaces {
+						if discovery.IsControlPlane(r.Context(), cluster.Name, ns.Name) {
+							publicConfig.ControlPlanes[cluster.Name] = ns.Name
+						}
+					}
+				}
+			}
 		}
 
 		RespondWithJSONIndent(w, http.StatusOK, publicConfig)
diff --git a/handlers/dashboards.go b/handlers/dashboards.go
index 634e26a3c..da51a098a 100644
--- a/handlers/dashboards.go
+++ b/handlers/dashboards.go
@@ -1,6 +1,7 @@
 package handlers
 
 import (
+	"fmt"
 	"net/http"
 	"net/url"
 	"strings"
@@ -287,6 +288,11 @@ func ZtunnelDashboard(
 			return
 		}
 
+		if !discovery.IsControlPlane(r.Context(), cluster, namespace) {
+			RespondWithError(w, http.StatusBadRequest, fmt.Sprintf("namespace [%s] is not the control plane namespace", namespace))
+			return
+		}
+
 		metricsService := business.NewMetricsService(prom, conf)
 
 		ztunnelMetrics, err := metricsService.GetZtunnelMetrics(params)
diff --git a/handlers/graph.go b/handlers/graph.go
index 23e84292a..93254b981 100644
--- a/handlers/graph.go
+++ b/handlers/graph.go
@@ -67,7 +67,7 @@ func GraphNamespaces(
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		graph.CheckError(err)
 
-		o := graph.NewOptions(r, &business.Namespace)
+		o := graph.NewOptions(r, business)
 
 		code, payload := api.GraphNamespaces(r.Context(), business, o)
 		respond(w, code, payload)
@@ -91,7 +91,7 @@ func GraphNode(
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		graph.CheckError(err)
 
-		o := graph.NewOptions(r, &business.Namespace)
+		o := graph.NewOptions(r, business)
 
 		code, payload := api.GraphNode(r.Context(), business, o)
 		respond(w, code, payload)
@@ -118,7 +118,7 @@ func handlePanic(ctx context.Context, w http.ResponseWriter) {
 		if code == http.StatusInternalServerError {
 			stack := debug.Stack()
 			log.FromContext(ctx).Error().Msgf("%s: %s", message, stack)
-			RespondWithDetailedError(w, code, message, "Stack trace available in Kiali logs")
+			RespondWithDetailedError(w, code, message, "Stack trace available in Kiali logs:")
 			return
 		}
 		RespondWithError(w, code, message)
diff --git a/handlers/istio_config.go b/handlers/istio_config.go
index d16534a07..6bdd45be3 100644
--- a/handlers/istio_config.go
+++ b/handlers/istio_config.go
@@ -420,7 +420,7 @@ func IstioConfigPermissions(
 		}
 
 		if !business.Mesh.IsValidCluster(cluster) {
-			RespondWithError(w, http.StatusBadRequest, "Cluster %s does not exist "+cluster)
+			RespondWithError(w, http.StatusBadRequest, "Cluster [%s] does not exist "+cluster)
 			return
 		}
 
diff --git a/handlers/metrics.go b/handlers/metrics.go
index bd8f22acc..15f88e121 100644
--- a/handlers/metrics.go
+++ b/handlers/metrics.go
@@ -197,14 +197,15 @@ func ControlPlaneMetrics(
 
 		vars := mux.Vars(r)
 		namespace := vars["namespace"]
-		if namespace != conf.IstioNamespace {
+		controlPlane := vars["controlplane"]
+		conf := config.Get()
+		cluster := clusterNameFromQuery(conf, r.URL.Query())
+
+		if !discovery.IsControlPlane(r.Context(), cluster, namespace) {
 			RespondWithError(w, http.StatusBadRequest, fmt.Sprintf("namespace [%s] is not the control plane namespace", namespace))
 			return
 		}
 
-		controlPlane := vars["controlplane"]
-		cluster := clusterNameFromQuery(conf, r.URL.Query())
-
 		namespaceInfo, err := checkNamespaceAccessWithService(w, r, &layer.Namespace, namespace, cluster)
 		if err != nil {
 			// any returned value nil means error & response already written
@@ -254,8 +255,14 @@ func ResourceUsageMetrics(conf *config.Config, cache cache.KialiCache, discovery
 		vars := mux.Vars(r)
 		namespace := vars["namespace"]
 		app := vars["app"]
+		conf := config.Get()
 		cluster := clusterNameFromQuery(conf, r.URL.Query())
 
+		if !discovery.IsControlPlane(r.Context(), cluster, namespace) {
+			RespondWithError(w, http.StatusBadRequest, fmt.Sprintf("namespace [%s] is not the control plane namespace", namespace))
+			return
+		}
+
 		namespaceInfo, err := checkNamespaceAccess(w, r, conf, cache, discovery, clientFactory, namespace, cluster)
 		if err != nil {
 			// any returned value nil means error & response already written
diff --git a/handlers/tracing.go b/handlers/tracing.go
index d9423f942..0844360bb 100644
--- a/handlers/tracing.go
+++ b/handlers/tracing.go
@@ -1,7 +1,6 @@
 package handlers
 
 import (
-	"context"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -407,11 +406,12 @@ func TracingDiagnose(
 	return func(w http.ResponseWriter, r *http.Request) {
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
-			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
+			RespondWithError(w, http.StatusInternalServerError, "TracingDiagnose getLayer error: "+err.Error())
 			return
 		}
-		if !isHomeCPAccessible(r.Context(), conf, business.Namespace, clientFactory.GetSAHomeClusterClient().ClusterInfo().Name) {
-			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
+
+		if !business.Namespace.HasMeshAccess(r.Context(), clientFactory.GetSAHomeClusterClient().ClusterInfo().Name) {
+			RespondWithError(w, http.StatusInternalServerError, "TracingDiagnose unauthorized")
 			return
 		}
 
@@ -451,11 +451,12 @@ func TracingConfigurationCheck(
 
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
-			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
+			RespondWithError(w, http.StatusInternalServerError, "TracingConfigurationCheck getLayer error: "+err.Error())
 			return
 		}
-		if !isHomeCPAccessible(r.Context(), conf, business.Namespace, clientFactory.GetSAHomeClusterClient().ClusterInfo().Name) {
-			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
+
+		if !business.Namespace.HasMeshAccess(r.Context(), clientFactory.GetSAHomeClusterClient().ClusterInfo().Name) {
+			RespondWithError(w, http.StatusInternalServerError, "TracingConfigurationCheck Unauthorized")
 			return
 		}
 
@@ -463,9 +464,3 @@ func TracingConfigurationCheck(
 		RespondWithJSON(w, http.StatusOK, status)
 	}
 }
-
-// isHomeCPAccessible Check access to the home istio namespace
-func isHomeCPAccessible(ctx context.Context, conf *config.Config, namespaceService business.NamespaceService, cluster string) bool {
-	_, err := namespaceService.GetClusterNamespace(ctx, conf.IstioNamespace, cluster)
-	return err == nil
-}
diff --git a/istio/discovery.go b/istio/discovery.go
index e662fc13c..07f6756b8 100644
--- a/istio/discovery.go
+++ b/istio/discovery.go
@@ -24,6 +24,7 @@ import (
 	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/models"
 	"github.com/kiali/kiali/observability"
+	"github.com/kiali/kiali/util/sliceutil"
 )
 
 const (
@@ -188,16 +189,16 @@ func setSharedConfig(controlPlane *models.ControlPlane, controlPlaneConf *models
 		ctrlclient.ObjectKey{Name: controlPlane.SharedMeshConfig, Namespace: controlPlane.IstiodNamespace},
 		sharedUserConfigMap,
 	); err != nil {
-		return fmt.Errorf("unable to get Shared User ConfigMap %s in namespace %s on cluster %s: %s", controlPlane.SharedMeshConfig, controlPlane.IstiodNamespace, controlPlane.Cluster.Name, err)
+		return fmt.Errorf("unable to get Shared User ConfigMap [%s] in namespace [%s] on cluster [%s]: %s", controlPlane.SharedMeshConfig, controlPlane.IstiodNamespace, controlPlane.Cluster.Name, err)
 	}
 
 	if err := parseIstioConfigMap(sharedUserConfigMap, sharedConfig.ConfigMap); err != nil {
-		return fmt.Errorf("unable to parse Shared User ConfigMap %s in namespace %s on cluster %s: %s", controlPlane.SharedMeshConfig, controlPlane.IstiodNamespace, controlPlane.Cluster.Name, err)
+		return fmt.Errorf("unable to parse Shared User ConfigMap [%s] in namespace [%s] on cluster [%s]: %s", controlPlane.SharedMeshConfig, controlPlane.IstiodNamespace, controlPlane.Cluster.Name, err)
 	}
 
 	// Unmarshal again into effective.
 	if err := parseIstioConfigMap(sharedUserConfigMap, controlPlaneConf.EffectiveConfig.ConfigMap); err != nil {
-		return fmt.Errorf("unable to parse Shared User ConfigMap %s in namespace %s on cluster %s into EffectiveConfig: %s", controlPlane.SharedMeshConfig, controlPlane.IstiodNamespace, controlPlane.Cluster.Name, err)
+		return fmt.Errorf("unable to parse Shared User ConfigMap [%s] in namespace [%s] on cluster [%s] into EffectiveConfig: %s", controlPlane.SharedMeshConfig, controlPlane.IstiodNamespace, controlPlane.Cluster.Name, err)
 	}
 
 	controlPlaneConf.SharedConfig = sharedConfig
@@ -273,6 +274,8 @@ func sidecarInjectorConfigMapName(revision string) string {
 
 type MeshDiscovery interface {
 	Clusters() ([]models.KubeCluster, error)
+	GetControlPlaneNamespaces(ctx context.Context, cluster string) []string
+	IsControlPlane(ctx context.Context, cluster, namespace string) bool
 	Mesh(ctx context.Context) (*models.Mesh, error)
 }
 
@@ -292,6 +295,38 @@ func NewDiscovery(clients map[string]kubernetes.ClientInterface, cache cache.Kia
 	}
 }
 
+// GetControlPlaneNamespaces returns control plane namespaces for the cluster. If cluster == "" then
+// it is for all clusters. it returns an empty slice.
+func (in *Discovery) GetControlPlaneNamespaces(ctx context.Context, cluster string) []string {
+	namespaces := map[string]bool{}
+
+	mesh, err := in.Mesh(ctx)
+	if err != nil {
+		log.Debugf("Unable to get mesh to determine control plane namespaces for cluster [%s]. Err: %s", cluster, err)
+		return maps.Keys(namespaces)
+	}
+
+	for _, cp := range mesh.ControlPlanes {
+		if cluster == "" || cluster == cp.Cluster.Name {
+			namespaces[cp.IstiodNamespace] = true
+		}
+	}
+
+	return maps.Keys(namespaces)
+}
+
+// IsControlPlane returns true if the cluster-namespace is an istio control plane. If cluster == "" it
+// is ignored, and only the namespace is considered. Otherwise false.
+func (in *Discovery) IsControlPlane(ctx context.Context, cluster, namespace string) bool {
+	for _, cpns := range in.GetControlPlaneNamespaces(ctx, cluster) {
+		if namespace == cpns {
+			return true
+		}
+	}
+
+	return false
+}
+
 // IsRemoteCluster determines if the cluster has a controlplane or if it's a remote cluster without one.
 // Clusters that do not exist or are not accessible are considered remote clusters.
 func (in *Discovery) IsRemoteCluster(ctx context.Context, cluster string) bool {
@@ -319,8 +354,8 @@ func (in *Discovery) Clusters() ([]models.KubeCluster, error) {
 		return clusters, nil
 	}
 
-	// Even if somehow there are no clusters found, which there should always be at least the homecluster,
-	// setting this to an empty slice will prevent us from trying to resolve again.
+	// Even if somehow there are no clusters found, setting this to an empty slice will prevent us
+	// from trying to resolve again.
 	clustersByName := map[string]models.KubeCluster{}
 	for cluster, client := range in.kialiSAClients {
 		info := client.ClusterInfo()
@@ -359,8 +394,9 @@ func (in *Discovery) Clusters() ([]models.KubeCluster, error) {
 	}
 
 	// TODO: Separate KialiInstance from Cluster model.
-	for idx := range clusters {
-		cluster := &clusters[idx]
+	for i := range clusters {
+		// need the actual object for update, so use the index, not the copy
+		cluster := &clusters[i]
 		instances, err := in.getKialiInstances(*cluster)
 		if err != nil {
 			log.Warningf("Unable to get Kiali instances for cluster [%s]: %v", cluster.Name, err)
@@ -385,7 +421,7 @@ type clusterRevisionKey struct {
 func (in *Discovery) Mesh(ctx context.Context) (*models.Mesh, error) {
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "Mesh",
-		observability.Attribute("package", "business"),
+		observability.Attribute("package", "istio"),
 	)
 	defer end()
 
@@ -412,7 +448,22 @@ func (in *Discovery) Mesh(ctx context.Context) (*models.Mesh, error) {
 			return nil, err
 		}
 
-		// If there's an istiod on it, then it's a controlplane cluster. Otherwise it is a remote cluster.
+		// if this is an external kiali cluster, don't look for control planes but ensure that Kiali is found
+		if in.conf.Clustering.IgnoreHomeCluster && in.conf.KubernetesConfig.ClusterName == cluster.Name {
+			log.Tracef("Cluster [%s] is an external Kiali cluster. Adding the external management cluster.", cluster.Name)
+			externalKialis := in.discoverKiali(cluster)
+			if len(externalKialis) != 0 {
+				mesh.ExternalKiali = &models.ExternalKialiInstance{
+					Cluster: &cluster,
+					Kiali:   &externalKialis[0],
+				}
+			} else {
+				log.Errorf("cluster [%s] is an external Kiali cluster. But a Kiali instance was not found", cluster.Name)
+			}
+			continue
+		}
+
+		// If there's an istiod on it, then it's a controlplane cluster. Otherwise it is a remote mesh cluster
 		labels := map[string]string{config.IstioAppLabel: istiodAppLabelValue}
 		depList := &appsv1.DeploymentList{}
 		err = kubeCache.List(ctx, depList, ctrlclient.MatchingLabels(labels))
@@ -653,8 +704,16 @@ func (in *Discovery) Mesh(ctx context.Context) (*models.Mesh, error) {
 
 			namespaces := FilterNamespacesWithDiscoverySelectors(
 				models.CastNamespaceCollection(k8sNamespaces, cluster.Name),
-				GetDiscoverySelectorsForCluster(cluster.Name, in.conf),
+				GetKialiDiscoverySelectors([]string{}, cluster.Name, in.conf),
 			)
+			// add control plane namespaces, which should always be included
+			for _, cp := range controlPlanesByClusterName[cluster.Name] {
+				if !sliceutil.Some(namespaces, func(ns models.Namespace) bool {
+					return ns.Name == cp.IstiodNamespace
+				}) {
+					namespaces = append(namespaces, models.Namespace{Cluster: cluster.Name, Name: cp.IstiodNamespace})
+				}
+			}
 
 			for _, n := range namespaces {
 				rev := GetRevision(n)
diff --git a/istio/discovery_selectors.go b/istio/discovery_selectors.go
index 7e2c3268f..d626f99be 100644
--- a/istio/discovery_selectors.go
+++ b/istio/discovery_selectors.go
@@ -1,6 +1,7 @@
 package istio
 
 import (
+	"context"
 	"regexp"
 
 	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -9,6 +10,7 @@ import (
 	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/models"
+	"github.com/kiali/kiali/util/sliceutil"
 )
 
 var systemNamespaceRegex = regexp.MustCompile(`^(kube-.*|openshift.*|ibm.*|kiali-operator|istio-operator)`)
@@ -17,33 +19,37 @@ var systemNamespaceRegex = regexp.MustCompile(`^(kube-.*|openshift.*|ibm.*|kiali
 // If the cluster has overrides defined in the Kiali config, those overrides will be returned.
 // If there are no overrides, but default discovery selectors are defined in the Kiali config, those will be returned.
 // If there are no selectors defined in the Kiali config, nil is returned (Istio's own discovery selectors will be ignored).
-// kialiConfig argument may be nil - if so, they are ignored and nil is returned.
-func GetDiscoverySelectorsForCluster(cluster string, kialiConfig *config.Config) config.DiscoverySelectorsType {
-	ds := GetKialiDiscoverySelectors(cluster, kialiConfig)
+// kialiConf argument may be nil - if so, they are ignored and nil is returned.
+func GetDiscoverySelectorsForCluster(ctx context.Context, discovery MeshDiscovery, cluster string, kialiConf *config.Config) config.DiscoverySelectorsType {
+	if kialiConf == nil {
+		return nil
+	}
+
+	cpNamespaces := discovery.GetControlPlaneNamespaces(ctx, cluster)
+	ds := GetKialiDiscoverySelectors(cpNamespaces, cluster, kialiConf)
 	return ds
 }
 
-// getKialiDiscoverySelectors will return the discovery selectors applicable for the named cluster as configured
+// GetKialiDiscoverySelectors will return the discovery selectors applicable for the named cluster as configured
 // in the Kiali config (this func does nothing with Istio discovery selectors - it is only concerned with the Kiali config).
 // If the cluster has overrides defined in the Kiali config, those overrides will be returned.
 // If there are no overrides, but default discovery selectors are defined in the Kiali config, those will be returned.
 // If there are no selectors defined in the Kiali config, nil is returned.
-// If selectors are defined, this function will always return one more than what was configured - this extra selector
-// will select the control plane namespace as defined in the Kiali config in order to assure Kiali will always match
-// the control plane namespace (Kiali should always see that namespace).
-// NOTE: You probably don't want to use this func; instead, see getDiscoverySelectorsForCluster()
-func GetKialiDiscoverySelectors(cluster string, conf *config.Config) config.DiscoverySelectorsType {
+// If selectors are defined, the function will return the selectors, and as a convenience, additional selectors for
+// any provided cpNamespaces (for a set that ensures we always have access to control-plane namespaces).
+// NOTE: This is mainly a test hook, You probably want to use getDiscoverySelectorsForCluster()
+func GetKialiDiscoverySelectors(cpNamespaces []string, cluster string, conf *config.Config) config.DiscoverySelectorsType {
 	if conf == nil {
 		return nil
 	}
 
-	cpNamespaceSelector := config.DiscoverySelectorsType{
-		&config.DiscoverySelectorType{
+	cpNamespaceSelectors := sliceutil.Map(cpNamespaces, func(ns string) *config.DiscoverySelectorType {
+		return &config.DiscoverySelectorType{
 			MatchLabels: map[string]string{
-				"kubernetes.io/metadata.name": conf.IstioNamespace,
+				"kubernetes.io/metadata.name": ns,
 			},
-		},
-	}
+		}
+	})
 
 	dsConfig := conf.Deployment.DiscoverySelectors
 
@@ -51,14 +57,14 @@ func GetKialiDiscoverySelectors(cluster string, conf *config.Config) config.Disc
 	dsOverrides := dsConfig.Overrides
 	if dsOverrides != nil {
 		if dsCluster, ok := dsOverrides[cluster]; ok {
-			return append(cpNamespaceSelector, dsCluster...)
+			return append(cpNamespaceSelectors, dsCluster...)
 		}
 	}
 
 	// there are no overrides for the given cluster, see if we have defaults that we can fallback to
 	dsDefault := dsConfig.Default
 	if dsDefault != nil {
-		return append(cpNamespaceSelector, dsDefault...)
+		return append(cpNamespaceSelectors, dsDefault...)
 	}
 
 	// there are no discovery selectors configured within the Kiali config; return nil to indicate this
diff --git a/kiali.go b/kiali.go
index 65e50c7cf..9419d78af 100644
--- a/kiali.go
+++ b/kiali.go
@@ -123,19 +123,20 @@ func main() {
 		config.Set(config.NewConfig())
 	}
 
-	updateConfigWithIstioInfo()
-
-	conf := config.Get()
-	log.Tracef("Kiali Configuration:\n%s", conf)
-
-	if err := config.Validate(*conf); err != nil {
+	if err := config.Validate(config.Get()); err != nil {
+		log.Debugf("Kiali Configuration before auto-discovery:\n%s", config.Get())
 		log.Fatal(err)
 	}
 
 	// prepare our internal metrics so Prometheus can scrape them
 	internalmetrics.RegisterInternalMetrics()
 
-	// Create the business package dependencies.
+	// determine the Kiali home cluster name. If necessary, this will try to autodiscover the Istiod cluster name
+	if err := determineHomeClusterName(); err != nil {
+		log.Fatalf("Failed to determine Kiali home cluster name. Err: %s", err)
+	}
+
+	// create the business package dependencies.
 	clientFactory, err := kubernetes.GetClientFactory()
 	if err != nil {
 		log.Fatalf("Failed to create client factory. Err: %s", err)
@@ -145,6 +146,23 @@ func main() {
 	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
 
+	// fetch a fresh config here, it could have been updated during auto-discovery
+	conf := config.Get()
+
+	// ensure the config is set to Kiali's home cluster name. Note that this may be accessed via a remote secret
+	if clientFactory.GetSAHomeClusterClient().ClusterInfo().Name != conf.KubernetesConfig.ClusterName {
+		conf.KubernetesConfig.ClusterName = clientFactory.GetSAHomeClusterClient().ClusterInfo().Name
+	}
+	// special case handling for run-kiali.sh or other "local" modes. If the the ONLY cluster is the local
+	// cluster, don't ignore it, even if we're accessing it via a remote secret.
+	if conf.Clustering.IgnoreHomeCluster && len(clientFactory.GetSAClients()) == 1 {
+		log.Debugf("Setting IgnoreHomeCluster=false, because the home cluster is the only cluster.")
+		conf.Clustering.IgnoreHomeCluster = false
+	}
+	config.Set(conf)
+
+	log.Tracef("Kiali Configuration after auto-discovery:\n%s", conf)
+
 	mgr, kubeCaches, err := newManager(ctx, conf, &zl, clientFactory)
 	if err != nil {
 		log.Fatalf("Unable to setup manager: %s", err)
@@ -223,7 +241,7 @@ func main() {
 
 	// Not totally sure we need to call this for all clusters but better to be safe.
 	for cluster, cache := range kubeCaches {
-		log.Infof("Waiting for cluster: %s cache to sync", cluster)
+		log.Infof("Waiting for cluster [%s] cache to sync", cluster)
 
 		if !cache.WaitForCacheSync(ctx) {
 			log.Fatal("Timed out waiting for cache to sync")
@@ -237,15 +255,15 @@ func main() {
 		// that tries to access these resources will block until the informer is synced
 		// which can take a long time.
 		if _, err := layer.Workload.GetAllWorkloads(ctx, cluster, ""); err != nil {
-			log.Warningf("Unable to get workloads to sync cache for cluster %s. First request that accesses workloads may take awhile: %v", cluster, err)
+			log.Warningf("Unable to get workloads to sync cache for cluster [%s]. First request that accesses workloads may take awhile: %v", cluster, err)
 		}
 
 		if _, err := layer.IstioConfig.GetIstioConfigList(ctx, cluster, business.IstioConfigCriteria{
 			IncludeGateways:               true,
 			IncludeK8sGateways:            true,
-			IncludeK8sInferencePools:      true,
 			IncludeK8sGRPCRoutes:          true,
 			IncludeK8sHTTPRoutes:          true,
+			IncludeK8sInferencePools:      true,
 			IncludeK8sTCPRoutes:           true,
 			IncludeK8sTLSRoutes:           true,
 			IncludeVirtualServices:        true,
@@ -262,7 +280,7 @@ func main() {
 			IncludeTelemetry:              true,
 			IncludeK8sReferenceGrants:     true,
 		}); err != nil {
-			log.Warningf("Unable to get Istio config to sync cache for cluster %s. First request that accesses Istio config may take awhile: %v", cluster, err)
+			log.Warningf("Unable to get Istio config to sync cache for cluster [%s]. First request that accesses Istio config may take awhile: %v", cluster, err)
 		}
 	}
 	log.Info("All caches synced")
@@ -331,17 +349,23 @@ func determineContainerVersion(defaultVersion string) string {
 
 // This is used to update the config with information about istio that
 // comes from the environment such as the cluster name.
-func updateConfigWithIstioInfo() {
-	conf := *config.Get()
+func determineHomeClusterName() error {
+	conf := config.Get()
 
+	// If the home cluster name is already set, we don't need to do anything
 	homeCluster := conf.KubernetesConfig.ClusterName
 	if homeCluster != "" {
-		// If the cluster name is already set, we don't need to do anything
-		return
+		return nil
 	}
 
+	// If the cluster name is not set and we don't have a co-located control plane, it's an error
+	if conf.Clustering.IgnoreHomeCluster {
+		return fmt.Errorf("could not determine Kiali home cluster name. You must set kubernetes_config.cluster_name when clustering.ignore_home_cluster=true")
+	}
+
+	// use the control plane's configured cluster name, or the default
 	err := func() error {
-		log.Debug("Cluster name is not set. Attempting to auto-detect the cluster name from the home cluster environment.")
+		log.Debug("Cluster name is not set. Attempting to auto-detect the cluster name from the Istio control plane environment.")
 		ctx, cancel := context.WithCancel(context.Background())
 		defer cancel()
 
@@ -373,7 +397,9 @@ func updateConfigWithIstioInfo() {
 
 	log.Debugf("Auto-detected the istio cluster name to be [%s]. Updating the kiali config", homeCluster)
 	conf.KubernetesConfig.ClusterName = homeCluster
-	config.Set(&conf)
+	config.Set(conf)
+
+	return nil
 }
 
 func asReaders(caches map[string]ctrlcache.Cache) map[string]client.Reader {
@@ -397,8 +423,9 @@ func newManager(ctx context.Context, conf *config.Config, logger *zerolog.Logger
 		return nil, nil, fmt.Errorf("error setting up manager when creating scheme: %s", err)
 	}
 
-	// In the future this could be any cluster and not just home cluster.
+	// This could be any cluster and not just the local cluster.
 	homeClusterInfo := clientFactory.GetSAHomeClusterClient().ClusterInfo()
+
 	var defaultNamespaces map[string]ctrlcache.Config
 	if !conf.AllNamespacesAccessible() {
 		defaultNamespaces = make(map[string]ctrlcache.Config)
diff --git a/kubernetes/client.go b/kubernetes/client.go
index 47c6a14bc..aa3363711 100644
--- a/kubernetes/client.go
+++ b/kubernetes/client.go
@@ -2,6 +2,7 @@ package kubernetes
 
 import (
 	"context"
+	"path/filepath"
 	"sync"
 
 	osappsclient "github.com/openshift/client-go/apps/clientset/versioned"
@@ -14,7 +15,7 @@ import (
 	"k8s.io/apimachinery/pkg/version"
 	kube "k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/rest"
-	"k8s.io/client-go/tools/clientcmd"
+
 	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
 	inferenceapiclient "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned"
 	gatewayapiclient "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"
@@ -146,14 +147,9 @@ func NewClientWithRemoteClusterInfo(config *rest.Config, remoteClusterInfo *Remo
 	}
 
 	if remoteClusterInfo != nil {
-		cfg, err := remoteClusterInfo.Config.RawConfig()
-		if err != nil {
-			return nil, err
-		}
-
-		clusterName := getClusterName(&cfg)
+		// the secret file name is the cluster name, the kube context may differ
 		client.clusterInfo = ClusterInfo{
-			Name:       clusterName,
+			Name:       filepath.Base(remoteClusterInfo.SecretFile),
 			SecretName: remoteClusterInfo.SecretName,
 		}
 	} else {
@@ -168,25 +164,15 @@ func NewClientWithRemoteClusterInfo(config *rest.Config, remoteClusterInfo *Remo
 	return client, nil
 }
 
-// getConfigForLocalCluster return a client with the correct configuration
-// Returns configuration if Kiali is in Cluster when InCluster is true
-// Returns configuration if Kiali is not in Cluster when InCluster is false
+// getConfigForLocalCluster returns the config for the cluster on which Kiali is running.
 // It returns an error on any problem
 func getConfigForLocalCluster() (*rest.Config, error) {
-	remoteSecretPath := kialiconfig.Get().Deployment.RemoteSecretPath
-	if remoteSecret, readErr := GetRemoteSecret(remoteSecretPath); readErr == nil {
-		log.Debugf("Using remote secret for local cluster config found at: [%s]. Kiali must be running outside the kube cluster.", remoteSecretPath)
-		return clientcmd.NewDefaultClientConfig(*remoteSecret, nil).ClientConfig()
-	} else {
-		log.Debugf("Unable to read remote secret. It may or may not exist. Error: %v. Falling back to in cluster config", readErr)
-		// Fallback to in cluster config
-		incluster, err := rest.InClusterConfig()
-		if err != nil {
-			log.Errorf("Error '%v' getting config for local cluster", err.Error())
-			return nil, err
-		}
-		return incluster, nil
+	incluster, err := rest.InClusterConfig()
+	if err != nil {
+		log.Errorf("Error '%v' getting config for local cluster", err.Error())
+		return nil, err
 	}
+	return incluster, nil
 }
 
 // newClientFromConfig creates a new client to the Kubernetes and Istio APIs.
diff --git a/kubernetes/client_factory.go b/kubernetes/client_factory.go
index a38c9b994..302aa81f0 100644
--- a/kubernetes/client_factory.go
+++ b/kubernetes/client_factory.go
@@ -53,8 +53,8 @@ type clientFactory struct {
 	// Keyed by hash code generated from auth data.
 	clientEntries map[string]map[string]UserClientInterface // By token by cluster
 
-	// Name of the home cluster. This is the cluster where Kiali is deployed which is usually the
-	// "in cluster" config. This name comes from the istio cluster id.
+	// Name of the cluster on which Kiali is deployed. If Istio is co-deployed this will be Istio's clusterID, otherwise it
+	// will be set to the local config Host.
 	homeCluster string
 
 	kialiConfig *kialiConfig.Config
@@ -87,29 +87,34 @@ func GetClientFactory() (ClientFactory, error) {
 		if factory != nil {
 			return
 		}
-
-		factory, err = getClientFactory(*kialiConfig.Get())
+		factory, err = getClientFactory(kialiConfig.Get())
 	})
 	return factory, err
 }
 
-func getClientFactory(conf kialiConfig.Config) (*clientFactory, error) {
-	// Get the normal configuration
-	var config *rest.Config
-	config, err := getConfigForLocalCluster()
-	if err != nil {
-		return nil, err
+func getClientFactory(kialiConf *kialiConfig.Config) (*clientFactory, error) {
+	// Create a new config but don't specify the bearer token to use
+	baseConfig := rest.Config{
+		QPS:   kialiConf.KubernetesConfig.QPS,
+		Burst: kialiConf.KubernetesConfig.Burst,
 	}
 
-	// Create a new config based on what was gathered above but don't specify the bearer token to use
-	baseConfig := rest.Config{
-		Host:            config.Host, // TODO: do we need this? remote cluster clients should ignore this
-		TLSClientConfig: config.TLSClientConfig,
-		QPS:             conf.KubernetesConfig.QPS,
-		Burst:           conf.KubernetesConfig.Burst,
+	// always try to use the local cluster first. this should work unless running kiali
+	// outside of a cluster (like with run-kiali.sh).
+	restConfig, err := getConfigForLocalCluster()
+	if err == nil {
+		baseConfig.Host = restConfig.Host // remote cluster clients should ignore this
+		baseConfig.TLSClientConfig = restConfig.TLSClientConfig
+		return newClientFactory(kialiConf, &baseConfig)
+	}
+
+	// if the local cluster doesn't work, and ignore_home_cluster=false, it's an error. Otherwise,
+	// we need to look for a remote secret with the home cluster name, and use that cluster to run kiali.
+	if !kialiConf.Clustering.IgnoreHomeCluster {
+		return nil, err
 	}
 
-	return newClientFactory(&conf, &baseConfig)
+	return newClientFactory(kialiConf, &baseConfig)
 }
 
 // NewClientFactory creates a new client factory that can be transitory.
@@ -117,7 +122,7 @@ func getClientFactory(conf kialiConfig.Config) (*clientFactory, error) {
 // Does not set the global ClientFactory. You should probably use
 // GetClientFactory instead of this method unless you temporaily need
 // to create a client like when Kiali sets the cluster id.
-func NewClientFactory(ctx context.Context, conf kialiConfig.Config) (ClientFactory, error) {
+func NewClientFactory(ctx context.Context, conf *kialiConfig.Config) (ClientFactory, error) {
 	cf, err := getClientFactory(conf)
 	if err != nil {
 		return nil, err
@@ -135,15 +140,16 @@ func NewClientFactory(ctx context.Context, conf kialiConfig.Config) (ClientFacto
 
 // newClientFactory allows for specifying the config and expiry duration
 // Mock friendly for testing purposes
-func newClientFactory(conf *kialiConfig.Config, restConfig *rest.Config) (*clientFactory, error) {
+func newClientFactory(kialiConf *kialiConfig.Config, restConf *rest.Config) (*clientFactory, error) {
 	f := &clientFactory{
-		kialiConfig:     conf,
-		baseRestConfig:  restConfig,
+		kialiConfig:     kialiConf,
+		baseRestConfig:  restConf,
 		clientEntries:   make(map[string]map[string]UserClientInterface),
+		homeCluster:     kialiConf.KubernetesConfig.ClusterName,
 		recycleChan:     make(chan string),
 		saClientEntries: make(map[string]UserClientInterface),
-		homeCluster:     kialiConfig.Get().KubernetesConfig.ClusterName,
 	}
+
 	// after creating a client factory
 	// background goroutines will be watching the clients` expiration
 	// if a client is expired, it will be removed from clientEntries
@@ -156,25 +162,43 @@ func newClientFactory(conf *kialiConfig.Config, restConfig *rest.Config) (*clien
 	}
 	f.remoteClusterInfos = remoteClusterInfos
 
-	// Create serivce account clients for the home cluster and each remote cluster.
-	// Note that this means each remote cluster secret token must be given the proper permissions
-	// in that remote cluster for Kiali to do its work. i.e. logging into a remote cluster with the
-	// remote cluster secret token must be given the same permissions as the local cluster Kiali SA.
-	homeClient, err := f.newSAClient(nil)
-	if err != nil {
-		return nil, fmt.Errorf("unable to create home cluster Kiali Service Account client. Err: %s", err)
-	}
-
-	f.saClientEntries[f.homeCluster] = homeClient
-
 	for cluster, clusterInfo := range remoteClusterInfos {
 		client, err := f.newSAClient(&clusterInfo)
 		if err != nil {
 			return nil, fmt.Errorf("unable to create remote Kiali Service Account client. Err: %s", err)
 		}
+		log.Debugf("Adding saClientEntry [%s]", cluster)
 		f.saClientEntries[cluster] = client
 	}
 
+	// if the baseRestConfig has a host set, then we're running in-cluster, create the local sa client. Otherwise,
+	// ensure home cluster has a remote secret, because we're going to run kiali remotely (out of cluster), like with run-kiali.sh
+	if restConf.Host != "" {
+		homeClient, err := f.newSAClient(nil)
+		if err != nil {
+			return nil, fmt.Errorf("unable to create home cluster Kiali Service Account client. Err: %s", err)
+		}
+
+		if f.homeCluster == "" {
+			f.homeCluster = restConf.Host
+		}
+		log.Debugf("Adding home saClientEntry [%s]", f.homeCluster)
+		f.saClientEntries[f.homeCluster] = homeClient
+
+	} else if f.saClientEntries[f.homeCluster] == nil {
+		return nil, fmt.Errorf("kiali will exit because kiali is running out-of-cluster, but there is no remote secret defined for the home cluster [%s]", f.homeCluster)
+	}
+
+	// sanity check that we have at least one cluster to manage
+	if len(f.saClientEntries) == 0 {
+		return nil, fmt.Errorf("kiali will exit because it has no local or remote cluster to manage. No remote secrets found")
+	}
+
+	// warning check to ensure a remote cluster when ignore_home_cluster=true, should be more >= 2, but I guess it's not fatal if it's only 1
+	if kialiConf.Clustering.IgnoreHomeCluster && len(f.saClientEntries) < 2 {
+		log.Warningf("Only one remote cluster detected. clustering.ignore_home_cluster=true but Kiali seems to be running on the same remote cluster as the mesh.")
+	}
+
 	return f, nil
 }
 
@@ -222,7 +246,7 @@ func (cf *clientFactory) newClient(authInfo *api.AuthInfo, expirationTime time.D
 	if cluster == cf.homeCluster {
 		client, err := NewClientWithRemoteClusterInfo(&config, nil)
 		if err != nil {
-			log.Errorf("Error creating client for cluster %s: %s", cluster, err.Error())
+			log.Errorf("Error creating client for cluster [%s]: %s", cluster, err.Error())
 			return nil, err
 		}
 		newClient = client
@@ -267,7 +291,7 @@ func (cf *clientFactory) newClient(authInfo *api.AuthInfo, expirationTime time.D
 
 // newSAClient returns a new client for the given cluster. If clusterInfo is nil then a client for the local cluster is returned.
 func (cf *clientFactory) newSAClient(remoteClusterInfo *RemoteClusterInfo) (*K8SClient, error) {
-	log.Debug("Creating new Kiali Service Account client")
+	log.Debugf("Creating new Kiali Service Account client [%v]", remoteClusterInfo)
 	// if no cluster info is provided, we are being asked to create a new client for the home cluster
 	config, err := cf.getConfig(remoteClusterInfo)
 	if err != nil {
@@ -465,7 +489,7 @@ func (cf *clientFactory) GetSAHomeClusterClient() ClientInterface {
 }
 
 func (cf *clientFactory) getConfig(clusterInfo *RemoteClusterInfo) (*rest.Config, error) {
-	kialiConfig := kialiConfig.Get()
+	kialiConf := kialiConfig.Get()
 	clientConfig := *cf.baseRestConfig
 
 	// Remote Cluster
@@ -479,7 +503,7 @@ func (cf *clientFactory) getConfig(clusterInfo *RemoteClusterInfo) (*rest.Config
 		clientConfig = *remoteConfig
 	} else {
 		// Just read the token and then use the base config.
-		// We're an in cluster client. Read the kiali service account token.
+		// We're an in-cluster client. Read the kiali service account token.
 		kialiToken, kialiTokenFile, err := GetKialiTokenForHomeCluster()
 		if err != nil {
 			return nil, fmt.Errorf("unable to get Kiali service account token: %s", err)
@@ -490,13 +514,13 @@ func (cf *clientFactory) getConfig(clusterInfo *RemoteClusterInfo) (*rest.Config
 		clientConfig.BearerTokenFile = kialiTokenFile
 	}
 
-	if !kialiConfig.KialiFeatureFlags.Clustering.EnableExecProvider {
+	if !kialiConf.KialiFeatureFlags.Clustering.EnableExecProvider {
 		clientConfig.ExecProvider = nil
 	}
 
 	// Override some settings with what's in kiali config
-	clientConfig.QPS = kialiConfig.KubernetesConfig.QPS
-	clientConfig.Burst = kialiConfig.KubernetesConfig.Burst
+	clientConfig.QPS = kialiConf.KubernetesConfig.QPS
+	clientConfig.Burst = kialiConf.KubernetesConfig.Burst
 
 	return &clientConfig, nil
 }
diff --git a/kubernetes/cluster_secret.go b/kubernetes/cluster_secret.go
index ddc7268d1..53112f484 100644
--- a/kubernetes/cluster_secret.go
+++ b/kubernetes/cluster_secret.go
@@ -6,7 +6,6 @@ import (
 	"os"
 
 	"k8s.io/client-go/tools/clientcmd"
-	"k8s.io/client-go/tools/clientcmd/api"
 
 	"github.com/kiali/kiali/log"
 )
@@ -126,14 +125,3 @@ func getRemoteClusterInfosFromDir(rootSecretsDir string) (map[string]RemoteClust
 
 	return meshClusters, nil
 }
-
-// getClusterName returns the name of the first cluster in the config.
-// Only useful if there's only one cluster in the config since maps are unordered.
-func getClusterName(config *api.Config) string {
-	var clusterName string
-	for name := range config.Clusters {
-		clusterName = name
-		break
-	}
-	return clusterName
-}
diff --git a/kubernetes/istio.go b/kubernetes/istio.go
index f6f6c7c2e..00c15732e 100644
--- a/kubernetes/istio.go
+++ b/kubernetes/istio.go
@@ -11,7 +11,7 @@ import (
 	networking_v1 "istio.io/client-go/pkg/apis/networking/v1"
 	security_v1 "istio.io/client-go/pkg/apis/security/v1"
 	istio "istio.io/client-go/pkg/clientset/versioned"
-	appsv1 "k8s.io/api/apps/v1"
+	v1 "k8s.io/api/apps/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	inferenceapiclient "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned"
 	k8s_networking_v1 "sigs.k8s.io/gateway-api/apis/v1"
@@ -20,6 +20,7 @@ import (
 	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/util/httputil"
+	"github.com/kiali/kiali/util/sliceutil"
 )
 
 const (
@@ -324,40 +325,55 @@ func DestinationRuleHasMTLSEnabled(destinationRule *networking_v1.DestinationRul
 	return false, ""
 }
 
-// ClusterNameFromIstiod attempts to resolve the cluster info of the "home" cluster where kiali is running
-// by inspecting the istiod deployment. Assumes that the istiod deployment is in the same cluster as the kiali pod.
-func ClusterNameFromIstiod(conf config.Config, k8s ClientInterface) (string, error) {
-	// The "cluster_id" is set in an environment variable of
-	// the "istiod" deployment. Let's try to fetch it.
-	var istiodDeployment *appsv1.Deployment
-	if istiodDeploymentName := conf.ExternalServices.Istio.IstiodDeploymentName; istiodDeploymentName != "" {
-		deployment, err := k8s.GetDeployment(conf.IstioNamespace, istiodDeploymentName)
-		if err != nil {
-			return "", err
-		}
+// ClusterNameFromIstiod checks for an istio control plane deployment (istiod) and if found returns the Istio
+// clusterID. Returns "" if no control plane is found or the cluster name can not be determined. Note that
+// this code is limited to the k8s API, the Kiali cache may not yet exist.
+func ClusterNameFromIstiod(conf *config.Config, k8s ClientInterface) (string, error) {
+	// The "cluster_id" is set in an environment variable of the "istiod" deployment. Let's try to fetch it.
+	istiods, err := k8s.GetDeployments("", metav1.ListOptions{LabelSelector: "app=istiod"})
+	if err != nil {
+		return "", err
+	}
 
-		istiodDeployment = deployment
-	} else {
-		istiodDeployments, err := k8s.GetDeployments(conf.IstioNamespace, metav1.ListOptions{LabelSelector: "app=istiod"})
-		if err != nil {
-			return "", err
-		}
+	if len(istiods) == 0 {
+		return "", fmt.Errorf("no valid istiod deployment found in any namespaces for kiali home cluster name discovery. You may need to set kubernetes_config.cluster_name in the Kiali CR")
+	}
 
-		if len(istiodDeployments) == 0 {
-			return "", fmt.Errorf("istiod deployment not found in namespace [%s]", conf.IstioNamespace)
+	// Do our best to filter out any external control plane, because the CLUSTER_ID will be set to
+	// the dataplane cluster, not the hosting cluster (which seems weird, but that's how Istio does it).
+	validIstiods := sliceutil.Filter(istiods, func(istiod v1.Deployment) bool {
+		// a typical namespace name is external-istiod
+		if strings.Contains(strings.ToLower(istiod.Namespace), "external") {
+			log.Tracef("Ignoring likely external controlplane [%s] during Kiali home clustername discovery. Namespace=[%s]", istiod.Name, istiod.Namespace)
+			return false
+		}
+		// also, look for the EXTERNAL_ISTIOD env var
+		for _, container := range istiod.Spec.Template.Spec.Containers {
+			for _, v := range container.Env {
+				if strings.Contains(strings.ToLower(v.Name), "external_istiod") && strings.ToLower(v.Value) == "true" {
+					log.Tracef("Ignoring likely external controlplane [%s] during Kiali home clustername discovery. EnvVar=[%s]", istiod.Name, v.Name)
+					return false
+				}
+			}
 		}
+		return true
+	})
 
-		// Just take the first one since they should all have the same cluster id.
-		istiodDeployment = &istiodDeployments[0]
+	// If we filtered out all of the istiods, just go back to what we had. In this case it's probably a primary-remote deployment where
+	// the primary may still have an EXTERNAL_ISTIOD env var.
+	if len(validIstiods) == 0 {
+		validIstiods = istiods
+		log.Tracef("Restoring original istiods as all were filtered. This is likely a primary-remote deployment.")
 	}
 
-	istiodContainers := istiodDeployment.Spec.Template.Spec.Containers
-	if len(istiodContainers) == 0 {
-		return "", fmt.Errorf("istiod deployment [%s] has no containers", istiodDeployment.Name)
+	validIstiod := validIstiods[0]
+	containers := validIstiod.Spec.Template.Spec.Containers
+	if len(containers) == 0 {
+		return "", fmt.Errorf("unexpected, istiod deployment [%s] has no containers", validIstiod.Name)
 	}
 
 	clusterName := ""
-	for _, v := range istiodContainers[0].Env {
+	for _, v := range containers[0].Env {
 		if v.Name == "CLUSTER_ID" {
 			clusterName = v.Value
 			break
@@ -366,7 +382,7 @@ func ClusterNameFromIstiod(conf config.Config, k8s ClientInterface) (string, err
 
 	if clusterName == "" {
 		// We didn't find it. This may mean that Istio is not setup with multi-cluster enabled.
-		return "", fmt.Errorf("istiod deployment [%s] does not have the CLUSTER_ID environment variable set", istiodDeployment.Name)
+		return "", fmt.Errorf("istiod deployment [%s] does not have the CLUSTER_ID environment variable set", validIstiod.Name)
 	}
 
 	return clusterName, nil
diff --git a/kubernetes/kubernetes.go b/kubernetes/kubernetes.go
index f4cf66a1b..028f582d2 100644
--- a/kubernetes/kubernetes.go
+++ b/kubernetes/kubernetes.go
@@ -370,11 +370,12 @@ func (in *K8SClient) GetDeployment(namespace, name string) (*apps_v1.Deployment,
 // GetDeployments returns an array of deployments for a given namespace.
 // It returns an error on any problem.
 func (in *K8SClient) GetDeployments(namespace string, opts meta_v1.ListOptions) ([]apps_v1.Deployment, error) {
-	if depList, err := in.k8s.AppsV1().Deployments(namespace).List(in.ctx, opts); err == nil {
-		return depList.Items, nil
-	} else {
+	depList, err := in.k8s.AppsV1().Deployments(namespace).List(in.ctx, opts)
+	if err != nil {
 		return []apps_v1.Deployment{}, err
 	}
+
+	return depList.Items, nil
 }
 
 func (in *K8SClient) GetReplicationControllers(namespace string) ([]core_v1.ReplicationController, error) {
diff --git a/kubernetes/token.go b/kubernetes/token.go
index 2e21b2894..055e0f670 100644
--- a/kubernetes/token.go
+++ b/kubernetes/token.go
@@ -1,11 +1,8 @@
 package kubernetes
 
 import (
-	"fmt"
 	"os"
 	"time"
-
-	"github.com/kiali/kiali/config"
 )
 
 // Be careful with how you use this token. This is the Kiali Service Account token, not the user token.
@@ -23,22 +20,12 @@ var (
 func GetKialiTokenForHomeCluster() (string, string, error) {
 	// TODO: refresh the token when it changes rather than after it expires
 	if KialiTokenForHomeCluster == "" || shouldRefreshToken() {
-		if remoteSecret, err := GetRemoteSecret(config.Get().Deployment.RemoteSecretPath); err == nil { // for experimental feature - for when data plane is in a remote cluster
-			currentContextAuthInfo := remoteSecret.Contexts[remoteSecret.CurrentContext].AuthInfo
-			if authInfo, ok := remoteSecret.AuthInfos[currentContextAuthInfo]; ok {
-				KialiTokenForHomeCluster = authInfo.Token
-				KialiTokenFileForHomeCluster = authInfo.TokenFile
-			} else {
-				return "", "", fmt.Errorf("auth info not found for current context: [%s]. Current context must be set for kiali remote secret", remoteSecret.CurrentContext)
-			}
-		} else {
-			token, err := os.ReadFile(DefaultServiceAccountPath)
-			if err != nil {
-				return "", "", err
-			}
-			KialiTokenForHomeCluster = string(token)
-			KialiTokenFileForHomeCluster = DefaultServiceAccountPath
+		token, err := os.ReadFile(DefaultServiceAccountPath)
+		if err != nil {
+			return "", "", err
 		}
+		KialiTokenForHomeCluster = string(token)
+		KialiTokenFileForHomeCluster = DefaultServiceAccountPath
 		tokenRead = time.Now()
 	}
 	return KialiTokenForHomeCluster, KialiTokenFileForHomeCluster, nil
diff --git a/make/Makefile.operator.mk b/make/Makefile.operator.mk
index 485f300c3..4bdd3cdf1 100644
--- a/make/Makefile.operator.mk
+++ b/make/Makefile.operator.mk
@@ -95,7 +95,6 @@ operator-create: .ensure-operator-repo-exists .ensure-operator-helm-chart-exists
     --kiali-image-name              "${CLUSTER_KIALI_INTERNAL_NAME}" \
     --kiali-image-pull-policy       "${KIALI_IMAGE_PULL_POLICY}" \
     --kiali-image-version           "${CONTAINER_VERSION}" \
-    --istio-namespace               "${ISTIO_NAMESPACE}" \
     --namespace                     "${NAMESPACE}" \
     --version                       "${KIALI_CR_SPEC_VERSION}"
 
@@ -123,7 +122,6 @@ KIALI_EXTERNAL_SERVICES_PASSWORD="$(shell ${OC} get secrets htpasswd -n ${NAMESP
 KIALI_IMAGE_NAME="${CLUSTER_KIALI_INTERNAL_NAME}" \
 KIALI_IMAGE_PULL_POLICY="${KIALI_IMAGE_PULL_POLICY}" \
 KIALI_IMAGE_VERSION="${CONTAINER_VERSION}" \
-ISTIO_NAMESPACE="${ISTIO_NAMESPACE}" \
 NAMESPACE="${NAMESPACE}" \
 ROUTER_HOSTNAME="$(shell ${OC} get $(shell (${OC} get routes -n ${NAMESPACE} -o name 2>/dev/null || echo 'noroute') | head -n 1) -n ${NAMESPACE} -o jsonpath='{.status.ingress[0].routerCanonicalHostname}' 2>/dev/null)" \
 SERVICE_TYPE="${SERVICE_TYPE}" \
diff --git a/mesh/generator/generator.go b/mesh/generator/generator.go
index fa3e99d39..fcb8f086c 100644
--- a/mesh/generator/generator.go
+++ b/mesh/generator/generator.go
@@ -80,6 +80,10 @@ func BuildMeshMap(ctx context.Context, o mesh.Options, gi *mesh.GlobalInfo) (mes
 	graph.CheckError(err)
 
 	clusterMap := make(map[string]bool)
+	conf := gi.Conf.Obfuscate()
+	es := conf.ExternalServices
+	hasExternalServices := false // external to the cluster/mesh (or a URL that can't be parsed)
+
 	for _, cp := range meshDef.ControlPlanes {
 		// Check if istio namespace is accessible for that cluster
 		cpKey := mesh.GetClusterSensitiveKey(cp.Cluster.Name, cp.IstiodNamespace)
@@ -144,14 +148,12 @@ func BuildMeshMap(ctx context.Context, o mesh.Options, gi *mesh.GlobalInfo) (mes
 		}
 
 		// add any Kiali instances
-		conf := gi.Conf.Obfuscate()
-		es := conf.ExternalServices
-		hasExternalServices := false // external to the cluster/mesh (or a URL that can't be parsed)
-
 		for _, ki := range cp.Cluster.KialiInstances {
 			kiali, _, err := addInfra(meshMap, mesh.InfraTypeKiali, cp.Cluster.Name, ki.Namespace, ki.ServiceName, es.Istio, ki.Version, false, "")
 			mesh.CheckError(err)
 
+			es := conf.ExternalServices
+
 			if es.Istio.IstioAPIEnabled {
 				kiali.AddEdge(istiod)
 			}
@@ -351,6 +353,70 @@ func BuildMeshMap(ctx context.Context, o mesh.Options, gi *mesh.GlobalInfo) (mes
 		}
 	}
 
+	if meshDef.ExternalKiali != nil {
+		cluster := meshDef.ExternalKiali.Cluster.Name
+
+		// add external cluster if not already added
+		if _, ok := clusterMap[cluster]; !ok {
+			k8sVersion := esVersions[fmt.Sprintf("%s-%s", "Kubernetes", cluster)]
+			if k8sVersion == "" {
+				k8sVersion = "Unknown"
+			}
+			_, _, err := addInfra(meshMap, mesh.InfraTypeCluster, cluster, "", cluster, cluster, k8sVersion, false, "")
+			mesh.CheckError(err)
+			clusterMap[cluster] = true
+		}
+
+		ek := meshDef.ExternalKiali.Kiali
+		kiali, _, err := addInfra(meshMap, mesh.InfraTypeKiali, cluster, ek.Namespace, ek.ServiceName, es.Istio, ek.Version, false, "")
+		mesh.CheckError(err)
+
+		if es.Istio.IstioAPIEnabled {
+			for _, infra := range meshMap {
+				if infra.InfraType == mesh.InfraTypeIstiod {
+					kiali.AddEdge(infra)
+				}
+			}
+		}
+
+		// add the Kiali external services...
+
+		// metrics/prometheus
+		cluster, namespace, isExternal := discoverInfraService(es.Prometheus.URL, ctx, gi)
+		var node *mesh.Node
+		name := "Prometheus"
+		node, _, err = addInfra(meshMap, mesh.InfraTypeMetricStore, cluster, namespace, name, es.Prometheus, esVersions[name], isExternal, healthData[promHealthKey])
+		mesh.CheckError(err)
+
+		kiali.AddEdge(node)
+		hasExternalServices = hasExternalServices || isExternal
+
+		if conf.ExternalServices.Tracing.Enabled {
+			cluster, namespace, isExternal = discoverInfraService(es.Tracing.InternalURL, ctx, gi)
+			name = string(es.Tracing.Provider)
+			node, _, err = addInfra(meshMap, mesh.InfraTypeTraceStore, cluster, namespace, name, es.Tracing, esVersions[name], isExternal, healthData[tracingHealthKey])
+			mesh.CheckError(err)
+
+			kiali.AddEdge(node)
+			hasExternalServices = hasExternalServices || isExternal
+		}
+
+		if conf.ExternalServices.Grafana.Enabled {
+			cluster, namespace, isExternal = discoverInfraService(es.Grafana.InternalURL, ctx, gi)
+			name = "Grafana"
+			node, _, err = addInfra(meshMap, mesh.InfraTypeGrafana, cluster, namespace, name, es.Grafana, esVersions[name], isExternal, healthData[grafanaHealthKey])
+			mesh.CheckError(err)
+
+			kiali.AddEdge(node)
+			hasExternalServices = hasExternalServices || isExternal
+		}
+
+		if hasExternalServices {
+			_, _, err = addInfra(meshMap, mesh.InfraTypeCluster, mesh.External, "", "External Deployments", nil, "", true, "")
+			mesh.CheckError(err)
+		}
+	}
+
 	// The finalizers can perform final manipulations on the complete graph
 	for _, f := range finalizers {
 		f.AppendGraph(meshMap, gi, nil)
diff --git a/models/mesh.go b/models/mesh.go
index 3b7c63da1..7b47d8f41 100644
--- a/models/mesh.go
+++ b/models/mesh.go
@@ -32,12 +32,19 @@ const (
 
 var maistraLabels = []string{maistraOwnerLabel, maistraOwnerNameLabel, maistraVersionLabel}
 
+type ExternalKialiInstance struct {
+	Cluster *KubeCluster
+	Kiali   *KialiInstance
+}
+
 // Mesh is one or more controlplanes (primaries) managing a dataPlane across one or more clusters.
 // There can be multiple primaries on a single cluster when istio revisions are used. A single
 // primary can also manage multiple clusters (primary-remote deployment).
 type Mesh struct {
 	// ControlPlanes that share the same mesh ID.
 	ControlPlanes []ControlPlane
+	// External Kiali mesh management cluster
+	ExternalKiali *ExternalKialiInstance
 }
 
 // Tag maps a controlplane revision to a namespace label.
@@ -271,6 +278,11 @@ func (ci *Certificate) Parse(certificate []byte) {
 // Cluster holds some metadata about a Kubernetes cluster that is
 // part of the mesh.
 type KubeCluster struct {
+	// Accessible specifies if the cluster is accessible or not. Clusters that are manually specified in the Kiali config
+	// but do not have an associated remote cluster secret are considered not accessible. This is helpful when you have
+	// two disconnected Kialis and want to link them without giving them access to each other.
+	Accessible bool `json:"accessible"`
+
 	// ApiEndpoint is the URL where the Kubernetes/Cluster API Server can be contacted
 	ApiEndpoint string `json:"apiEndpoint"`
 
@@ -285,11 +297,6 @@ type KubeCluster struct {
 
 	// SecretName is the name of the kubernetes "remote cluster secret" that was mounted to the file system and where data of this cluster was resolved
 	SecretName string `json:"secretName"`
-
-	// Accessible specifies if the cluster is accessible or not. Clusters that are manually specified in the Kiali config
-	// but do not have an associated remote cluster secret are considered not accessible. This is helpful when you have
-	// two disconnected Kialis and want to link them without giving them access to each other.
-	Accessible bool `json:"accessible"`
 }
 
 // KialiInstance represents a Kiali installation. It holds some data about
diff --git a/models/pod.go b/models/pod.go
index b6e5bfa21..af5a3079f 100644
--- a/models/pod.go
+++ b/models/pod.go
@@ -1,6 +1,7 @@
 package models
 
 import (
+	"context"
 	"encoding/json"
 	"strings"
 
@@ -59,14 +60,14 @@ type ContainerInfo struct {
 }
 
 // Parse extracts desired information from k8s []Pod info
-func (pods *Pods) Parse(list []core_v1.Pod) {
+func (pods *Pods) Parse(list []core_v1.Pod, isControlPlane func(ctx context.Context, cluster, namespace string) bool) {
 	if list == nil {
 		return
 	}
 
 	for _, pod := range list {
 		casted := Pod{}
-		casted.Parse(&pod)
+		casted.Parse(&pod, isControlPlane)
 		*pods = append(*pods, &casted)
 	}
 }
@@ -78,7 +79,7 @@ type sideCarStatus struct {
 }
 
 // Parse extracts desired information from k8s Pod info
-func (pod *Pod) Parse(p *core_v1.Pod) {
+func (pod *Pod) Parse(p *core_v1.Pod, isControlPlane func(ctx context.Context, cluster, namespace string) bool) {
 	pod.Name = p.Name
 	pod.Labels = p.Labels
 	pod.Annotations = p.Annotations
@@ -125,7 +126,7 @@ func (pod *Pod) Parse(p *core_v1.Pod) {
 		container := ContainerInfo{
 			Name:      c.Name,
 			Image:     c.Image,
-			IsProxy:   isIstioProxy(p, &c, conf),
+			IsProxy:   isIstioProxy(p, &c, *conf, isControlPlane),
 			IsReady:   lookupReady(c.Name, p.Status.ContainerStatuses),
 			IsAmbient: isIstioAmbient(p),
 		}
@@ -143,8 +144,8 @@ func (pod *Pod) Parse(p *core_v1.Pod) {
 	pod.ServiceAccountName = p.Spec.ServiceAccountName
 }
 
-func isIstioProxy(pod *core_v1.Pod, container *core_v1.Container, conf *config.Config) bool {
-	if pod.Namespace != conf.IstioNamespace {
+func isIstioProxy(pod *core_v1.Pod, container *core_v1.Container, conf config.Config, isControlPlane func(ctx context.Context, cluster, namespace string) bool) bool {
+	if !isControlPlane(context.TODO(), "", pod.Namespace) {
 		return false
 	}
 	if container.Name == IstioProxy {
diff --git a/models/service.go b/models/service.go
index 75817f4d8..cc87975c9 100644
--- a/models/service.go
+++ b/models/service.go
@@ -1,6 +1,8 @@
 package models
 
 import (
+	"context"
+
 	networking_v1 "istio.io/client-go/pkg/apis/networking/v1"
 	core_v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/labels"
@@ -214,9 +216,9 @@ func (s *ServiceDetails) SetService(cluster string, svc *core_v1.Service, conf *
 	s.Service.Parse(cluster, svc, conf)
 }
 
-func (s *ServiceDetails) SetPods(pods []core_v1.Pod) {
+func (s *ServiceDetails) SetPods(pods []core_v1.Pod, isControlPlane func(ctx context.Context, cluster, namespace string) bool) {
 	mPods := Pods{}
-	mPods.Parse(pods)
+	mPods.Parse(pods, isControlPlane)
 }
 
 func (s *ServiceDetails) SetIstioSidecar(workloads WorkloadOverviews) {
diff --git a/models/workload.go b/models/workload.go
index b6b01caf9..1c6311fdf 100644
--- a/models/workload.go
+++ b/models/workload.go
@@ -1,6 +1,7 @@
 package models
 
 import (
+	"context"
 	"strconv"
 
 	osapps_v1 "github.com/openshift/api/apps/v1"
@@ -603,8 +604,8 @@ func (workload *Workload) ParsePods(controllerName string, controllerGVK schema.
 	}
 }
 
-func (workload *Workload) SetPods(pods []core_v1.Pod) {
-	workload.Pods.Parse(pods)
+func (workload *Workload) SetPods(pods []core_v1.Pod, isControlPlane func(ctx context.Context, cluster, namespace string) bool) {
+	workload.Pods.Parse(pods, isControlPlane)
 	workload.IstioSidecar = workload.HasIstioSidecar()
 	workload.IsAmbient = workload.HasIstioAmbient()
 }
diff --git a/tracing/client.go b/tracing/client.go
index 9db9cf484..142f313e2 100644
--- a/tracing/client.go
+++ b/tracing/client.go
@@ -131,13 +131,20 @@ func newClient(ctx context.Context, conf *config.Config, token string) (*Client,
 		auth.Token = token
 	}
 
-	u, errParse := url.Parse(cfgTracing.InternalURL)
-	if !conf.InCluster {
+	var u *url.URL
+	var errParse error
+	if cfgTracing.InternalURL != "" {
+		u, errParse = url.Parse(cfgTracing.InternalURL)
+		if errParse != nil {
+			zl.Error().Msgf("Error parsing tracing InternalURL: %s", errParse)
+			return nil, errParse
+		}
+	} else {
 		u, errParse = url.Parse(cfgTracing.ExternalURL)
-	}
-	if errParse != nil {
-		zl.Error().Msgf("Error parsing Tracing URL: %s", errParse)
-		return nil, errParse
+		if errParse != nil {
+			zl.Error().Msgf("Error parsing tracing ExternalURL: %s", errParse)
+			return nil, errParse
+		}
 	}
 
 	port := u.Port()
diff --git a/tracing/discovery_client.go b/tracing/discovery_client.go
index 5277aa742..002840680 100644
--- a/tracing/discovery_client.go
+++ b/tracing/discovery_client.go
@@ -50,7 +50,7 @@ func TestNewClient(ctx context.Context, conf *config.Config, token string) (*mod
 
 	// Internal URL not set
 	url := cfgTracing.InternalURL
-	if !conf.InCluster {
+	if url == "" {
 		url = cfgTracing.ExternalURL
 		logs = append(logs, model.LogLine{Time: time.Now(), Test: fmt.Sprintf("Using external url %s because not in cluster", url)})
 		if url == "" {



Test patch:
diff --git a/business/checkers/services/port_mapping_checker_test.go b/business/checkers/services/port_mapping_checker_test.go
index dcc14e63d..f07f85b82 100644
--- a/business/checkers/services/port_mapping_checker_test.go
+++ b/business/checkers/services/port_mapping_checker_test.go
@@ -10,20 +10,27 @@ import (
 	"k8s.io/apimachinery/pkg/util/intstr"
 
 	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/istio/istiotest"
 	"github.com/kiali/kiali/tests/testutils/validations"
 )
 
+var discovery = &istiotest.FakeDiscovery{
+	IsControlPlaneReturn: false,
+}
+var discoveryCP = &istiotest.FakeDiscovery{
+	IsControlPlaneReturn: true,
+}
+
 func TestPortMappingMatch(t *testing.T) {
 	conf := config.NewConfig()
 	config.Set(conf)
 
 	assert := assert.New(t)
 
-	pmc := PortMappingChecker{
-		Service:     getService(9080, "http", nil, "test-namespace", "app", "labelName1"),
-		Deployments: getDeployment(9080),
-		Pods:        getPods(true),
-	}
+	deployments := getDeployment(9080)
+	pods := getPods(true)
+	service := getService(9080, "http", nil, "test-namespace", "app", "labelName1")
+	pmc := NewPortMappingChecker(deployments, discovery, pods, service)
 
 	vals, valid := pmc.Check()
 	assert.True(valid)
@@ -48,11 +55,9 @@ func TestTargetPortMappingMatch(t *testing.T) {
 
 	*/
 
-	pmc := PortMappingChecker{
-		Service:     service,
-		Deployments: getDeployment(8080),
-		Pods:        getPods(true),
-	}
+	deployments := getDeployment(8080)
+	pods := getPods(true)
+	pmc := NewPortMappingChecker(deployments, discovery, pods, service)
 
 	vals, valid := pmc.Check()
 	assert.True(valid)
@@ -73,11 +78,10 @@ func TestPortMappingMismatch(t *testing.T) {
 
 	assert := assert.New(t)
 
-	pmc := PortMappingChecker{
-		Service:     getService(9080, "http", nil, "test-namespace", "app", "labelName1"),
-		Deployments: getDeployment(8080),
-		Pods:        getPods(true),
-	}
+	deployments := getDeployment(8080)
+	pods := getPods(true)
+	service := getService(9080, "http", nil, "test-namespace", "app", "labelName1")
+	pmc := NewPortMappingChecker(deployments, discovery, pods, service)
 
 	vals, valid := pmc.Check()
 	assert.False(valid)
@@ -92,11 +96,10 @@ func TestPortMappingSkipWaypoint(t *testing.T) {
 
 	assert := assert.New(t)
 
-	pmc := PortMappingChecker{
-		Service:     getService(9080, "http", nil, "test-namespace", config.WaypointLabel, config.WaypointLabelValue),
-		Deployments: getDeployment(8080),
-		Pods:        getPods(false),
-	}
+	deployments := getDeployment(8080)
+	pods := getPods(false)
+	service := getService(9080, "http", nil, "test-namespace", config.WaypointLabel, config.WaypointLabelValue)
+	pmc := NewPortMappingChecker(deployments, discovery, pods, service)
 
 	vals, valid := pmc.Check()
 	assert.True(valid)
@@ -110,11 +113,10 @@ func TestPortMappingNoMismatchIstio(t *testing.T) {
 
 	assert := assert.New(t)
 
-	pmc := PortMappingChecker{
-		Service:     getService(9080, "http", nil, "istio-system", "app", "labelName1"),
-		Deployments: getDeployment(8080),
-		Pods:        getPods(true),
-	}
+	deployments := getDeployment(8080)
+	pods := getPods(true)
+	service := getService(9080, "http", nil, "istio-system", "app", "labelName1")
+	pmc := NewPortMappingChecker(deployments, discoveryCP, pods, service)
 
 	vals, valid := pmc.Check()
 	assert.True(valid)
@@ -127,11 +129,10 @@ func TestServicePortNaming(t *testing.T) {
 
 	assert := assert.New(t)
 
-	pmc := PortMappingChecker{
-		Service:     getService(9080, "http2foo", nil, "test-namespace", "app", "labelName1"),
-		Deployments: getDeployment(9080),
-		Pods:        getPods(true),
-	}
+	deployments := getDeployment(9080)
+	pods := getPods(true)
+	service := getService(9080, "http2foo", nil, "test-namespace", "app", "labelName1")
+	pmc := NewPortMappingChecker(deployments, discovery, pods, service)
 
 	vals, valid := pmc.Check()
 	assert.False(valid)
@@ -146,11 +147,10 @@ func TestServicePortNamingIstioSystem(t *testing.T) {
 
 	assert := assert.New(t)
 
-	pmc := PortMappingChecker{
-		Service:     getService(9080, "http2foo", nil, "istio-system", "app", "labelName1"),
-		Deployments: getDeployment(9080),
-		Pods:        getPods(true),
-	}
+	deployments := getDeployment(9080)
+	pods := getPods(true)
+	service := getService(9080, "http2foo", nil, "istio-system", "app", "labelName1")
+	pmc := NewPortMappingChecker(deployments, discoveryCP, pods, service)
 
 	vals, valid := pmc.Check()
 	assert.False(valid)
@@ -165,11 +165,10 @@ func TestServicePortNamingWizard(t *testing.T) {
 
 	assert := assert.New(t)
 
-	pmc := PortMappingChecker{
-		Service:     getService(9080, "status-port", nil, "test-namespace", "kiali_wizard", "labelName1"),
-		Deployments: getDeployment(9080),
-		Pods:        getPods(true),
-	}
+	deployments := getDeployment(9080)
+	pods := getPods(true)
+	service := getService(9080, "status-port", nil, "test-namespace", "kiali_wizard", "labelName1")
+	pmc := NewPortMappingChecker(deployments, discovery, pods, service)
 
 	vals, valid := pmc.Check()
 	assert.True(valid)
@@ -183,11 +182,10 @@ func TestServicePortAppProtocol(t *testing.T) {
 	assert := assert.New(t)
 
 	appProtocol := "mysql-wrong"
-	pmc := PortMappingChecker{
-		Service:     getService(9080, "database", &appProtocol, "test-namespace", "app", "labelName1"),
-		Deployments: getDeployment(9080),
-		Pods:        getPods(true),
-	}
+	deployments := getDeployment(9080)
+	pods := getPods(true)
+	service := getService(9080, "database", &appProtocol, "test-namespace", "app", "labelName1")
+	pmc := NewPortMappingChecker(deployments, discovery, pods, service)
 
 	vals, valid := pmc.Check()
 	assert.False(valid)
@@ -196,11 +194,8 @@ func TestServicePortAppProtocol(t *testing.T) {
 	assert.Equal("spec/ports[0]", vals[0].Path)
 
 	appProtocol = "mysql"
-	pmc = PortMappingChecker{
-		Service:     getService(9080, "database", &appProtocol, "test-namespace", "app", "labelName1"),
-		Deployments: getDeployment(9080),
-		Pods:        getPods(true),
-	}
+	service = getService(9080, "database", &appProtocol, "test-namespace", "app", "labelName1")
+	pmc = NewPortMappingChecker(deployments, discovery, pods, service)
 
 	vals, valid = pmc.Check()
 	assert.True(valid)
@@ -213,11 +208,10 @@ func TestServicePortNamingWithoutSidecar(t *testing.T) {
 
 	assert := assert.New(t)
 
-	pmc := PortMappingChecker{
-		Service:     getService(9080, "http2foo", nil, "test-namespace", "app", "labelName1"),
-		Deployments: getDeployment(9080),
-		Pods:        getPods(false),
-	}
+	deployments := getDeployment(9080)
+	pods := getPods(false)
+	service := getService(9080, "http2foo", nil, "test-namespace", "app", "labelName1")
+	pmc := NewPortMappingChecker(deployments, discovery, pods, service)
 
 	vals, valid := pmc.Check()
 	assert.True(valid)
diff --git a/business/istio_status_test.go b/business/istio_status_test.go
index 2a1736709..77b33111e 100644
--- a/business/istio_status_test.go
+++ b/business/istio_status_test.go
@@ -756,7 +756,7 @@ func confWithComponentNamespaces() *config.Config {
 	conf.ExternalServices.Istio.ComponentStatuses = config.ComponentStatuses{
 		Enabled: true,
 		Components: []config.ComponentStatus{
-			{AppLabel: "pilot", IsCore: true},
+			{AppLabel: "pilot", IsCore: true, Namespace: "istio-system"},
 			{AppLabel: "ingress", IsCore: true, Namespace: "ingress-egress"},
 			{AppLabel: "egress", IsCore: false, Namespace: "ingress-egress"},
 			{AppLabel: "sds", IsCore: false, Namespace: "istio-admin"},
diff --git a/business/tls_perf_test.go b/business/tls_perf_test.go
index 9cac8f4cf..ce4d5d685 100644
--- a/business/tls_perf_test.go
+++ b/business/tls_perf_test.go
@@ -87,7 +87,7 @@ func testPerfScenario(exStatus string, namespaces []core_v1.Namespace, drs []*ne
 	discovery := &istiotest.FakeDiscovery{
 		MeshReturn: models.Mesh{
 			ControlPlanes: []models.ControlPlane{{
-				IstiodNamespace: conf.IstioNamespace,
+				IstiodNamespace: config.IstioNamespaceDefault,
 				Revision:        "default",
 				Cluster:         &models.KubeCluster{Name: conf.KubernetesConfig.ClusterName},
 				MeshConfig: &models.MeshConfig{
diff --git a/business/tls_test.go b/business/tls_test.go
index 61b0f56ce..489f7c527 100644
--- a/business/tls_test.go
+++ b/business/tls_test.go
@@ -39,7 +39,6 @@ func TestMeshStatusEnabled(t *testing.T) {
 
 	conf := config.NewConfig()
 	conf.Deployment.ClusterWideAccess = true
-	conf.IstioNamespace = "istio-system"
 	kubernetes.SetConfig(t, *conf)
 
 	pa := fakeStrictMeshPeerAuthentication("default")
@@ -49,7 +48,7 @@ func TestMeshStatusEnabled(t *testing.T) {
 	}
 	objs := []runtime.Object{
 		kubetest.FakeNamespaceWithLabels("test", injectionEnabledLabel),
-		kubetest.FakeNamespace("istio-system"),
+		kubetest.FakeNamespace(config.IstioNamespaceDefault),
 		kubetest.FakeNamespaceWithLabels("default", injectionEnabledLabel),
 	}
 	objs = append(objs, kubernetes.ToRuntimeObjects(pa)...)
@@ -59,7 +58,7 @@ func TestMeshStatusEnabled(t *testing.T) {
 	discovery := &istiotest.FakeDiscovery{
 		MeshReturn: models.Mesh{
 			ControlPlanes: []models.ControlPlane{{
-				IstiodNamespace: conf.IstioNamespace,
+				IstiodNamespace: config.IstioNamespaceDefault,
 				Revision:        "default",
 				Cluster:         &models.KubeCluster{Name: conf.KubernetesConfig.ClusterName},
 				MeshConfig:      meshConfigWithAutomTLSDisabled,
@@ -95,7 +94,7 @@ func TestMeshStatusEnabledAutoMtls(t *testing.T) {
 	discovery := &istiotest.FakeDiscovery{
 		MeshReturn: models.Mesh{
 			ControlPlanes: []models.ControlPlane{{
-				IstiodNamespace: conf.IstioNamespace,
+				IstiodNamespace: config.IstioNamespaceDefault,
 				Revision:        "default",
 				Cluster:         &models.KubeCluster{Name: conf.KubernetesConfig.ClusterName},
 				MeshConfig:      meshConfigWithAutomTLSEnabled,
@@ -134,7 +133,7 @@ func TestMeshStatusPartiallyEnabled(t *testing.T) {
 	discovery := &istiotest.FakeDiscovery{
 		MeshReturn: models.Mesh{
 			ControlPlanes: []models.ControlPlane{{
-				IstiodNamespace: conf.IstioNamespace,
+				IstiodNamespace: config.IstioNamespaceDefault,
 				Revision:        "default",
 				Cluster:         &models.KubeCluster{Name: conf.KubernetesConfig.ClusterName},
 				MeshConfig:      meshConfigWithAutomTLSDisabled,
@@ -169,7 +168,7 @@ func TestMeshStatusNotEnabled(t *testing.T) {
 	discovery := &istiotest.FakeDiscovery{
 		MeshReturn: models.Mesh{
 			ControlPlanes: []models.ControlPlane{{
-				IstiodNamespace: conf.IstioNamespace,
+				IstiodNamespace: config.IstioNamespaceDefault,
 				Revision:        "default",
 				Cluster:         &models.KubeCluster{Name: conf.KubernetesConfig.ClusterName},
 				MeshConfig:      meshConfigWithAutomTLSDisabled,
@@ -207,7 +206,7 @@ func TestMeshStatusDisabled(t *testing.T) {
 	discovery := &istiotest.FakeDiscovery{
 		MeshReturn: models.Mesh{
 			ControlPlanes: []models.ControlPlane{{
-				IstiodNamespace: conf.IstioNamespace,
+				IstiodNamespace: config.IstioNamespaceDefault,
 				Revision:        "default",
 				Cluster:         &models.KubeCluster{Name: conf.KubernetesConfig.ClusterName},
 				MeshConfig:      meshConfigWithAutomTLSDisabled,
@@ -232,7 +231,7 @@ func TestMeshStatusNotEnabledAutoMtls(t *testing.T) {
 	discovery := &istiotest.FakeDiscovery{
 		MeshReturn: models.Mesh{
 			ControlPlanes: []models.ControlPlane{{
-				IstiodNamespace: conf.IstioNamespace,
+				IstiodNamespace: config.IstioNamespaceDefault,
 				Revision:        "default",
 				Cluster:         &models.KubeCluster{Name: conf.KubernetesConfig.ClusterName},
 				MeshConfig:      meshConfigWithAutomTLSEnabled,
@@ -366,7 +365,7 @@ func TestNamespaceHasDestinationRuleEnabledDifferentNs(t *testing.T) {
 	discovery := &istiotest.FakeDiscovery{
 		MeshReturn: models.Mesh{
 			ControlPlanes: []models.ControlPlane{{
-				IstiodNamespace: conf.IstioNamespace,
+				IstiodNamespace: config.IstioNamespaceDefault,
 				Revision:        "default",
 				Cluster:         &models.KubeCluster{Name: conf.KubernetesConfig.ClusterName},
 				MeshConfig:      meshConfigWithAutomTLSDisabled,
@@ -408,7 +407,7 @@ func testNamespaceScenario(exStatus string, drs []*networking_v1.DestinationRule
 	discovery := &istiotest.FakeDiscovery{
 		MeshReturn: models.Mesh{
 			ControlPlanes: []models.ControlPlane{{
-				IstiodNamespace: conf.IstioNamespace,
+				IstiodNamespace: config.IstioNamespaceDefault,
 				Revision:        "default",
 				Cluster:         &models.KubeCluster{Name: conf.KubernetesConfig.ClusterName},
 				MeshConfig:      meshConfig(autoMtls),
diff --git a/config/config_test.go b/config/config_test.go
index 6251425aa..bab0ea109 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -350,14 +350,14 @@ func TestValidateWebRoot(t *testing.T) {
 
 	for _, webroot := range validWebRoots {
 		conf.Server.WebRoot = webroot
-		if err := Validate(*conf); err != nil {
+		if err := Validate(conf); err != nil {
 			t.Errorf("Web root validation should have succeeded for [%v]: %v", conf.Server.WebRoot, err)
 		}
 	}
 
 	for _, webroot := range invalidWebRoots {
 		conf.Server.WebRoot = webroot
-		if err := Validate(*conf); err == nil {
+		if err := Validate(conf); err == nil {
 			t.Errorf("Web root validation should have failed [%v]", conf.Server.WebRoot)
 		}
 	}
@@ -385,14 +385,14 @@ func TestValidateAuthStrategy(t *testing.T) {
 
 	for _, strategies := range validStrategies {
 		conf.Auth.Strategy = strategies
-		if err := Validate(*conf); err != nil {
+		if err := Validate(conf); err != nil {
 			t.Errorf("Auth Strategy validation should have succeeded for [%v]: %v", conf.Auth.Strategy, err)
 		}
 	}
 
 	for _, strategies := range invalidStrategies {
 		conf.Auth.Strategy = strategies
-		if err := Validate(*conf); err == nil {
+		if err := Validate(conf); err == nil {
 			t.Errorf("Auth Strategy validation should have failed [%v]", conf.Auth.Strategy)
 		}
 	}
diff --git a/graph/api/api_perf_test.go b/graph/api/api_perf_test.go
index 2cfaa80d2..dba8369f7 100644
--- a/graph/api/api_perf_test.go
+++ b/graph/api/api_perf_test.go
@@ -1145,7 +1145,7 @@ func BenchmarkVersionedAppGraph(b *testing.B) {
 
 	mr := mux.NewRouter()
 	mr.HandleFunc("/api/namespaces/graph", func(w http.ResponseWriter, r *http.Request) {
-		options := graph.NewOptions(r, &biz.Namespace)
+		options := graph.NewOptions(r, biz)
 		options.Rates.Ambient = graph.AmbientTrafficNone
 		options.Appenders.AppenderNames = []string{"deadNode", "istio", "serviceEntry", "meshCheck", "workloadEntry", "health"}
 		code, config := graphNamespacesIstio(r.Context(), biz, client, options)
diff --git a/graph/api/api_test.go b/graph/api/api_test.go
index f87c6c142..4ead90b60 100644
--- a/graph/api/api_test.go
+++ b/graph/api/api_test.go
@@ -1235,7 +1235,7 @@ func TestAppGraph(t *testing.T) {
 	mr := mux.NewRouter()
 
 	mr.HandleFunc("/api/namespaces/graph", func(w http.ResponseWriter, r *http.Request) {
-		options := graph.NewOptions(r, &biz.Namespace)
+		options := graph.NewOptions(r, biz)
 		options.Rates.Ambient = graph.AmbientTrafficNone
 		code, config := graphNamespacesIstio(r.Context(), biz, client, options)
 		respond(w, code, config)
@@ -1270,7 +1270,7 @@ func TestVersionedAppGraph(t *testing.T) {
 
 	mr := mux.NewRouter()
 	mr.HandleFunc("/api/namespaces/graph", func(w http.ResponseWriter, r *http.Request) {
-		options := graph.NewOptions(r, &biz.Namespace)
+		options := graph.NewOptions(r, biz)
 		options.Rates.Ambient = graph.AmbientTrafficNone
 		code, config := graphNamespacesIstio(r.Context(), biz, client, options)
 		respond(w, code, config)
@@ -1305,7 +1305,7 @@ func TestServiceGraph(t *testing.T) {
 
 	mr := mux.NewRouter()
 	mr.HandleFunc("/api/namespaces/graph", func(w http.ResponseWriter, r *http.Request) {
-		options := graph.NewOptions(r, &biz.Namespace)
+		options := graph.NewOptions(r, biz)
 		options.Rates.Ambient = graph.AmbientTrafficNone
 		code, config := graphNamespacesIstio(r.Context(), biz, client, options)
 		respond(w, code, config)
@@ -1340,7 +1340,7 @@ func TestWorkloadGraph(t *testing.T) {
 
 	mr := mux.NewRouter()
 	mr.HandleFunc("/api/namespaces/graph", func(w http.ResponseWriter, r *http.Request) {
-		options := graph.NewOptions(r, &biz.Namespace)
+		options := graph.NewOptions(r, biz)
 		options.Rates.Ambient = graph.AmbientTrafficNone
 		code, config := graphNamespacesIstio(r.Context(), biz, client, options)
 		respond(w, code, config)
@@ -1376,7 +1376,7 @@ func TestRatesGraphSent(t *testing.T) {
 	mr := mux.NewRouter()
 	mr.HandleFunc("/api/namespaces/graph",
 		func(w http.ResponseWriter, r *http.Request) {
-			options := graph.NewOptions(r, &biz.Namespace)
+			options := graph.NewOptions(r, biz)
 			options.Rates.Ambient = graph.AmbientTrafficNone
 			code, config := graphNamespacesIstio(r.Context(), biz, client, options)
 			respond(w, code, config)
@@ -1411,7 +1411,7 @@ func TestRatesGraphReceived(t *testing.T) {
 
 	mr := mux.NewRouter()
 	mr.HandleFunc("/api/namespaces/graph", func(w http.ResponseWriter, r *http.Request) {
-		options := graph.NewOptions(r, &biz.Namespace)
+		options := graph.NewOptions(r, biz)
 		options.Rates.Ambient = graph.AmbientTrafficNone
 		code, config := graphNamespacesIstio(r.Context(), biz, client, options)
 		respond(w, code, config)
@@ -1446,7 +1446,7 @@ func TestRatesGraphTotal(t *testing.T) {
 
 	mr := mux.NewRouter()
 	mr.HandleFunc("/api/namespaces/graph", func(w http.ResponseWriter, r *http.Request) {
-		options := graph.NewOptions(r, &biz.Namespace)
+		options := graph.NewOptions(r, biz)
 		options.Rates.Ambient = graph.AmbientTrafficNone
 		code, config := graphNamespacesIstio(r.Context(), biz, client, options)
 		respond(w, code, config)
@@ -1482,7 +1482,7 @@ func TestRatesGraphNone(t *testing.T) {
 	mr := mux.NewRouter()
 	mr.HandleFunc("/api/namespaces/graph",
 		func(w http.ResponseWriter, r *http.Request) {
-			options := graph.NewOptions(r, &biz.Namespace)
+			options := graph.NewOptions(r, biz)
 			options.Rates.Ambient = graph.AmbientTrafficNone
 			code, config := graphNamespacesIstio(r.Context(), biz, client, options)
 			respond(w, code, config)
@@ -1802,7 +1802,7 @@ func TestWorkloadNodeGraph(t *testing.T) {
 
 	mr := mux.NewRouter()
 	mr.HandleFunc("/api/namespaces/{namespace}/workloads/{workload}/graph", func(w http.ResponseWriter, r *http.Request) {
-		options := graph.NewOptions(r, &biz.Namespace)
+		options := graph.NewOptions(r, biz)
 		options.Rates.Ambient = graph.AmbientTrafficNone
 		code, config := graphNodeIstio(r.Context(), biz, client, options)
 		respond(w, code, config)
@@ -2122,7 +2122,7 @@ func TestAppNodeGraph(t *testing.T) {
 
 	mr := mux.NewRouter()
 	mr.HandleFunc("/api/namespaces/{namespace}/applications/{app}/graph", func(w http.ResponseWriter, r *http.Request) {
-		code, config := graphNodeIstio(r.Context(), biz, client, graph.NewOptions(r, &biz.Namespace))
+		code, config := graphNodeIstio(r.Context(), biz, client, graph.NewOptions(r, biz))
 		respond(w, code, config)
 	},
 	)
@@ -2440,7 +2440,7 @@ func TestVersionedAppNodeGraph(t *testing.T) {
 
 	mr := mux.NewRouter()
 	mr.HandleFunc("/api/namespaces/{namespace}/applications/{app}/versions/{version}/graph", func(w http.ResponseWriter, r *http.Request) {
-		options := graph.NewOptions(r, &biz.Namespace)
+		options := graph.NewOptions(r, biz)
 		options.Rates.Ambient = graph.AmbientTrafficNone
 		code, config := graphNodeIstio(r.Context(), biz, client, options)
 		respond(w, code, config)
@@ -2529,7 +2529,7 @@ func TestServiceNodeGraph(t *testing.T) {
 
 	mr := mux.NewRouter()
 	mr.HandleFunc("/api/namespaces/{namespace}/services/{service}/graph", func(w http.ResponseWriter, r *http.Request) {
-		options := graph.NewOptions(r, &biz.Namespace)
+		options := graph.NewOptions(r, biz)
 		options.Rates.Ambient = graph.AmbientTrafficNone
 		code, config := graphNodeIstio(r.Context(), biz, client, options)
 		respond(w, code, config)
@@ -2959,7 +2959,7 @@ func TestRatesNodeGraphTotal(t *testing.T) {
 
 	mr := mux.NewRouter()
 	mr.HandleFunc("/api/namespaces/{namespace}/workloads/{workload}/graph", func(w http.ResponseWriter, r *http.Request) {
-		options := graph.NewOptions(r, &biz.Namespace)
+		options := graph.NewOptions(r, biz)
 		options.Rates.Ambient = graph.AmbientTrafficNone
 		code, config := graphNodeIstio(r.Context(), biz, client, options)
 		respond(w, code, config)
@@ -3515,7 +3515,7 @@ func TestComplexGraph(t *testing.T) {
 
 	mr := mux.NewRouter()
 	mr.HandleFunc("/api/namespaces/graph", func(w http.ResponseWriter, r *http.Request) {
-		options := graph.NewOptions(r, &biz.Namespace)
+		options := graph.NewOptions(r, biz)
 		options.Rates.Ambient = graph.AmbientTrafficNone
 		code, config := graphNamespacesIstio(r.Context(), biz, client, options)
 		respond(w, code, config)
@@ -3831,7 +3831,7 @@ func TestMultiClusterSourceGraph(t *testing.T) {
 
 	mr := mux.NewRouter()
 	mr.HandleFunc("/api/namespaces/graph", func(w http.ResponseWriter, r *http.Request) {
-		options := graph.NewOptions(r, &biz.Namespace)
+		options := graph.NewOptions(r, biz)
 		options.Rates.Ambient = graph.AmbientTrafficNone
 		code, config := graphNamespacesIstio(r.Context(), biz, client, options)
 		respond(w, code, config)
@@ -4170,7 +4170,7 @@ func TestAmbientGraph(t *testing.T) {
 
 	mr := mux.NewRouter()
 	mr.HandleFunc("/api/namespaces/graph", func(w http.ResponseWriter, r *http.Request) {
-		code, config := graphNamespacesIstio(r.Context(), businessLayer, client, graph.NewOptions(r, &businessLayer.Namespace))
+		code, config := graphNamespacesIstio(r.Context(), businessLayer, client, graph.NewOptions(r, businessLayer))
 		respond(w, code, config)
 	},
 	)
diff --git a/handlers/mesh_test.go b/handlers/mesh_test.go
index 30b0e819c..938469a76 100644
--- a/handlers/mesh_test.go
+++ b/handlers/mesh_test.go
@@ -78,13 +78,13 @@ func TestGetMeshGraph(t *testing.T) {
 
 	clients := map[string]kubernetes.UserClientInterface{
 		conf.KubernetesConfig.ClusterName: kubetest.NewFakeK8sClient(
-			kubetest.FakeNamespace(conf.IstioNamespace),
+			kubetest.FakeNamespace(config.IstioNamespaceDefault),
 			// Ideally we wouldn't need to set all this stuff up here but there's not a good way
 			// mock out the business.IstioStatus service since it's a struct.
 			&appsv1.Deployment{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "istiod",
-					Namespace: conf.IstioNamespace,
+					Namespace: config.IstioNamespaceDefault,
 					Labels: map[string]string{
 						"app":          "istiod",
 						"istio.io/rev": "default",
@@ -101,12 +101,12 @@ func TestGetMeshGraph(t *testing.T) {
 			&corev1.ConfigMap{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "istio",
-					Namespace: conf.IstioNamespace,
+					Namespace: config.IstioNamespaceDefault,
 					Labels:    map[string]string{"istio.io/rev": "default"},
 				},
 				Data: map[string]string{"mesh": ""},
 			},
-			FakeCertificateConfigMap(conf.IstioNamespace),
+			FakeCertificateConfigMap(config.IstioNamespaceDefault),
 		),
 	}
 	cf := kubetest.NewFakeClientFactory(conf, clients)
diff --git a/istio/discovery_selectors_test.go b/istio/discovery_selectors_test.go
index 69840dbb2..e7b6c332c 100644
--- a/istio/discovery_selectors_test.go
+++ b/istio/discovery_selectors_test.go
@@ -11,7 +11,7 @@ import (
 )
 
 func TestGetKialiDiscoverySelectors(t *testing.T) {
-	assert.Nil(t, GetKialiDiscoverySelectors("a", nil))
+	assert.Nil(t, GetKialiDiscoverySelectors([]string{}, "a", nil))
 
 	// config with no selectors defined
 	cfgNoSelectors := config.Config{
@@ -294,7 +294,7 @@ func TestGetKialiDiscoverySelectors(t *testing.T) {
 			config, err := config.Unmarshal(yaml)
 			assert.Nil(err)
 
-			selectors := GetKialiDiscoverySelectors(tc.clusterName, config)
+			selectors := GetKialiDiscoverySelectors([]string{}, tc.clusterName, config)
 			filteredNamespaces := FilterNamespacesWithDiscoverySelectors(tc.allNamespaces, selectors)
 			assert.Equal(tc.matchedNamespaces, filteredNamespaces)
 		})
@@ -302,11 +302,10 @@ func TestGetKialiDiscoverySelectors(t *testing.T) {
 }
 
 func TestGetDiscoverySelectorsForCluster(t *testing.T) {
-	assert.Nil(t, GetDiscoverySelectorsForCluster("cluster1", nil))
+	assert.Nil(t, GetDiscoverySelectorsForCluster(t.Context(), nil, "cluster1", nil))
 
 	// config with only "cluster1" override selectors
 	overrideSelectors := config.Config{
-		IstioNamespace: "istio-system",
 		Deployment: config.DeploymentConfig{
 			DiscoverySelectors: config.DiscoverySelectorsConfig{
 				Default: nil,
@@ -362,7 +361,7 @@ func TestGetDiscoverySelectorsForCluster(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
 			assert := assert.New(t)
 
-			selectors := GetDiscoverySelectorsForCluster(tc.clusterName, &tc.config)
+			selectors := GetKialiDiscoverySelectors([]string{"istio-system"}, tc.clusterName, &tc.config)
 			filteredNamespaces := FilterNamespacesWithDiscoverySelectors(tc.allNamespaces, selectors)
 			assert.Equal(tc.matchedNamespaces, filteredNamespaces)
 		})
diff --git a/istio/discovery_test.go b/istio/discovery_test.go
index d48c30499..aff470547 100644
--- a/istio/discovery_test.go
+++ b/istio/discovery_test.go
@@ -106,7 +106,6 @@ func TestGetClustersResolvesTheKialiCluster(t *testing.T) {
 	require := require.New(t)
 
 	conf := config.NewConfig()
-	conf.InCluster = false
 	conf.KubernetesConfig.ClusterName = "KialiCluster"
 
 	istioDeploymentMock := apps_v1.Deployment{
@@ -214,9 +213,8 @@ func TestGetClustersResolvesRemoteClusters(t *testing.T) {
 	check := require.New(t)
 
 	conf := config.NewConfig()
-	conf.InCluster = false
 
-	remoteNs := kubetest.FakeNamespaceWithLabels(conf.IstioNamespace, map[string]string{"topology.istio.io/network": "TheRemoteNetwork"})
+	remoteNs := kubetest.FakeNamespaceWithLabels(config.IstioNamespaceDefault, map[string]string{"topology.istio.io/network": "TheRemoteNetwork"})
 
 	kialiSvc := &core_v1.Service{
 		ObjectMeta: v1.ObjectMeta{
@@ -228,7 +226,7 @@ func TestGetClustersResolvesRemoteClusters(t *testing.T) {
 				"app.kubernetes.io/part-of": "kiali",
 			},
 			Name:      "kiali-service",
-			Namespace: conf.IstioNamespace,
+			Namespace: config.IstioNamespaceDefault,
 		},
 		Spec: core_v1.ServiceSpec{
 			Selector: map[string]string{
@@ -266,7 +264,7 @@ func TestGetClustersResolvesRemoteClusters(t *testing.T) {
 	check.Equal("https://192.168.144.17:123", remoteCluster.ApiEndpoint)
 
 	check.Len(remoteCluster.KialiInstances, 1, "GetClusters didn't resolve the remote Kiali instance")
-	check.Equal(conf.IstioNamespace, remoteCluster.KialiInstances[0].Namespace, "GetClusters didn't set the right namespace of the Kiali instance")
+	check.Equal(config.IstioNamespaceDefault, remoteCluster.KialiInstances[0].Namespace, "GetClusters didn't set the right namespace of the Kiali instance")
 	check.Equal("kiali-operator/myKialiCR", remoteCluster.KialiInstances[0].OperatorResource, "GetClusters didn't set the right operator resource of the Kiali instance")
 	check.Equal("", remoteCluster.KialiInstances[0].Url, "GetClusters didn't set the right URL of the Kiali instance")
 	check.Equal("v1.25", remoteCluster.KialiInstances[0].Version, "GetClusters didn't set the right version of the Kiali instance")
@@ -293,7 +291,6 @@ func TestResolveKialiControlPlaneClusterIsCached(t *testing.T) {
 
 	// Prepare mocks for first time call.
 	conf := config.NewConfig()
-	conf.IstioNamespace = "foo"
 	conf.ExternalServices.Istio.IstiodDeploymentName = "bar"
 	conf.KubernetesConfig.ClusterName = "KialiCluster"
 
@@ -2587,3 +2584,174 @@ func TestSharedMeshConfig(t *testing.T) {
 		})
 	}
 }
+
+func TestIsControlPlane(t *testing.T) {
+	require := require.New(t)
+	conf := config.NewConfig()
+	conf.KubernetesConfig.ClusterName = "cluster1"
+
+	// Create istiod deployment for cluster1
+	istiodDeployment := fakeIstiodDeployment("cluster1", false)
+
+	// Create istio config map
+	const configMapData = `accessLogFile: /dev/stdout
+enableAutoMtls: true
+rootNamespace: istio-system
+trustDomain: cluster.local
+`
+	istioConfigMap := &core_v1.ConfigMap{
+		ObjectMeta: v1.ObjectMeta{
+			Name:      "istio",
+			Namespace: "istio-system",
+			Labels:    map[string]string{config.IstioRevisionLabel: "default"},
+		},
+		Data: map[string]string{"mesh": configMapData},
+	}
+
+	// Set up client for cluster1 with control plane in istio-system
+	k8sClient := kubetest.NewFakeK8sClient(
+		kubetest.FakeNamespace("istio-system"),
+		kubetest.FakeNamespace("bookinfo"),
+		kubetest.FakeNamespace("default"),
+		istiodDeployment,
+		istioConfigMap,
+		istio.FakeCertificateConfigMap("istio-system"),
+	)
+
+	// Set up clients for discovery
+	clients := map[string]kubernetes.ClientInterface{
+		"cluster1": k8sClient,
+	}
+	cache := cache.NewTestingCacheWithClients(t, clients, *conf)
+	discovery := istio.NewDiscovery(clients, cache, conf)
+
+	ctx := context.Background()
+
+	// Test: namespace is a control plane namespace in the specified cluster
+	require.True(discovery.IsControlPlane(ctx, "cluster1", "istio-system"),
+		"Should return true when namespace matches control plane namespace")
+
+	// Test: namespace is not a control plane namespace
+	require.False(discovery.IsControlPlane(ctx, "cluster1", "bookinfo"),
+		"Should return false when namespace is not a control plane namespace")
+
+	require.False(discovery.IsControlPlane(ctx, "cluster1", "default"),
+		"Should return false when namespace is not a control plane namespace")
+
+	// Test: empty cluster parameter should ignore cluster and only check namespace
+	require.True(discovery.IsControlPlane(ctx, "", "istio-system"),
+		"Should return true when cluster is empty and namespace matches control plane")
+
+	require.False(discovery.IsControlPlane(ctx, "", "bookinfo"),
+		"Should return false when cluster is empty and namespace doesn't match control plane")
+
+	// Test: non-existent cluster
+	require.False(discovery.IsControlPlane(ctx, "non-existent", "istio-system"),
+		"Should return false for non-existent cluster")
+
+	// Test: empty namespace
+	require.False(discovery.IsControlPlane(ctx, "cluster1", ""),
+		"Should return false for empty namespace")
+}
+
+func TestGetControlPlaneNamespaces(t *testing.T) {
+	require := require.New(t)
+	conf := config.NewConfig()
+	conf.KubernetesConfig.ClusterName = "cluster1"
+
+	// Create istiod deployment for cluster1
+	istiodDeploymentCluster1 := fakeIstiodDeployment("cluster1", false)
+
+	// Create istio config map
+	const configMapData = `accessLogFile: /dev/stdout
+enableAutoMtls: true
+rootNamespace: istio-system
+trustDomain: cluster.local
+`
+	istioConfigMapCluster1 := &core_v1.ConfigMap{
+		ObjectMeta: v1.ObjectMeta{
+			Name:      "istio",
+			Namespace: "istio-system",
+			Labels:    map[string]string{config.IstioRevisionLabel: "default"},
+		},
+		Data: map[string]string{"mesh": configMapData},
+	}
+
+	// Set up client for cluster1 with control plane in istio-system
+	cluster1Client := kubetest.NewFakeK8sClient(
+		kubetest.FakeNamespace("istio-system"),
+		kubetest.FakeNamespace("bookinfo"),
+		kubetest.FakeNamespace("default"),
+		istiodDeploymentCluster1,
+		istioConfigMapCluster1,
+		istio.FakeCertificateConfigMap("istio-system"),
+	)
+
+	// Create istiod deployment for cluster2 with different namespace
+	istiodDeploymentCluster2 := fakeIstiodDeployment("cluster2", false)
+	istiodDeploymentCluster2.Namespace = "istio-control"
+
+	istioConfigMapCluster2 := &core_v1.ConfigMap{
+		ObjectMeta: v1.ObjectMeta{
+			Name:      "istio",
+			Namespace: "istio-control",
+			Labels:    map[string]string{config.IstioRevisionLabel: "default"},
+		},
+		Data: map[string]string{"mesh": configMapData},
+	}
+
+	// Set up client for cluster2 with control plane in istio-control
+	cluster2Client := kubetest.NewFakeK8sClient(
+		kubetest.FakeNamespace("istio-control"),
+		kubetest.FakeNamespace("production"),
+		istiodDeploymentCluster2,
+		istioConfigMapCluster2,
+		istio.FakeCertificateConfigMap("istio-control"),
+	)
+
+	// Set up clients for discovery
+	clients := map[string]kubernetes.ClientInterface{
+		"cluster1": cluster1Client,
+		"cluster2": cluster2Client,
+	}
+	cacheNotEmpty := cache.NewTestingCacheWithClients(t, clients, *conf)
+	discovery := istio.NewDiscovery(clients, cacheNotEmpty, conf)
+
+	ctx := context.Background()
+
+	// Test: get control plane namespaces for specific cluster1
+	namespaces := discovery.GetControlPlaneNamespaces(ctx, "cluster1")
+	require.Len(namespaces, 1, "Should return exactly one control plane namespace for cluster1")
+	require.Contains(namespaces, "istio-system", "Should contain istio-system namespace for cluster1")
+
+	// Test: get control plane namespaces for specific cluster2
+	namespaces = discovery.GetControlPlaneNamespaces(ctx, "cluster2")
+	require.Len(namespaces, 1, "Should return exactly one control plane namespace for cluster2")
+	require.Contains(namespaces, "istio-control", "Should contain istio-control namespace for cluster2")
+
+	// Test: get control plane namespaces for all clusters (empty cluster parameter)
+	namespaces = discovery.GetControlPlaneNamespaces(ctx, "")
+	require.Len(namespaces, 2, "Should return control plane namespaces from all clusters")
+	require.Contains(namespaces, "istio-system", "Should contain istio-system namespace from cluster1")
+	require.Contains(namespaces, "istio-control", "Should contain istio-control namespace from cluster2")
+
+	// Test: get control plane namespaces for non-existent cluster
+	namespaces = discovery.GetControlPlaneNamespaces(ctx, "non-existent")
+	require.Empty(namespaces, "Should return empty slice for non-existent cluster")
+
+	// Test: get control plane namespaces for cluster with no control plane
+	emptyClient := kubetest.NewFakeK8sClient(
+		kubetest.FakeNamespace("default"),
+		kubetest.FakeNamespace("kube-system"),
+	)
+	clientsWithEmpty := map[string]kubernetes.ClientInterface{
+		"cluster1":      cluster1Client,
+		"cluster2":      cluster2Client,
+		"empty-cluster": emptyClient,
+	}
+	cacheEmpty := cache.NewTestingCacheWithClients(t, clientsWithEmpty, *conf)
+	discoveryWithEmpty := istio.NewDiscovery(clientsWithEmpty, cacheEmpty, conf)
+
+	namespaces = discoveryWithEmpty.GetControlPlaneNamespaces(ctx, "empty-cluster")
+	require.Empty(namespaces, "Should return empty slice for cluster with no control plane")
+}
diff --git a/istio/istiotest/fake.go b/istio/istiotest/fake.go
index ed22e18da..6bb5865a9 100644
--- a/istio/istiotest/fake.go
+++ b/istio/istiotest/fake.go
@@ -10,14 +10,26 @@ import (
 type FakeDiscovery struct {
 	// ClustersReturn is the return value of Clusters().
 	ClustersReturn []models.KubeCluster
+	// GetControlPlaneNamespacesReturn is the return value of GetControlPlaneNamespaces().
+	GetControlPlaneNamespacesReturn []string
+	// IsControlPlaneReturn is the return value of IsControlPlane().
+	IsControlPlaneReturn bool
 	// MeshReturn is the return value of Mesh().
 	MeshReturn models.Mesh
 }
 
-func (fmd *FakeDiscovery) Mesh(ctx context.Context) (*models.Mesh, error) {
-	return &fmd.MeshReturn, nil
-}
-
 func (fmd *FakeDiscovery) Clusters() ([]models.KubeCluster, error) {
 	return fmd.ClustersReturn, nil
 }
+
+func (fmd *FakeDiscovery) GetControlPlaneNamespaces(ctx context.Context, cluster string) []string {
+	return fmd.GetControlPlaneNamespacesReturn
+}
+
+func (fmd *FakeDiscovery) IsControlPlane(ctx context.Context, cluster, namespace string) bool {
+	return fmd.IsControlPlaneReturn
+}
+
+func (fmd *FakeDiscovery) Mesh(ctx context.Context) (*models.Mesh, error) {
+	return &fmd.MeshReturn, nil
+}
diff --git a/kubernetes/client_factory_test.go b/kubernetes/client_factory_test.go
index 096c1d1d5..b78c96ac6 100644
--- a/kubernetes/client_factory_test.go
+++ b/kubernetes/client_factory_test.go
@@ -39,6 +39,7 @@ func TestClientExpiration(t *testing.T) {
 	conf := config.Get()
 	conf.Auth.Strategy = config.AuthStrategyOpenId
 	conf.Auth.OpenId.DisableRBAC = false
+	conf.KubernetesConfig.ClusterName = config.DefaultClusterID
 	SetConfig(t, *conf)
 
 	clientFactory := NewTestingClientFactory(t)
@@ -100,7 +101,7 @@ func TestConcurrentClientExpiration(t *testing.T) {
 			defer wg.Done()
 			authInfo := api.NewAuthInfo()
 			authInfo.Token = fmt.Sprintf("%d", rand.Intn(10000000000))
-			_, innerErr := clientFactory.getRecycleClient(authInfo, 10*time.Millisecond, config.Get().KubernetesConfig.ClusterName)
+			_, innerErr := clientFactory.getRecycleClient(authInfo, 10*time.Millisecond, config.DefaultClusterID)
 			assert.NoError(innerErr)
 		}()
 	}
@@ -115,7 +116,7 @@ func TestConcurrentClientExpiration(t *testing.T) {
 func TestConcurrentClientFactory(t *testing.T) {
 	require := require.New(t)
 	conf := config.NewConfig()
-	istioConfig := rest.Config{}
+	istioConfig := rest.Config{Host: "Kubernetes"}
 	count := 100
 
 	wg := sync.WaitGroup{}
@@ -143,6 +144,7 @@ func TestClientCreatedWithClusterInfo(t *testing.T) {
 	assert := assert.New(t)
 
 	conf := config.NewConfig()
+	conf.KubernetesConfig.ClusterName = config.DefaultClusterID
 	config.Set(conf)
 
 	const testClusterName = "TestRemoteCluster"
@@ -431,8 +433,8 @@ func TestClientCreatedWithProxyInfo(t *testing.T) {
 			require := require.New(t)
 
 			cfg := config.NewConfig()
-			cfg.Deployment.RemoteSecretPath = t.TempDir() // Random dir so that the remote secret isn't read if it exists.
 			cfg.Auth = tc.auth
+			cfg.KubernetesConfig.ClusterName = config.DefaultClusterID
 			SetConfig(t, *cfg)
 
 			clientFactory := NewTestingClientFactory(t)
@@ -463,15 +465,16 @@ func TestNewClientFactoryClosesRecycleWhenCTXCancelled(t *testing.T) {
 	// Create the remote secret so that the "in cluster" config is not used.
 	// Otherwise the "in cluster" config looks for some env vars that are not present.
 	const testClusterName = "TestRemoteCluster"
-	filename := createTestRemoteClusterSecret(t, testClusterName, remoteClusterYAML)
+	createTestRemoteClusterSecret(t, testClusterName, remoteClusterYAML)
 
 	cfg := config.NewConfig()
-	cfg.Deployment.RemoteSecretPath = filename
+	cfg.Clustering.IgnoreHomeCluster = true
+	cfg.KubernetesConfig.ClusterName = testClusterName
 	SetConfig(t, *cfg)
 
 	ctx, cancel := context.WithCancel(context.Background())
 	t.Cleanup(cancel)
-	f, err := NewClientFactory(ctx, *cfg)
+	f, err := NewClientFactory(ctx, cfg)
 	t.Cleanup(func() {
 		KialiTokenForHomeCluster = ""     // Need to reset this global because other tests depend on it being empty.
 		KialiTokenFileForHomeCluster = "" // Need to reset this global because other tests depend on it being empty.
@@ -499,15 +502,16 @@ func TestNewClientFactoryDoesNotSetGlobalClientFactory(t *testing.T) {
 	// Create the remote secret so that the "in cluster" config is not used.
 	// Otherwise the "in cluster" config looks for some env vars that are not present.
 	const testClusterName = "TestRemoteCluster"
-	filename := createTestRemoteClusterSecret(t, testClusterName, remoteClusterYAML)
+	createTestRemoteClusterSecret(t, testClusterName, remoteClusterYAML)
 
 	cfg := config.NewConfig()
-	cfg.Deployment.RemoteSecretPath = filename
+	cfg.Clustering.IgnoreHomeCluster = true
+	cfg.KubernetesConfig.ClusterName = testClusterName
 	SetConfig(t, *cfg)
 
 	ctx, cancel := context.WithCancel(context.Background())
 	t.Cleanup(cancel)
-	_, err := NewClientFactory(ctx, *cfg)
+	_, err := NewClientFactory(ctx, cfg)
 	t.Cleanup(func() {
 		KialiTokenForHomeCluster = ""     // Need to reset this global because other tests depend on it being empty.
 		KialiTokenFileForHomeCluster = "" // Need to reset this global because other tests depend on it being empty.
@@ -517,29 +521,24 @@ func TestNewClientFactoryDoesNotSetGlobalClientFactory(t *testing.T) {
 	require.Nil(factory)
 }
 
-func TestClientFactoryReturnsSAClientWhenConfigClusterNameIsEmpty(t *testing.T) {
+func TestClientFactoryReturnsNilWhenLocalClusterIsIgnored(t *testing.T) {
 	require := require.New(t)
 
-	// Create the remote secret so that the "in cluster" config is not used.
-	// Otherwise the "in cluster" config looks for some env vars that are not present.
-	const testClusterName = "TestRemoteCluster"
-	filename := createTestRemoteClusterSecret(t, testClusterName, remoteClusterYAML)
-
 	cfg := config.NewConfig()
-	cfg.Deployment.RemoteSecretPath = filename
 	cfg.KubernetesConfig.ClusterName = ""
+	// Ignore the local cluster, otherwise the "in cluster" config looks for some env vars that are not present.
+	cfg.Clustering.IgnoreHomeCluster = true
 	SetConfig(t, *cfg)
 
 	ctx, cancel := context.WithCancel(context.Background())
 	t.Cleanup(cancel)
-	clientFactory, err := NewClientFactory(ctx, *cfg)
+	clientFactory, err := NewClientFactory(ctx, cfg)
 	t.Cleanup(func() {
 		KialiTokenForHomeCluster = ""     // Need to reset this global because other tests depend on it being empty.
 		KialiTokenFileForHomeCluster = "" // Need to reset this global because other tests depend on it being empty.
 	})
-	require.NoError(err)
-
-	require.NotNil(clientFactory.GetSAHomeClusterClient())
+	require.Error(err)
+	require.Nil(clientFactory)
 }
 
 func TestClientFactoryGetClients(t *testing.T) {
diff --git a/kubernetes/cluster_secret_test.go b/kubernetes/cluster_secret_test.go
index cb7b8820d..52b18b821 100644
--- a/kubernetes/cluster_secret_test.go
+++ b/kubernetes/cluster_secret_test.go
@@ -11,6 +11,8 @@ import (
 func TestReloadRemoteClusterSecret(t *testing.T) {
 	check := assert.New(t)
 	conf := config.NewConfig()
+	conf.KubernetesConfig.ClusterName = config.DefaultClusterID
+	config.Set(conf)
 
 	const testClusterName = "TestRemoteCluster"
 
diff --git a/kubernetes/istio_test.go b/kubernetes/istio_test.go
index d3baba6cb..19e1382d5 100644
--- a/kubernetes/istio_test.go
+++ b/kubernetes/istio_test.go
@@ -46,7 +46,7 @@ func TestGetClusterInfoFromIstiod(t *testing.T) {
 			},
 		},
 	)
-	clusterID, err := kubernetes.ClusterNameFromIstiod(*conf, k8s)
+	clusterID, err := kubernetes.ClusterNameFromIstiod(conf, k8s)
 	require.NoError(err)
 
 	assert.Equal("east", clusterID)
@@ -78,7 +78,7 @@ func TestGetClusterInfoFromIstiodFails(t *testing.T) {
 			},
 		},
 	)
-	_, err := kubernetes.ClusterNameFromIstiod(*conf, k8s)
+	_, err := kubernetes.ClusterNameFromIstiod(conf, k8s)
 	require.Error(err)
 }
 
@@ -130,7 +130,7 @@ func TestClusterNameFromIstiodUsesConfigWhenSet(t *testing.T) {
 		},
 	)
 
-	clusterName, err := kubernetes.ClusterNameFromIstiod(*conf, k8s)
+	clusterName, err := kubernetes.ClusterNameFromIstiod(conf, k8s)
 	require.NoError(err)
 
 	require.Equal("west", clusterName)
@@ -184,7 +184,7 @@ func TestClusterNameFromIstiodResolvesClusterWithoutConfig(t *testing.T) {
 		},
 	)
 
-	clusterName, err := kubernetes.ClusterNameFromIstiod(*conf, k8s)
+	clusterName, err := kubernetes.ClusterNameFromIstiod(conf, k8s)
 	require.NoError(err)
 
 	require.Equal("west", clusterName)
diff --git a/kubernetes/testdata/remote-cluster-multiple-users.yaml b/kubernetes/testdata/remote-cluster-multiple-users.yaml
deleted file mode 100644
index 3d3675bba..000000000
--- a/kubernetes/testdata/remote-cluster-multiple-users.yaml
+++ /dev/null
@@ -1,20 +0,0 @@
-apiVersion: v1
-clusters:
-  - cluster:
-      server: https://192.168.1.2:1234
-    name: TestRemoteCluster
-contexts:
-  - context:
-      cluster: TestRemoteCluster
-      user: remoteuser2
-    name: TestRemoteClusterUser2
-current-context: "TestRemoteClusterUser2"
-kind: Config
-preferences: {}
-users:
-  - name: remoteuser1
-    user:
-      token: token1
-  - name: remoteuser2
-    user:
-      token: token2
diff --git a/kubernetes/testing.go b/kubernetes/testing.go
index 1bd58dfa6..97a135e88 100644
--- a/kubernetes/testing.go
+++ b/kubernetes/testing.go
@@ -55,7 +55,7 @@ func NewTestingClientFactory(t *testing.T) *clientFactory {
 		t.Fatalf("Unable to create token file for testing. Err: %s", err)
 	}
 
-	clientConfig := rest.Config{}
+	clientConfig := rest.Config{Host: "Kubernetes"}
 	client, err := newClientFactory(config.Get(), &clientConfig)
 	if err != nil {
 		t.Fatalf("Error creating client factory: %v", err)
diff --git a/kubernetes/token_test.go b/kubernetes/token_test.go
index 0ab8048a1..16c381490 100644
--- a/kubernetes/token_test.go
+++ b/kubernetes/token_test.go
@@ -3,7 +3,6 @@ package kubernetes
 import (
 	"os"
 	"testing"
-	"time"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -21,7 +20,6 @@ func TestIsTokenExpired(t *testing.T) {
 	require := require.New(t)
 	assert := assert.New(t)
 	config := config.NewConfig()
-	config.Deployment.RemoteSecretPath = t.TempDir()
 	SetConfig(t, *config)
 
 	DefaultServiceAccountPath = tmpFileTokenExpired
@@ -39,7 +37,6 @@ func TestGetKialiToken(t *testing.T) {
 	require := require.New(t)
 	assert := assert.New(t)
 	config := config.NewConfig()
-	config.Deployment.RemoteSecretPath = t.TempDir()
 	SetConfig(t, *config)
 
 	DefaultServiceAccountPath = tmpFileGetToken
@@ -53,20 +50,6 @@ func TestGetKialiToken(t *testing.T) {
 	assert.Equal(data, token)
 }
 
-func TestGetKialiTokenRemoteCluster(t *testing.T) {
-	require := require.New(t)
-
-	config := config.NewConfig()
-	config.Deployment.RemoteSecretPath = "testdata/remote-cluster-multiple-users.yaml"
-	SetConfig(t, *config)
-	tokenRead = time.Time{}
-
-	token, _, err := GetKialiTokenForHomeCluster()
-	require.NoError(err)
-
-	require.Equal("token2", token)
-}
-
 // Aux func to setup files
 func setupFile(t *testing.T, content string, name string) {
 	data := []byte(content)
diff --git a/mesh/api/testdata/test_mesh_graph.expected b/mesh/api/testdata/test_mesh_graph.expected
index a3577e94a..29103c25a 100644
--- a/mesh/api/testdata/test_mesh_graph.expected
+++ b/mesh/api/testdata/test_mesh_graph.expected
@@ -1,518 +1,572 @@
 {
-  "elements" : {
-    "nodes" : [ {
-      "data" : {
-        "id" : "85774128be9f0db40aeb4c610e9436f63cb0945838e6db0c05ae36b62c6c6c10",
-        "cluster" : "_external_",
-        "infraName" : "External Deployments",
-        "infraType" : "cluster",
-        "namespace" : "",
-        "nodeType" : "box",
-        "healthData" : "Healthy",
-        "isBox" : "cluster",
-        "isExternal" : true,
-        "isInaccessible" : true
-      }
-    }, {
-      "data" : {
-        "id" : "52e2139bb2622c3156273a148bb95c794e674e820ffcf433a3e530e31e5fcd36",
-        "cluster" : "cluster-primary",
-        "infraName" : "cluster-primary",
-        "infraType" : "cluster",
-        "namespace" : "",
-        "nodeType" : "box",
-        "healthData" : "Healthy",
-        "infraData" : {
-          "apiEndpoint" : "http://127.0.0.2:9443",
-          "isKialiHome" : true,
-          "kialiInstances" : [ {
-            "namespace" : "istio-system",
-            "operatorResource" : "",
-            "serviceName" : "kiali",
-            "url" : "",
-            "version" : ""
-          } ],
-          "name" : "cluster-primary",
-          "secretName" : "",
-          "accessible" : true
-        },
-        "isBox" : "cluster",
-        "version" : "Unknown"
-      }
-    }, {
-      "data" : {
-        "id" : "421c6926a61f0f21f1fe0babb48ea9fac6454c1d9a448dea2c559402723ec4da",
-        "cluster" : "cluster-remote",
-        "infraName" : "cluster-remote",
-        "infraType" : "cluster",
-        "namespace" : "",
-        "nodeType" : "box",
-        "healthData" : "Healthy",
-        "infraData" : {
-          "apiEndpoint" : "",
-          "isKialiHome" : false,
-          "kialiInstances" : null,
-          "name" : "cluster-remote",
-          "secretName" : "",
-          "accessible" : true
-        },
-        "isBox" : "cluster"
-      }
-    }, {
-      "data" : {
-        "id" : "5d47c4043773968316f9d71a07212a1c363d76a3d9ffe7c52198f864cde8c5f5",
-        "parent" : "52e2139bb2622c3156273a148bb95c794e674e820ffcf433a3e530e31e5fcd36",
-        "cluster" : "cluster-primary",
-        "infraName" : "data-plane-1",
-        "infraType" : "namespace",
-        "namespace" : "data-plane-1",
-        "nodeType" : "box",
-        "healthData" : null,
-        "isBox" : "namespace"
-      }
-    }, {
-      "data" : {
-        "id" : "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
-        "parent" : "52e2139bb2622c3156273a148bb95c794e674e820ffcf433a3e530e31e5fcd36",
-        "cluster" : "cluster-primary",
-        "infraName" : "istio-system",
-        "infraType" : "namespace",
-        "namespace" : "istio-system",
-        "nodeType" : "box",
-        "healthData" : null,
-        "isBox" : "namespace"
-      }
-    }, {
-      "data" : {
-        "id" : "33493098cdd63801f773396da680159d0bb57b482ef38a4b208899a2e477e1f0",
-        "parent" : "85774128be9f0db40aeb4c610e9436f63cb0945838e6db0c05ae36b62c6c6c10",
-        "cluster" : "_external_",
-        "infraName" : "Grafana",
-        "infraType" : "grafana",
-        "namespace" : "",
-        "nodeType" : "infra",
-        "healthData" : "Healthy",
-        "infraData" : {
-          "auth" : {
-            "caFile" : "xxx",
-            "insecureSkipVerify" : false,
-            "password" : "xxx",
-            "token" : "xxx",
-            "type" : "none",
-            "useKialiToken" : false,
-            "username" : "xxx"
-          },
-          "dashboards" : null,
-          "enabled" : true,
-          "externalURL" : "",
-          "internalURL" : "http://grafana.istio-system:3000"
-        },
-        "isExternal" : true,
-        "isInaccessible" : true
-      }
-    }, {
-      "data" : {
-        "id" : "41b50563edf837e2e828b50492b24356bf392be6e0a7a7f21d90eabe551dd686",
-        "parent" : "85774128be9f0db40aeb4c610e9436f63cb0945838e6db0c05ae36b62c6c6c10",
-        "cluster" : "_external_",
-        "infraName" : "Prometheus",
-        "infraType" : "metricStore",
-        "namespace" : "",
-        "nodeType" : "infra",
-        "healthData" : "Healthy",
-        "infraData" : {
-          "auth" : {
-            "caFile" : "xxx",
-            "insecureSkipVerify" : false,
-            "password" : "xxx",
-            "token" : "xxx",
-            "type" : "none",
-            "useKialiToken" : false,
-            "username" : "xxx"
-          },
-          "cacheDuration" : 7,
-          "cacheEnabled" : true,
-          "cacheExpiration" : 300,
-          "thanosProxy" : {
-            "retentionPeriod" : "7d",
-            "scrapeInterval" : "30s"
-          },
-          "url" : "http://prometheus.istio-system:9090"
-        },
-        "isExternal" : true,
-        "isInaccessible" : true
-      }
-    }, {
-      "data" : {
-        "id" : "e0733a58436c292ece7cbcf37db7b77510545c027d03c1798c3d6d8202b93f6f",
-        "parent" : "85774128be9f0db40aeb4c610e9436f63cb0945838e6db0c05ae36b62c6c6c10",
-        "cluster" : "_external_",
-        "infraName" : "jaeger",
-        "infraType" : "traceStore",
-        "namespace" : "",
-        "nodeType" : "infra",
-        "healthData" : "Healthy",
-        "infraData" : {
-          "auth" : {
-            "caFile" : "xxx",
-            "insecureSkipVerify" : false,
-            "password" : "xxx",
-            "token" : "xxx",
-            "type" : "none",
-            "useKialiToken" : false,
-            "username" : "xxx"
-          },
-          "enabled" : true,
-          "externalURL" : "",
-          "grpcPort" : 9095,
-          "internalURL" : "http://tracing.istio-system:16685/jaeger",
-          "provider" : "jaeger",
-          "tempoConfig" : {
-            "cacheCapacity" : 200,
-            "cacheEnabled" : true
-          },
-          "namespaceSelector" : true,
-          "queryTimeout" : 5,
-          "useGRPC" : true,
-          "whiteListIstioSystem" : [ "jaeger-query", "istio-ingressgateway" ]
-        },
-        "isExternal" : true,
-        "isInaccessible" : true
-      }
-    }, {
-      "data" : {
-        "id" : "0bb699228f65bc52ec82ed3c72726df27b56e7518b38b7e2021196ea0065233c",
-        "parent" : "52e2139bb2622c3156273a148bb95c794e674e820ffcf433a3e530e31e5fcd36",
-        "cluster" : "cluster-primary",
-        "infraName" : "Data Plane",
-        "infraType" : "dataplane",
-        "namespace" : "",
-        "nodeType" : "infra",
-        "healthData" : "Healthy",
-        "infraData" : [ {
-          "name" : "data-plane-1",
-          "cluster" : "cluster-primary",
-          "isAmbient" : false,
-          "labels" : {
-            "istio-injection" : "enabled",
-            "kubernetes.io/metadata.name" : "data-plane-1"
+  "elements": {
+    "nodes": [
+      {
+        "data": {
+          "id": "85774128be9f0db40aeb4c610e9436f63cb0945838e6db0c05ae36b62c6c6c10",
+          "cluster": "_external_",
+          "infraName": "External Deployments",
+          "infraType": "cluster",
+          "namespace": "",
+          "nodeType": "box",
+          "healthData": "Healthy",
+          "isBox": "cluster",
+          "isExternal": true,
+          "isInaccessible": true
+        }
+      },
+      {
+        "data": {
+          "id": "52e2139bb2622c3156273a148bb95c794e674e820ffcf433a3e530e31e5fcd36",
+          "cluster": "cluster-primary",
+          "infraName": "cluster-primary",
+          "infraType": "cluster",
+          "namespace": "",
+          "nodeType": "box",
+          "healthData": "Healthy",
+          "infraData": {
+            "apiEndpoint": "http://127.0.0.2:9443",
+            "isKialiHome": true,
+            "kialiInstances": [
+              {
+                "namespace": "istio-system",
+                "operatorResource": "",
+                "serviceName": "kiali",
+                "url": "",
+                "version": ""
+              }
+            ],
+            "name": "cluster-primary",
+            "secretName": "",
+            "accessible": true
           },
-          "annotations" : null,
-          "revision" : "default"
-        }, {
-          "name" : "data-plane-2",
-          "cluster" : "cluster-primary",
-          "isAmbient" : false,
-          "labels" : {
-            "istio.io/rev" : "default",
-            "kubernetes.io/metadata.name" : "data-plane-2"
+          "isBox": "cluster",
+          "version": "Unknown"
+        }
+      },
+      {
+        "data": {
+          "id": "421c6926a61f0f21f1fe0babb48ea9fac6454c1d9a448dea2c559402723ec4da",
+          "cluster": "cluster-remote",
+          "infraName": "cluster-remote",
+          "infraType": "cluster",
+          "namespace": "",
+          "nodeType": "box",
+          "healthData": "Healthy",
+          "infraData": {
+            "apiEndpoint": "",
+            "isKialiHome": false,
+            "kialiInstances": null,
+            "name": "cluster-remote",
+            "secretName": "",
+            "accessible": true
           },
-          "annotations" : null,
-          "revision" : "default"
-        } ],
-        "version" : "default"
-      }
-    }, {
-      "data" : {
-        "id" : "5d63e313a5acadb4bf9f5bcaa606b69bbaf81a4193a511719a878e9ad8729a5f",
-        "parent" : "5d47c4043773968316f9d71a07212a1c363d76a3d9ffe7c52198f864cde8c5f5",
-        "cluster" : "cluster-primary",
-        "infraName" : "waypoint",
-        "infraType" : "waypoint",
-        "namespace" : "data-plane-1",
-        "nodeType" : "infra",
-        "healthData" : "Healthy",
-        "infraData" : {
-          "Annotations" : { },
-          "Labels" : {
-            "gateway.istio.io/managed" : "istio.io-mesh-controller",
-            "gateway.networking.k8s.io/gateway-name" : "waypoint"
+          "isBox": "cluster"
+        }
+      },
+      {
+        "data": {
+          "id": "5d47c4043773968316f9d71a07212a1c363d76a3d9ffe7c52198f864cde8c5f5",
+          "parent": "52e2139bb2622c3156273a148bb95c794e674e820ffcf433a3e530e31e5fcd36",
+          "cluster": "cluster-primary",
+          "infraName": "data-plane-1",
+          "infraType": "namespace",
+          "namespace": "data-plane-1",
+          "nodeType": "box",
+          "healthData": null,
+          "isBox": "namespace"
+        }
+      },
+      {
+        "data": {
+          "id": "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
+          "parent": "52e2139bb2622c3156273a148bb95c794e674e820ffcf433a3e530e31e5fcd36",
+          "cluster": "cluster-primary",
+          "infraName": "istio-system",
+          "infraType": "namespace",
+          "namespace": "istio-system",
+          "nodeType": "box",
+          "healthData": null,
+          "isBox": "namespace"
+        }
+      },
+      {
+        "data": {
+          "id": "33493098cdd63801f773396da680159d0bb57b482ef38a4b208899a2e477e1f0",
+          "parent": "85774128be9f0db40aeb4c610e9436f63cb0945838e6db0c05ae36b62c6c6c10",
+          "cluster": "_external_",
+          "infraName": "Grafana",
+          "infraType": "grafana",
+          "namespace": "",
+          "nodeType": "infra",
+          "healthData": "Healthy",
+          "infraData": {
+            "auth": {
+              "caFile": "xxx",
+              "insecureSkipVerify": false,
+              "password": "xxx",
+              "token": "xxx",
+              "type": "none",
+              "useKialiToken": false,
+              "username": "xxx"
+            },
+            "dashboards": null,
+            "enabled": true,
+            "externalURL": "",
+            "internalURL": "http://grafana.istio-system:3000"
           },
-          "TemplateAnnotations" : null,
-          "TemplateLabels" : {
-            "gateway.istio.io/managed" : "istio.io-mesh-controller",
-            "gateway.networking.k8s.io/gateway-name" : "waypoint"
-          }
-        },
-        "version" : "default"
-      }
-    }, {
-      "data" : {
-        "id" : "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
-        "parent" : "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
-        "cluster" : "cluster-primary",
-        "infraName" : "kiali",
-        "infraType" : "kiali",
-        "namespace" : "istio-system",
-        "nodeType" : "infra",
-        "healthData" : "Healthy",
-        "infraData" : {
-          "componentStatuses" : {
-            "enabled" : true
+          "isExternal": true,
+          "isInaccessible": true
+        }
+      },
+      {
+        "data": {
+          "id": "41b50563edf837e2e828b50492b24356bf392be6e0a7a7f21d90eabe551dd686",
+          "parent": "85774128be9f0db40aeb4c610e9436f63cb0945838e6db0c05ae36b62c6c6c10",
+          "cluster": "_external_",
+          "infraName": "Prometheus",
+          "infraType": "metricStore",
+          "namespace": "",
+          "nodeType": "infra",
+          "healthData": "Healthy",
+          "infraData": {
+            "auth": {
+              "caFile": "xxx",
+              "insecureSkipVerify": false,
+              "password": "xxx",
+              "token": "xxx",
+              "type": "none",
+              "useKialiToken": false,
+              "username": "xxx"
+            },
+            "cacheDuration": 7,
+            "cacheEnabled": true,
+            "cacheExpiration": 300,
+            "thanosProxy": {
+              "retentionPeriod": "7d",
+              "scrapeInterval": "30s"
+            },
+            "url": "http://prometheus.istio-system:9090"
           },
-          "envoyAdminLocalPort" : 15000,
-          "istioApiEnabled" : true,
-          "istioIdentityDomain" : "svc.cluster.local",
-          "istioInjectionAnnotation" : "sidecar.istio.io/inject",
-          "istioSidecarAnnotation" : "sidecar.istio.io/status",
-          "istiodPodMonitoringPort" : 15014,
-          "istiodPollingIntervalSeconds" : 20,
-          "rootNamespace" : "istio-system",
-          "urlServiceVersion" : "",
-          "validationChangeDetectionEnabled" : true,
-          "validationReconcileInterval" : 60000000000
+          "isExternal": true,
+          "isInaccessible": true
         }
-      }
-    }, {
-      "data" : {
-        "id" : "4ffd132efe1291fcd70490d6add887f9e160a72c4885a83669432bea09de23f9",
-        "parent" : "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
-        "cluster" : "cluster-primary",
-        "infraName" : "gateway",
-        "infraType" : "gateway",
-        "namespace" : "istio-system",
-        "nodeType" : "infra",
-        "healthData" : "Healthy",
-        "infraData" : {
-          "kind" : "Gateway",
-          "apiVersion" : "networking.istio.io/v1",
-          "metadata" : {
-            "name" : "gateway",
-            "namespace" : "istio-system",
-            "resourceVersion" : "999",
-            "creationTimestamp" : null
+      },
+      {
+        "data": {
+          "id": "e0733a58436c292ece7cbcf37db7b77510545c027d03c1798c3d6d8202b93f6f",
+          "parent": "85774128be9f0db40aeb4c610e9436f63cb0945838e6db0c05ae36b62c6c6c10",
+          "cluster": "_external_",
+          "infraName": "jaeger",
+          "infraType": "traceStore",
+          "namespace": "",
+          "nodeType": "infra",
+          "healthData": "Healthy",
+          "infraData": {
+            "auth": {
+              "caFile": "xxx",
+              "insecureSkipVerify": false,
+              "password": "xxx",
+              "token": "xxx",
+              "type": "none",
+              "useKialiToken": false,
+              "username": "xxx"
+            },
+            "enabled": true,
+            "externalURL": "",
+            "grpcPort": 9095,
+            "internalURL": "http://tracing.istio-system:16685/jaeger",
+            "provider": "jaeger",
+            "tempoConfig": {
+              "cacheCapacity": 200,
+              "cacheEnabled": true
+            },
+            "namespaceSelector": true,
+            "queryTimeout": 5,
+            "useGRPC": true,
+            "whiteListIstioSystem": [
+              "jaeger-query",
+              "istio-ingressgateway"
+            ]
           },
-          "spec" : {
-            "selector" : {
-              "istio" : "ingressgateway"
+          "isExternal": true,
+          "isInaccessible": true
+        }
+      },
+      {
+        "data": {
+          "id": "0bb699228f65bc52ec82ed3c72726df27b56e7518b38b7e2021196ea0065233c",
+          "parent": "52e2139bb2622c3156273a148bb95c794e674e820ffcf433a3e530e31e5fcd36",
+          "cluster": "cluster-primary",
+          "infraName": "Data Plane",
+          "infraType": "dataplane",
+          "namespace": "",
+          "nodeType": "infra",
+          "healthData": "Healthy",
+          "infraData": [
+            {
+              "name": "data-plane-1",
+              "cluster": "cluster-primary",
+              "isAmbient": false,
+              "labels": {
+                "istio-injection": "enabled",
+                "kubernetes.io/metadata.name": "data-plane-1"
+              },
+              "annotations": null,
+              "revision": "default"
+            },
+            {
+              "name": "data-plane-2",
+              "cluster": "cluster-primary",
+              "isAmbient": false,
+              "labels": {
+                "istio.io/rev": "default",
+                "kubernetes.io/metadata.name": "data-plane-2"
+              },
+              "annotations": null,
+              "revision": "default"
+            }
+          ],
+          "version": "default"
+        }
+      },
+      {
+        "data": {
+          "id": "5d63e313a5acadb4bf9f5bcaa606b69bbaf81a4193a511719a878e9ad8729a5f",
+          "parent": "5d47c4043773968316f9d71a07212a1c363d76a3d9ffe7c52198f864cde8c5f5",
+          "cluster": "cluster-primary",
+          "infraName": "waypoint",
+          "infraType": "waypoint",
+          "namespace": "data-plane-1",
+          "nodeType": "infra",
+          "healthData": "Healthy",
+          "infraData": {
+            "Annotations": {},
+            "Labels": {
+              "gateway.istio.io/managed": "istio.io-mesh-controller",
+              "gateway.networking.k8s.io/gateway-name": "waypoint"
+            },
+            "TemplateAnnotations": null,
+            "TemplateLabels": {
+              "gateway.istio.io/managed": "istio.io-mesh-controller",
+              "gateway.networking.k8s.io/gateway-name": "waypoint"
             }
           },
-          "status" : { }
-        },
-        "version" : "default"
-      }
-    }, {
-      "data" : {
-        "id" : "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
-        "parent" : "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
-        "cluster" : "cluster-primary",
-        "infraName" : "istiod",
-        "infraType" : "istiod",
-        "namespace" : "istio-system",
-        "nodeType" : "infra",
-        "healthData" : "Healthy",
-        "infraData" : {
-          "cluster" : {
-            "apiEndpoint" : "http://127.0.0.2:9443",
-            "isKialiHome" : true,
-            "kialiInstances" : [ {
-              "namespace" : "istio-system",
-              "operatorResource" : "",
-              "serviceName" : "kiali",
-              "url" : "",
-              "version" : ""
-            } ],
-            "name" : "cluster-primary",
-            "secretName" : "",
-            "accessible" : true
+          "version": "default"
+        }
+      },
+      {
+        "data": {
+          "id": "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
+          "parent": "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
+          "cluster": "cluster-primary",
+          "infraName": "kiali",
+          "infraType": "kiali",
+          "namespace": "istio-system",
+          "nodeType": "infra",
+          "healthData": "Healthy",
+          "infraData": {
+            "ComponentStatuses": {
+              "enabled": true
+            },
+            "ConfigMapName": "",
+            "EnvoyAdminLocalPort": 15000,
+            "GatewayAPIClasses": [],
+            "GatewayAPIClassesLabelSelector": "",
+            "IstioAPIEnabled": true,
+            "IstioIdentityDomain": "svc.cluster.local",
+            "IstioInjectionAnnotation": "sidecar.istio.io/inject",
+            "IstioSidecarInjectorConfigMapName": "",
+            "IstioSidecarAnnotation": "sidecar.istio.io/status",
+            "IstiodDeploymentName": "",
+            "IstiodPodMonitoringPort": 15014,
+            "istiodPollingIntervalSeconds": 20,
+            "rootNamespace": "istio-system",
+            "urlServiceVersion": "",
+            "validationChangeDetectionEnabled": true,
+            "validationReconcileInterval": 60000000000
+          }
+        }
+      },
+      {
+        "data": {
+          "id": "4ffd132efe1291fcd70490d6add887f9e160a72c4885a83669432bea09de23f9",
+          "parent": "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
+          "cluster": "cluster-primary",
+          "infraName": "gateway",
+          "infraType": "gateway",
+          "namespace": "istio-system",
+          "nodeType": "infra",
+          "healthData": "Healthy",
+          "infraData": {
+            "kind": "Gateway",
+            "apiVersion": "networking.istio.io/v1",
+            "metadata": {
+              "name": "gateway",
+              "namespace": "istio-system",
+              "resourceVersion": "999",
+              "creationTimestamp": null
+            },
+            "spec": {
+              "selector": {
+                "istio": "ingressgateway"
+              }
+            },
+            "status": {}
           },
-          "config" : {
-            "certificates" : [ {
-              "dnsNames" : null,
-              "configMapName" : "istio-ca-root-cert",
-              "issuer" : "O=cluster.local",
-              "notBefore" : "2021-07-27T14:37:00Z",
-              "notAfter" : "2031-07-25T14:37:00Z",
-              "error" : "",
-              "accessible" : true,
-              "cluster" : ""
-            } ],
-            "effectiveConfig" : {
-              "configMap" : {
-                "mesh" : {
-                  "accessLogFile" : "/dev/stdout",
-                  "enableAutoMtls" : true,
-                  "trustDomain" : "cluster.local",
-                  "rootNamespace" : "istio-system"
+          "version": "default"
+        }
+      },
+      {
+        "data": {
+          "id": "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
+          "parent": "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
+          "cluster": "cluster-primary",
+          "infraName": "istiod",
+          "infraType": "istiod",
+          "namespace": "istio-system",
+          "nodeType": "infra",
+          "healthData": "Healthy",
+          "infraData": {
+            "cluster": {
+              "apiEndpoint": "http://127.0.0.2:9443",
+              "isKialiHome": true,
+              "kialiInstances": [
+                {
+                  "namespace": "istio-system",
+                  "operatorResource": "",
+                  "serviceName": "kiali",
+                  "url": "",
+                  "version": ""
                 }
-              }
+              ],
+              "name": "cluster-primary",
+              "secretName": "",
+              "accessible": true
             },
-            "network" : "kialiNetwork",
-            "standardConfig" : {
-              "cluster" : "cluster-primary",
-              "configMap" : {
-                "mesh" : {
-                  "accessLogFile" : "/dev/stdout",
-                  "enableAutoMtls" : true,
-                  "trustDomain" : "cluster.local",
-                  "rootNamespace" : "istio-system"
+            "config": {
+              "certificates": [
+                {
+                  "dnsNames": null,
+                  "configMapName": "istio-ca-root-cert",
+                  "issuer": "O=cluster.local",
+                  "notBefore": "2021-07-27T14:37:00Z",
+                  "notAfter": "2031-07-25T14:37:00Z",
+                  "error": "",
+                  "accessible": true,
+                  "cluster": ""
+                }
+              ],
+              "effectiveConfig": {
+                "configMap": {
+                  "mesh": {
+                    "accessLogFile": "/dev/stdout",
+                    "enableAutoMtls": true,
+                    "trustDomain": "cluster.local",
+                    "rootNamespace": "istio-system"
+                  }
                 }
               },
-              "name" : "istio",
-              "namespace" : "istio-system"
+              "network": "kialiNetwork",
+              "standardConfig": {
+                "cluster": "cluster-primary",
+                "configMap": {
+                  "mesh": {
+                    "accessLogFile": "/dev/stdout",
+                    "enableAutoMtls": true,
+                    "trustDomain": "cluster.local",
+                    "rootNamespace": "istio-system"
+                  }
+                },
+                "name": "istio",
+                "namespace": "istio-system"
+              }
+            },
+            "externalControlPlane": false,
+            "id": "cluster-primary",
+            "istiodName": "istiod",
+            "istiodNamespace": "istio-system",
+            "managedClusters": [
+              {
+                "apiEndpoint": "http://127.0.0.2:9443",
+                "isKialiHome": true,
+                "kialiInstances": [
+                  {
+                    "namespace": "istio-system",
+                    "operatorResource": "",
+                    "serviceName": "kiali",
+                    "url": "",
+                    "version": ""
+                  }
+                ],
+                "name": "cluster-primary",
+                "secretName": "",
+                "accessible": true
+              },
+              {
+                "apiEndpoint": "",
+                "isKialiHome": false,
+                "kialiInstances": null,
+                "name": "cluster-remote",
+                "secretName": "",
+                "accessible": true
+              }
+            ],
+            "managesExternal": true,
+            "managedNamespaces": [
+              {
+                "name": "data-plane-1",
+                "cluster": "cluster-primary",
+                "isAmbient": false,
+                "labels": {
+                  "istio-injection": "enabled",
+                  "kubernetes.io/metadata.name": "data-plane-1"
+                },
+                "annotations": null,
+                "revision": "default"
+              },
+              {
+                "name": "data-plane-2",
+                "cluster": "cluster-primary",
+                "isAmbient": false,
+                "labels": {
+                  "istio.io/rev": "default",
+                  "kubernetes.io/metadata.name": "data-plane-2"
+                },
+                "annotations": null,
+                "revision": "default"
+              },
+              {
+                "name": "data-plane-3",
+                "cluster": "cluster-remote",
+                "isAmbient": false,
+                "labels": {
+                  "istio-injection": "enabled",
+                  "kubernetes.io/metadata.name": "data-plane-3"
+                },
+                "annotations": null,
+                "revision": "default"
+              },
+              {
+                "name": "data-plane-4",
+                "cluster": "cluster-remote",
+                "isAmbient": false,
+                "labels": {
+                  "istio.io/rev": "default",
+                  "kubernetes.io/metadata.name": "data-plane-4"
+                },
+                "annotations": null,
+                "revision": "default"
+              }
+            ],
+            "resources": {},
+            "revision": "default",
+            "status": "",
+            "thresholds": {
+              "memory": 0,
+              "cpu": 0
             }
           },
-          "externalControlPlane" : false,
-          "id" : "cluster-primary",
-          "istiodName" : "istiod",
-          "istiodNamespace" : "istio-system",
-          "managedClusters" : [ {
-            "apiEndpoint" : "http://127.0.0.2:9443",
-            "isKialiHome" : true,
-            "kialiInstances" : [ {
-              "namespace" : "istio-system",
-              "operatorResource" : "",
-              "serviceName" : "kiali",
-              "url" : "",
-              "version" : ""
-            } ],
-            "name" : "cluster-primary",
-            "secretName" : "",
-            "accessible" : true
-          }, {
-            "apiEndpoint" : "",
-            "isKialiHome" : false,
-            "kialiInstances" : null,
-            "name" : "cluster-remote",
-            "secretName" : "",
-            "accessible" : true
-          } ],
-          "managesExternal" : true,
-          "managedNamespaces" : [ {
-            "name" : "data-plane-1",
-            "cluster" : "cluster-primary",
-            "isAmbient" : false,
-            "labels" : {
-              "istio-injection" : "enabled",
-              "kubernetes.io/metadata.name" : "data-plane-1"
-            },
-            "annotations" : null,
-            "revision" : "default"
-          }, {
-            "name" : "data-plane-2",
-            "cluster" : "cluster-primary",
-            "isAmbient" : false,
-            "labels" : {
-              "istio.io/rev" : "default",
-              "kubernetes.io/metadata.name" : "data-plane-2"
-            },
-            "annotations" : null,
-            "revision" : "default"
-          }, {
-            "name" : "data-plane-3",
-            "cluster" : "cluster-remote",
-            "isAmbient" : false,
-            "labels" : {
-              "istio-injection" : "enabled",
-              "kubernetes.io/metadata.name" : "data-plane-3"
-            },
-            "annotations" : null,
-            "revision" : "default"
-          }, {
-            "name" : "data-plane-4",
-            "cluster" : "cluster-remote",
-            "isAmbient" : false,
-            "labels" : {
-              "istio.io/rev" : "default",
-              "kubernetes.io/metadata.name" : "data-plane-4"
+          "version": "Unknown"
+        }
+      },
+      {
+        "data": {
+          "id": "5ff0f691c850d727e75813fb484e06f5bad835b005b08caf2a62c0cff7cd5405",
+          "parent": "421c6926a61f0f21f1fe0babb48ea9fac6454c1d9a448dea2c559402723ec4da",
+          "cluster": "cluster-remote",
+          "infraName": "Data Plane",
+          "infraType": "dataplane",
+          "namespace": "",
+          "nodeType": "infra",
+          "healthData": "Healthy",
+          "infraData": [
+            {
+              "name": "data-plane-3",
+              "cluster": "cluster-remote",
+              "isAmbient": false,
+              "labels": {
+                "istio-injection": "enabled",
+                "kubernetes.io/metadata.name": "data-plane-3"
+              },
+              "annotations": null,
+              "revision": "default"
             },
-            "annotations" : null,
-            "revision" : "default"
-          } ],
-          "resources" : { },
-          "revision" : "default",
-          "status" : "",
-          "thresholds" : {
-            "memory" : 0,
-            "cpu" : 0
-          }
-        },
-        "version" : "Unknown"
-      }
-    }, {
-      "data" : {
-        "id" : "5ff0f691c850d727e75813fb484e06f5bad835b005b08caf2a62c0cff7cd5405",
-        "parent" : "421c6926a61f0f21f1fe0babb48ea9fac6454c1d9a448dea2c559402723ec4da",
-        "cluster" : "cluster-remote",
-        "infraName" : "Data Plane",
-        "infraType" : "dataplane",
-        "namespace" : "",
-        "nodeType" : "infra",
-        "healthData" : "Healthy",
-        "infraData" : [ {
-          "name" : "data-plane-3",
-          "cluster" : "cluster-remote",
-          "isAmbient" : false,
-          "labels" : {
-            "istio-injection" : "enabled",
-            "kubernetes.io/metadata.name" : "data-plane-3"
-          },
-          "annotations" : null,
-          "revision" : "default"
-        }, {
-          "name" : "data-plane-4",
-          "cluster" : "cluster-remote",
-          "isAmbient" : false,
-          "labels" : {
-            "istio.io/rev" : "default",
-            "kubernetes.io/metadata.name" : "data-plane-4"
-          },
-          "annotations" : null,
-          "revision" : "default"
-        } ],
-        "version" : "default"
-      }
-    } ],
-    "edges" : [ {
-      "data" : {
-        "id" : "3c1e5459c5b6268c2f2452f3e6b620fd1ae0297b4621175c97e376558bea9d58",
-        "source" : "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
-        "target" : "33493098cdd63801f773396da680159d0bb57b482ef38a4b208899a2e477e1f0"
-      }
-    }, {
-      "data" : {
-        "id" : "3800bf57dd84bfc7bb159fc26de3fd964dc4faa7274b6250dcde483edeba542d",
-        "source" : "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
-        "target" : "41b50563edf837e2e828b50492b24356bf392be6e0a7a7f21d90eabe551dd686"
-      }
-    }, {
-      "data" : {
-        "id" : "3444b8aee4ed1e0a9d9388de97152e089e723e1efae8db5658d9558b12346ac4",
-        "source" : "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
-        "target" : "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2"
-      }
-    }, {
-      "data" : {
-        "id" : "6fb152c8305cf91514cbb0d86796cc62eb47e275cf803af3fbb26224c9ddf600",
-        "source" : "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
-        "target" : "e0733a58436c292ece7cbcf37db7b77510545c027d03c1798c3d6d8202b93f6f"
-      }
-    }, {
-      "data" : {
-        "id" : "8213394da06023363e2d5747d26db6f7e23d0beeacc84fcf97119768822e6dc8",
-        "source" : "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
-        "target" : "0bb699228f65bc52ec82ed3c72726df27b56e7518b38b7e2021196ea0065233c"
-      }
-    }, {
-      "data" : {
-        "id" : "ea8e134859bb70d340128c62d2468a12174517151b37014f82613d6a0957eb8e",
-        "source" : "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
-        "target" : "4ffd132efe1291fcd70490d6add887f9e160a72c4885a83669432bea09de23f9"
-      }
-    }, {
-      "data" : {
-        "id" : "f25288d2a684e5be99e297439adad0d0f65e5893ba592b397f4841a42e3bab68",
-        "source" : "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
-        "target" : "5d63e313a5acadb4bf9f5bcaa606b69bbaf81a4193a511719a878e9ad8729a5f"
+            {
+              "name": "data-plane-4",
+              "cluster": "cluster-remote",
+              "isAmbient": false,
+              "labels": {
+                "istio.io/rev": "default",
+                "kubernetes.io/metadata.name": "data-plane-4"
+              },
+              "annotations": null,
+              "revision": "default"
+            }
+          ],
+          "version": "default"
+        }
       }
-    }, {
-      "data" : {
-        "id" : "1622664f016a337006cce1427c522c2e24c1b1b39548109f49ad478594f7d8ca",
-        "source" : "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
-        "target" : "5ff0f691c850d727e75813fb484e06f5bad835b005b08caf2a62c0cff7cd5405"
+    ],
+    "edges": [
+      {
+        "data": {
+          "id": "3c1e5459c5b6268c2f2452f3e6b620fd1ae0297b4621175c97e376558bea9d58",
+          "source": "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
+          "target": "33493098cdd63801f773396da680159d0bb57b482ef38a4b208899a2e477e1f0"
+        }
+      },
+      {
+        "data": {
+          "id": "3800bf57dd84bfc7bb159fc26de3fd964dc4faa7274b6250dcde483edeba542d",
+          "source": "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
+          "target": "41b50563edf837e2e828b50492b24356bf392be6e0a7a7f21d90eabe551dd686"
+        }
+      },
+      {
+        "data": {
+          "id": "3444b8aee4ed1e0a9d9388de97152e089e723e1efae8db5658d9558b12346ac4",
+          "source": "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
+          "target": "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2"
+        }
+      },
+      {
+        "data": {
+          "id": "6fb152c8305cf91514cbb0d86796cc62eb47e275cf803af3fbb26224c9ddf600",
+          "source": "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
+          "target": "e0733a58436c292ece7cbcf37db7b77510545c027d03c1798c3d6d8202b93f6f"
+        }
+      },
+      {
+        "data": {
+          "id": "8213394da06023363e2d5747d26db6f7e23d0beeacc84fcf97119768822e6dc8",
+          "source": "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
+          "target": "0bb699228f65bc52ec82ed3c72726df27b56e7518b38b7e2021196ea0065233c"
+        }
+      },
+      {
+        "data": {
+          "id": "ea8e134859bb70d340128c62d2468a12174517151b37014f82613d6a0957eb8e",
+          "source": "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
+          "target": "4ffd132efe1291fcd70490d6add887f9e160a72c4885a83669432bea09de23f9"
+        }
+      },
+      {
+        "data": {
+          "id": "f25288d2a684e5be99e297439adad0d0f65e5893ba592b397f4841a42e3bab68",
+          "source": "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
+          "target": "5d63e313a5acadb4bf9f5bcaa606b69bbaf81a4193a511719a878e9ad8729a5f"
+        }
+      },
+      {
+        "data": {
+          "id": "1622664f016a337006cce1427c522c2e24c1b1b39548109f49ad478594f7d8ca",
+          "source": "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
+          "target": "5ff0f691c850d727e75813fb484e06f5bad835b005b08caf2a62c0cff7cd5405"
+        }
       }
-    } ]
+    ]
   },
-  "meshName" : "",
-  "timestamp" : 1523364075
+  "meshName": "",
+  "timestamp": 1523364075
 }
\ No newline at end of file
diff --git a/models/pod_test.go b/models/pod_test.go
index b8deb12c4..ddbfc8dbe 100644
--- a/models/pod_test.go
+++ b/models/pod_test.go
@@ -1,6 +1,7 @@
 package models
 
 import (
+	"context"
 	"testing"
 	"time"
 
@@ -11,6 +12,8 @@ import (
 	"github.com/kiali/kiali/config"
 )
 
+func fakeIsControlPlane(ctx context.Context, c, n string) bool { return false }
+
 func TestPodFullyParsing(t *testing.T) {
 	assert := assert.New(t)
 	config.Set(config.NewConfig())
@@ -37,7 +40,7 @@ func TestPodFullyParsing(t *testing.T) {
 		}}
 
 	pod := Pod{}
-	pod.Parse(&k8sPod)
+	pod.Parse(&k8sPod, fakeIsControlPlane)
 	assert.Equal("details-v1-3618568057-dnkjp", pod.Name)
 	assert.Equal("2018-03-08T14:44:00Z", pod.CreatedAt)
 	assert.Equal(map[string]string{"apps": "details", "version": "v1"}, pod.Labels)
@@ -69,7 +72,7 @@ func TestPodParsingMissingImage(t *testing.T) {
 	}
 
 	pod := Pod{}
-	pod.Parse(&k8sPod)
+	pod.Parse(&k8sPod, fakeIsControlPlane)
 	assert.Equal("details-v1-3618568057-dnkjp", pod.Name)
 	assert.Equal("2018-03-08T14:44:00Z", pod.CreatedAt)
 	assert.Equal(map[string]string{"apps": "details", "version": "v1"}, pod.Labels)
@@ -96,7 +99,7 @@ func TestPodParsingMissingAnnotations(t *testing.T) {
 		}}
 
 	pod := Pod{}
-	pod.Parse(&k8sPod)
+	pod.Parse(&k8sPod, fakeIsControlPlane)
 	assert.Equal("details-v1-3618568057-dnkjp", pod.Name)
 	assert.Equal("2018-03-08T14:44:00Z", pod.CreatedAt)
 	assert.Equal(map[string]string{"apps": "details", "version": "v1"}, pod.Labels)
@@ -118,7 +121,7 @@ func TestPodParsingInvalidAnnotations(t *testing.T) {
 	}
 
 	pod := Pod{}
-	pod.Parse(&k8sPod)
+	pod.Parse(&k8sPod, fakeIsControlPlane)
 	assert.Equal("details-v1-3618568057-dnkjp", pod.Name)
 	assert.Equal("2018-03-08T14:44:00Z", pod.CreatedAt)
 	assert.Equal(map[string]string{"apps": "details", "version": "v1"}, pod.Labels)
@@ -155,7 +158,7 @@ func TestSyncedPodProxiesCount(t *testing.T) {
 
 	pods := make(Pods, 0, 2)
 	pod := &Pod{}
-	pod.Parse(&k8sPod)
+	pod.Parse(&k8sPod, fakeIsControlPlane)
 	pod.ProxyStatus = &ProxyStatus{
 		CDS: "Synced",
 		EDS: "Synced",
@@ -177,7 +180,7 @@ func TestSyncedPodProxiesCount(t *testing.T) {
 
 	pods = make(Pods, 0, 2)
 	pod = &Pod{}
-	pod.Parse(&k8sPod)
+	pod.Parse(&k8sPod, fakeIsControlPlane)
 	pods = append(pods, pod)
 	pods = append(pods, pod)
 	assert.Equal(int32(-1), pods.SyncedPodProxiesCount())
diff --git a/models/service_test.go b/models/service_test.go
index bfb55781a..262dbd214 100644
--- a/models/service_test.go
+++ b/models/service_test.go
@@ -18,7 +18,7 @@ func TestServiceDetailParsing(t *testing.T) {
 
 	service := ServiceDetails{}
 	service.SetService(config.DefaultClusterID, fakeService(), config.Get())
-	service.SetPods(fakePods())
+	service.SetPods(fakePods(), fakeIsControlPlane)
 	service.SetIstioSidecar(fakeWorkloads())
 
 	// Kubernetes Details
diff --git a/server/server_test.go b/server/server_test.go
index 5f52d85c1..0be69b8d9 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -52,6 +52,7 @@ func TestRootContextPath(t *testing.T) {
 	conf.Server.Address = testHostname
 	conf.Server.Port = testPort
 	conf.Auth.Strategy = "anonymous"
+	conf.KubernetesConfig.ClusterName = config.DefaultClusterID
 
 	serverURL := fmt.Sprintf("http://%v", testServerHostPort)
 
@@ -111,6 +112,7 @@ func TestAnonymousMode(t *testing.T) {
 	conf.Server.Address = testHostname
 	conf.Server.Port = testPort
 	conf.Auth.Strategy = "anonymous"
+	conf.KubernetesConfig.ClusterName = config.DefaultClusterID
 
 	serverURL := fmt.Sprintf("http://%v", testServerHostPort)
 	apiURLWithAuthentication := serverURL + "/api/authenticate"
@@ -201,6 +203,7 @@ func TestSecureComm(t *testing.T) {
 	conf.Server.Observability.Metrics.Port = testMetricsPort
 	conf.Server.Profiler.Enabled = true
 	conf.Auth.Strategy = "anonymous"
+	conf.KubernetesConfig.ClusterName = config.DefaultClusterID
 	util.Clock = util.RealClock{}
 
 	serverURL := fmt.Sprintf("https://%v", testServerHostPort)
@@ -319,6 +322,7 @@ func TestTracingConfigured(t *testing.T) {
 	conf.Server.Observability.Tracing.Enabled = true
 	conf.Server.Observability.Tracing.CollectorType = "otel"
 	conf.Auth.Strategy = "anonymous"
+	conf.KubernetesConfig.ClusterName = config.DefaultClusterID
 
 	// Set the global client factory.
 	cf := kubernetes.NewTestingClientFactory(t)
diff --git a/tests/integration/multicluster/mesh_test.go b/tests/integration/multicluster/mesh_test.go
index 961c3a678..1cf0654b5 100644
--- a/tests/integration/multicluster/mesh_test.go
+++ b/tests/integration/multicluster/mesh_test.go
@@ -24,7 +24,7 @@ func TestMeshShowsExternalControlPlane(t *testing.T) {
 
 	require.Len(istiodNodes, 2)
 	for _, node := range istiodNodes {
-		require.Equal(node.Data.Cluster, "Kubernetes")
+		require.Equal("Kubernetes", node.Data.Cluster)
 	}
 
 	// TODO: When this is a mesh page test, need to ensure that there's two controlplanes
diff --git a/tests/integration/tests/remote_istiod_test.go b/tests/integration/tests/remote_istiod_test.go
index 6750b277f..e3bee7e92 100644
--- a/tests/integration/tests/remote_istiod_test.go
+++ b/tests/integration/tests/remote_istiod_test.go
@@ -41,7 +41,7 @@ func TestRemoteIstiod(t *testing.T) {
 	// Set this to something so that the patch isn't omitted for being empty.
 	originalConf.ExternalServices.Istio.Registry = &config.RegistryConfig{}
 
-	ipods, err := kubeClient.AppsV1().Deployments(originalConf.IstioNamespace).List(ctx, metav1.ListOptions{LabelSelector: "app=istiod"})
+	ipods, err := kubeClient.AppsV1().Deployments(config.IstioNamespaceDefault).List(ctx, metav1.ListOptions{LabelSelector: "app=istiod"})
 	istioDeploymentName := ipods.Items[0].Name
 
 	// Register clean up before creating resources in case of failure.
@@ -51,7 +51,7 @@ func TestRemoteIstiod(t *testing.T) {
 		require.NoError(instance.Restart(ctx))
 
 		// Remove service:
-		err = kubeClient.CoreV1().Services(originalConf.IstioNamespace).Delete(ctx, "istiod-debug", metav1.DeleteOptions{})
+		err = kubeClient.CoreV1().Services(config.IstioNamespaceDefault).Delete(ctx, "istiod-debug", metav1.DeleteOptions{})
 		if kubeerrors.IsNotFound(err) {
 			err = nil
 		}
@@ -59,7 +59,7 @@ func TestRemoteIstiod(t *testing.T) {
 
 		log.Debugf("Remove nginx container from istio deployment %s", istioDeploymentName)
 		// Remove nginx container
-		istiod, err := kubeClient.AppsV1().Deployments(originalConf.IstioNamespace).Get(ctx, istioDeploymentName, metav1.GetOptions{})
+		istiod, err := kubeClient.AppsV1().Deployments(config.IstioNamespaceDefault).Get(ctx, istioDeploymentName, metav1.GetOptions{})
 		require.NoError(err)
 
 		for i, container := range istiod.Spec.Template.Spec.Containers {
@@ -68,20 +68,20 @@ func TestRemoteIstiod(t *testing.T) {
 				break
 			}
 		}
-		_, err = kubeClient.AppsV1().Deployments(originalConf.IstioNamespace).Update(ctx, istiod, metav1.UpdateOptions{})
+		_, err = kubeClient.AppsV1().Deployments(config.IstioNamespaceDefault).Update(ctx, istiod, metav1.UpdateOptions{})
 		require.NoError(err)
 
-		require.NoError(kube.WaitForDeploymentReady(ctx, kubeClient, originalConf.IstioNamespace, istioDeploymentName))
+		require.NoError(kube.WaitForDeploymentReady(ctx, kubeClient, config.IstioNamespaceDefault, istioDeploymentName))
 	})
 
 	// Expose the istiod /debug endpoints by adding a proxy to the pod.
 	log.Debugf("Patching istiod %s deployment with proxy", istioDeploymentName)
-	_, err = kubeClient.AppsV1().Deployments(originalConf.IstioNamespace).Patch(ctx, istioDeploymentName, types.StrategicMergePatchType, proxyPatch, metav1.PatchOptions{})
+	_, err = kubeClient.AppsV1().Deployments(config.IstioNamespaceDefault).Patch(ctx, istioDeploymentName, types.StrategicMergePatchType, proxyPatch, metav1.PatchOptions{})
 	require.NoError(err)
 	log.Debug("Successfully patched istiod deployment with proxy")
 
 	// Then create a service for the proxy/debug endpoint.
-	require.True(utils.ApplyFile(assetsFolder+"/remote-istiod/istiod-debug-service.yaml", originalConf.IstioNamespace), "Could not create istiod debug service")
+	require.True(utils.ApplyFile(assetsFolder+"/remote-istiod/istiod-debug-service.yaml", config.IstioNamespaceDefault), "Could not create istiod debug service")
 
 	// Now patch kiali to use that remote endpoint.
 	log.Debug("Patching kiali to use remote istiod")