Designing Istio for the Enterprise: One Architecture at Five Resolutions
A multi-cloud service mesh is not a diagram — it is a stack of decisions, and the expensive ones are made long before anyone writes YAML. This is a working design document you can step through: one architecture held fixed and redrawn five times, from the constraints it has to survive, through the bar it is graded against, down to the replica counts an SRE stands up, with the argument behind every element on screen.
Scope. A service mesh for a large Kubernetes estate — two or more clouds, five or more clusters, workloads that cross trust and regulatory boundaries. Verified against the Istio 1.24 / 1.25 track; ambient mesh and the Kubernetes Gateway API both move between minors, so re-check version-sensitive decisions at each upgrade. Physical values (CIDRs, replica counts, circuit sizing) are illustrative placeholders, not observed values — see the footer.
Service-mesh designs tend to fail the same way. Someone draws a topology, everyone nods, and eighteen months later the estate is wedged behind a choice nobody remembers making. The trust domain is wrong and every identity would have to be re-issued to fix it. The network model assumed a flat, non-overlapping pod address space that was never going to arrive. Config was never scoped, and istiod is now the least reliable component in the fleet.
None of those are implementation mistakes. They are decisions that got made implicitly, by a picture, instead of explicitly, by an argument. A diagram is very good at showing what you built and very bad at showing what you rejected — and in mesh design, the rejected option is usually the whole point.
So this design is drawn five times. Three of those passes — Environment, Logical, Physical — share one topology canvas: the same nine blocks in the same nine positions, gaining resolution as a vague constraint becomes a named component and then a sized, placed instance. Every step you land on carries the decision that put the element there: the options genuinely considered, the recommendation, the reason, and the cost you are accepting. Fifty-one of them, traced back to a 75-decision register.
Two passes deliberately break that constancy, and it is worth saying why. Requirements is a board, not a topology — a bar has no deployment shape. And the Conceptual pass has its own decomposition for a sharper reason. A capability model that inherits the deployment topology is not a conceptual design — it is a logical design with the product names filed off. The test is whether the picture survives a change of technology: if swapping Istio for a mesh that enforces in eBPF at the node would redraw it, then "workload proxy" was never a capability, it was Envoy wearing a generic label. So tab 2 has its own decomposition — capabilities layered by dependency, no locations, no counts — and a mapping table below carries you from there into the named components. The mapping is many-to-many, which is exactly why it cannot be the same picture.
The verdict
One mesh where it buys shared identity; a separate federated mesh only at a hard boundary. Prod versus non-prod, and each strong regulatory zone. Splitting further throws away the reason you adopted a mesh.
Multi-primary control planes — one istiod per cluster. No cluster sits on another cluster's config path, so losing one degrades one.
Multi-network, with an east-west gateway per network. Flat cross-cloud pod routing is a multi-year IPAM programme that is not coming. Bridging costs one hop and is true on day one.
One trust domain per mesh, under the enterprise root, with a per-cluster intermediate. Cross-cluster identity with local issuance. This one is permanent.
Config scope is a day-one requirement, not an optimisation. Discovery selectors, Sidecar and exportTo are cheap at five clusters and near-impossible to retrofit at fifty.
Revisioned canary upgrades, promoted per namespace. Blast radius is one namespace and rollback is a label change — the only version of this that stays safe at fleet scale.
How to read the five resolutions
1 · EnvironmentThe world the design has to survive: who calls in, where it runs, what the network actually is, and the numbers it is measured against. No solution on the canvas.
2 · RequirementsThe bar the design is graded against, stated so each one can be verified — plus the constraints it assumes rather than delivers.
3 · ConceptualA capability model with its own shape — layered by dependency, no locations, no products, no counts. The one tab that is not the topology.
4 · LogicalNamed components, protocols and the flows between them. Real products — but no instance counts, subnets or regions yet.
5 · PhysicalWhat gets stood up: replicas, sizing, load-balancer types, subnets, ports and circuits. The values an IaC module encodes.
Watch block 5, the connectivity box in the middle of the canvas, across the three topology tabs — “pod CIDRs are not routable between clouds” becomes “bridged, not flat”, becomes “mTLS end to end, SNI-routed at the gateway”, becomes “2×10G Direct Connect, :15443 to the peer gateway and :443 to the peer API server”. Same block, same position, three depths. Use ← → to step.
51 stepped decisionsfrom a 75-decision registerIstio 1.24 / 1.25 track← → to step
Resolution
—
Flows traffic & connectivity control & config identity & trust telemetry structural, not configuredBorders a real container the mesh, or outside it
Every box in istio-system is one Deployment per cluster, and the ×3 is replicas across availability zones — not three gateways. A cluster has one east-west gateway and one istiod, each running three pods. Ingress is the exception: there is one ingress gateway per exposure tier — public, internal and partner — each its own Deployment with its own load balancer, certificates and WAF posture. Still not per namespace and not per application: an application attaches its Gateway or VirtualService configuration to the tier that matches how it is exposed, and inherits that tier's posture rather than running a gateway of its own. The east-west gateway is per cluster but used only when the destination is on another network — and it is the destination's gateway that ends up in the path, not the caller's, because the sidecar dials the target network's entry point directly. Clusters that share a VPC share a network label, so calls between them go direct and never touch a gateway at all. One cluster, with the rest of the estate removed. Two namespaces are meshed and one is not — that is the opt-in model, and it is the single most useful thing to understand about running Istio: the injector webhook selects namespaces by revision label, so nothing is meshed until someone asks. Note also what the mesh does not replace (kube-proxy still does Service VIPs), and that all three gateways are ordinary Deployments in istio-system rather than anything special. What fronts each of them is where the differences actually live. Every gateway is the same thing — a Deployment and a Service — and all that changes is the Service type and the load-balancer scheme. The public and partner tiers get an internet-facing L4 load balancer; the internal tier gets an internal one, reached from the corporate network over Direct Connect or VPN rather than from the internet. The east-west gateway also gets an internal one on :15443, because its callers are peer clusters and never the public. All of them pass TLS through rather than terminating it, which is why the answer is an NLB and not an ALB: the design terminates at Envoy so the gateway can see the client certificate and make the first authorization decision, and an application load balancer that decrypted first would take both away. Where partners are themselves on the same cloud, the same NLB can be published as a PrivateLink endpoint service instead, which removes the internet exposure entirely. There is no egress gateway here, and that is a decision rather than an omission. Istio's documentation states that it cannot securely enforce that egress traffic passes through one — a workload that bypasses its sidecar just leaves — and that something outside the mesh must enforce it. That something is a firewall and a single NAT route, which has to exist either way; adding a gateway on top of it buys a second policy surface, an extra hop and an extra layer of internal mTLS for an outcome the network already delivers. What the mesh keeps is the half only the mesh can do: REGISTRY_ONLY plus a declared ServiceEntry per dependency, so undeclared calls fail closed and every external dependency is a reviewable object. Attribution is not lost either — each sidecar logs its own outbound calls under its own identity, which is the same data, collected from many places instead of one.
Two clusters running the same namespace and the same service name are not two services — they are one, with merged endpoints. That is the defining semantic of multi-cluster, and it is what makes failover work at all: a caller asks for payments and gets every healthy endpoint in the mesh, wherever it runs. It is also the sharpest footgun in the model. Two teams that independently create default/api in different clusters have not created two services; they have silently joined one, and traffic will land on both. Namespace sameness is a naming contract before it is a routing feature. The boundary that decides whether a gateway is involved is the network, not the cloud. Clusters 1 and 2 share a VPC and therefore a topology.istio.io/network value, so istiod hands out pod IPs and the call is ordinary routed traffic. Cluster 3 sits in a second VPC in the same region and the same account — a different network — so istiod substitutes that network's east-west gateway address and the sidecar dials :15443 with the destination in SNI. Note which gateway is in the path: the destination's. The caller does not egress through its own; it dials the target network's entry point directly, so there is one gateway rather than one at each end. Every cluster runs one anyway, because every cluster is somebody else's destination. Locality-preferred routing chooses between the two. Cluster 2's endpoints are local and cheap, so they win while they are healthy; outlier detection ejects them when they are not, and the same call starts taking the gateway hop with no config change and no deploy. Discovery is drawn deliberately across the VPC boundary. istiod in cluster 1 watches cluster 3's Kubernetes API server on :443 exactly as it watches cluster 2's — the network split changes the address it hands back, never whether it knows the endpoint exists.
Discovery is a control-plane relationship; reachability is a data-plane one. They are drawn apart because they fail apart. A cluster learns its peers through a remote secret — a Secret in istio-system holding a kubeconfig for one peer's Kubernetes API server, created by istioctl create-remote-secret and applied like any other object. istiod reads it and watches that API server on :443, merging the peer's Services and EndpointSlices into the registry it already keeps for its own cluster. Two peers are drawn on purpose: the second cloud and an on-premises OpenShift cluster. Neither is a special case — on-prem is another network reached by the same gateway, discovered by the same secret, which is what makes phase four of the rollout an extension rather than a second architecture. Two consequences fall out of the shape. Control planes never talk to each other, so there is no istiod-to-istiod link to firewall, debug or version-match, and a sidecar is only ever configured by the istiod in its own cluster. And every control plane holds a credential for and a watch on every peer, so the relationships grow as N×(N−1): six at three clusters, thirty at six, 2,450 at the fifty in the scale requirement. That is a rotation and audit surface, not just a number. When a watch does break, the endpoints it fed go stale and keep being used — the mesh degrades toward a wrong-but-working picture rather than stopping, which is the trade multi-primary is bought for. Reachability begins before any of that, at the name. A caller resolving payments.payments.svc.cluster.local gets an answer only if that Service exists in its own cluster or DNS proxying answers for it; without one of those the call fails at resolution, with nothing in the mesh telemetry to explain it because Envoy never saw a connection. Only once a name resolves does the network label decide the address istiod hands back: a pod IP inside the same network, that network's east-west gateway outside it.
functional — what it must do non-functional — the bar, and how it is verified constraint — given, not chosen
Every requirement states its own test. A bar with no way to verify it is a preference, and preferences do not survive a delivery argument. The constraints along the bottom are listed with an owning team on purpose — a dependency with no owner is an assumption in disguise, and it is the failure mode that shows up in month five rather than week one.
governance & boundary mediation control & convergence per-call enforcement identity & trust assurance depends on / delivers to
This tab is not the topology. A capability model has its own decomposition: capabilities layered by dependency, with no locations, no products and no counts. Governance decides what may be declared; control delivers that intent to the enforcement points; enforcement applies it on every call; and all of it is decided against a foundation of workload identity. Swap Istio for another mesh and this picture should barely change — that is the test it exists to pass. The mapping into named components is in the table below.
structural path (exists regardless of config) control & config data plane connectivity & gateways identity & trust telemetry
The dashed teal envelope is the prod mesh — one of three. It is a single trust domain spanning both estates, which is why a workload in either can prove who it is to the other. Two things make that envelope real rather than decorative: every workload inside it chains to one root CA, and every control plane inside it holds a remote secret for every other, so they share one registry. Networks nest inside a mesh rather than the other way round — crossing a network costs a gateway hop, while crossing a mesh costs trust-bundle federation, which is why there are three of these and not thirty. The other two meshes (7 and 8) sit below it with their own trust domains: the regulated one federates with prod for three exported services, the non-prod one is isolated on purpose. The envelope carries no number because it is not a component — it is the boundary the numbered components sit inside, and trust, config and consumers all arrive from outside it. It is a scope, not a perimeter. Nothing is deployed on that line and nothing is filtered at it; a workload inside the mesh can still reach anything the network permits. What the boundary decides is what comes for free: inside it, one workload can authenticate to another with no per-edge setup, a service name means one service wherever it runs, and a policy naming a principal means the same thing in every cluster. It also decides blast radius — one policy vocabulary is also one place a mistake reaches — which is the actual reason non-prod is a separate mesh rather than a namespace with rules on it. Domains A1, A4, E2.
Consumers & the ingress edge — who arrives from outside, and the one sanctioned way in and out. Domains D, E8.
Root of trust — where workload identity is anchored and how far it reaches. Domain E; one-way doors.
Config governance — how a change to the mesh gets reviewed, promoted, and reverted. Domains F, J, M.
Estate A — a cloud fleet: its control plane, data plane, gateways and local trust anchor. Domains A, B, C.
Cross-location connectivity — what carries service-to-service traffic between estates. Domains A3, D.
Estate B — the second cloud fleet, deliberately drawn identically. Domains A, B, C, K.
Non-prod mesh — its own trust domain, and deliberately not federated to prod. Domains A1, A4.
Regulated mesh — its own trust domain, joined to prod by trust-bundle exchange for three exported services. Domains A1, A4, E9.
Observability — the signals every estate emits and where they land. Domain H.
Scale & cost envelope — the read-out the design is measured against, not a component. Domains I, L.
Capability → component: how the conceptual tab lands
Because tab 2 has its own decomposition, it needs an explicit bridge into the named components. Note how little of this is one-to-one: a single capability lands in several components, and one component — the gateway — carries three different capabilities at once. That many-to-many mapping is the reason a capability model cannot just be the topology with the labels changed.
Capability (tab 2)
Resolves to (tab 3)
Stood up as (tab 4)
Workload identity
SPIFFE IDs, signed locally by each cluster's istiod
24h certificates, rotated at half-life without a restart
Trust anchoring & federation
enterprise root → per-cluster intermediate; trust-bundle exchange across meshes
offline HSM root, Vault-issued 365d intermediates delivered as cacerts
Authenticated transport
mTLS, PeerAuthentication STRICT
permissive → strict per namespace, port exceptions catalogued
Authorization
AuthorizationPolicy, default-deny
audit mode first, baseline allows generated from observed callers
platform defaults per workload class; canary weights
Policy distribution & convergence
istiod serving xDS
istiod ×3, sized from the watch set; push-latency SLO
Reachability & discovery
remote secrets, network labels, service entries
cross-cluster endpoint discovery over the interconnect
North-south ingress
one ingress gateway per exposure tier (public / internal / partner), authored through the Kubernetes Gateway API
a Deployment per tier, ×3 across AZs, each behind its own L4 NLB; TLS 1.3 terminates at the gateway, not at the LB
Boundary mediation
ingress and east-west gateways, plus a declared egress posture — one capability, two deployments and a firewall
ingress tiers behind L4 NLBs, internet-facing for public and partner and internal for corp; east-west Envoy ×3 behind an internal NLB on :15443; outbound declared as ServiceEntry under REGISTRY_ONLY and enforced by a VPC egress firewall onto one NAT address per AZ — no Istio egress gateway
Segmentation & isolation
a separate federated mesh per risk tier
PCI mesh on its own clusters, own root, own Argo project
Config governance
GitOps repo + admission policy + the three scoping levels
Argo CD app-of-apps, OPA/Kyverno gates
Tenancy & ownership
namespace as tenant, RBAC split, ownership labels
one Argo project per mesh; CODEOWNERS on mesh paths
Lifecycle
revisions and revision tags
canary promotion per namespace; rollback is a label change
~0.7 vCPU + 1Gi per 10 meshed pods, transfer attributed per tenant
The spine: one concern, five resolutions
Progressive resolution only works if you can follow a single concern all the way down. Cross-cluster connectivity is the clearest thread — note that it is not a block at all in two of the five resolutions, and at the conceptual layer it becomes two capabilities, which is the mapping above doing its job:
Resolution
What block 5 says
The decision captured
Environment
Workloads span two clouds and on-prem; VPCs are separate and CIDRs may overlap. Cross-cloud L3 is a network-team dependency with its own latency and egress bill.
Assumption: a non-flat network is a given, not a choice. Flat cross-cloud pod routing is treated as unavailable.
Requirements
Not a block at all — a stated bar: a workload must be able to call a peer in another location, within the per-hop latency budget, with cross-location transfer attributed to whoever caused it.
What the bar is, and how it is checked → verified by a cross-location failover test and a per-hop latency budget, not by assertion.
Conceptual
Two capabilities, not one block: reachability & discovery (a workload can reach a peer wherever it runs) and boundary mediation (anything crossing passes one control point). Neither mentions a location.
Flat address space vs separate spaces bridged → separate, bridged at a mediated boundary. ⚠ one-way
Logical
East-west gateway per network doing SNI passthrough, plus cross-cluster endpoint discovery and topology.istio.io/network labels.
How traffic crosses a boundary → per-network gateway, mTLS never terminated at the hop; endpoint discovery via remote secrets.
Physical
istio-eastwestgateway ×3 across zones behind an internal L4 load balancer on 10.20.0.0/24, reaching the peer over 2×10G Direct Connect; only :15443 to the peer gateway and :443 to the peer cluster's API server are open.
Sizing, placement and the port contract → three replicas, private subnet, narrow firewall contract, transfer metered per tenant.
Every major concern in the design should thread like that. If one only appears in a single tab, either it is genuinely scoped to that depth or a resolution is incomplete.
⚠ The five one-way doors
Who owns the root of trust — a self-signed mesh root is a second, unaudited PKI, and it cannot be cross-signed later without re-issuing every identity. step E4
One mesh or many — the number of trust domains and control-plane fleets follows directly from this. step C1
Where control lives — multi-primary versus a shared control cluster decides whether a cluster loss is local or estate-wide. step C2
Trust-domain layout — the domain is baked into every SPIFFE ID; changing it later is an estate-wide re-issue. step C4
The CA chain and certificate lifetimes — issuance topology sits on the hot path of every pod start. steps L3, P7
Decision register — every step on this canvas
Step
Resolution
Decision
Recommendation
Source
Rollout — the order that keeps the doors open
The sequence matters as much as the design. The one-way doors are decided first, before anything is installed, because every later phase assumes them.
0
Foundationsweeks 0–4
Decide the one-way doors: mesh split, trust domains, CA topology, network model.
Stand up CA integration, the GitOps repo structure and the supported-platform matrix.
Build a staging mesh that mirrors the target topology.
1
Single-cluster pilotweeks 4–8
Install via Helm and GitOps with revisions in one non-prod cluster; mTLS permissive.
Enforce config scoping and the injection strategy from the first namespace.
Onboard one or two friendly teams; stand up observability.
2
Hardenweeks 8–14
Flip pilot namespaces to strict mTLS; roll default-deny authorization in audit, then enforce.
Add admission guardrails, the RBAC split, egress posture and the ingress pattern.
Define control-plane SLOs; rehearse rollback.
3
Multi-clusterweeks 14–22
Go multi-primary and multi-network with east-west gateways; wire remote secrets.
Enable cross-cluster discovery and locality failover; test cluster loss deliberately.
Roll to more production clusters in waves.
4
Multi-cloudweeks 22–30
Extend to the second cloud over the private interconnect; federate trust if the mesh is split.
Admit the on-prem / OpenShift estate as another network, on the same gateway-and-remote-secret pattern.
Validate cross-cloud mTLS, DNS, failover and data-transfer cost.
Stand up the regulated-zone mesh, with a FIPS build if in scope.
5
Scale & optimiseongoing
Pilot ambient for high-density namespaces; tune convergence against the SLOs.
Migrate north-south to Gateway API where ready; mature progressive delivery.
Continuous N-2 upgrades; quarterly DR and chaos game-days.
Who owns what
The platform team runs the paved road; app teams drive on it. The split below is what makes the guardrails enforceable rather than advisory — R/A is responsible and accountable, C is consulted.
Activity
Mesh platform
App teams
Security
Network
Control plane run & upgrade
R/A
I
C
I
Gateways & the shared ingress edge
R/A
C
C
C
Namespaced routing config
C
R/A
I
I
Namespaced authorization policy
C
R/A
C
I
Mesh-wide policy & defaults
R/A
I
C
I
CA / PKI & key custody
C
I
R/A
I
Trust domain & federation
R
I
A
I
Cross-cloud interconnect
C
I
C
R/A
Admission guardrails
R/A
I
C
I
Observability platform
R/A
C
I
I
What is most likely to go wrong
Risk
Impact
Mitigation
Unscoped config overwhelms the control plane at scale
High
Enforce all three scoping levels through admission policy from day one; watch push latency as an SLO.
Wrong trust domain or CA topology chosen
High
Treat as one-way doors decided in phase 0 with security in the room; back up the root.
Strict mTLS rollout breaks non-meshed callers
Med
Staged permissive → strict per namespace, with port exceptions catalogued before the flip.
Default-deny authorization causes a mass outage
High
Roll out in audit mode first; generate the baseline allow list from observed caller identity.
Multi-primary remote-secret fan-out at fleet scale — every control plane watches every peer, N×(N−1)
High
Cap clusters per mesh and shard by tier or region rather than growing one mesh; use a shared control location for small edge clusters; alert on watch count and push latency per control plane.
Observability cost explosion from cardinality
Med
Telemetry pruning, sampling and retention tiers designed in, not added after the first bill.