Istio service mesh
on a single sheet.
A dense reference for senior platform engineers and SREs. Traffic management, security, observability, performance, multi-cluster and the failure modes you actually hit in production. No beginner slides.
Preview (3 pages, A4 landscape)



Download the PDF
Direct download, no email required. CC BY-SA 4.0, copying, printing and redistributing is explicitly allowed as long as the attribution stays visible.
What’s inside
Traffic
Gateway, VirtualService, DestinationRule, ServiceEntry, Sidecar scoping, with outlier detection and connection-pool defaults.
Security
PeerAuthentication, AuthorizationPolicy with JWT claims, RequestAuthentication, SPIFFE identity, migration pattern for mTLS STRICT.
Observability
Telemetry API, the metrics that matter from the Envoy stack, tracing header propagation, OTel logging with filters.
Performance
Sidecar sizing from the Istio benchmark, Pilot tuning for push storms, ambient mode (GA since 1.24) as the sidecar-less option.
Multi-cluster
Primary-remote, multi-primary, external control plane. Trust-domain setup and endpoint discovery in one block.
Diagnostics
istioctl tools in the order you reach for them during an incident. Typical UF/UC/NR/UO response flags with root cause.
Full-text cheatsheet
The same content as the PDF, to read along, search and copy the YAML snippets directly. As of: Istio 1.31 (v1.31 · 2026.10).
Architecture
Datapath & components
N-S: Client → ingress GW → sidecar(dst) → app. E-W: App → sidecar(src) → sidecar(dst) → app, two Envoy hops (ambient: ztunnel → ztunnel, +waypoint for L7). mTLS handshake only on connect.
istiod: Pilot/Citadel/Galley combined, xDS push + CA. Data plane: Envoy sidecar (mutating webhook) or ambient ztunnel (Rust, L4) + optional waypoint (Envoy, L7). Gateway: dedicated Envoys, Deployment + LB service.
Envoy version = Istio version + 8. Istio 1.31 → Envoy 1.39.
Install profiles
default (prod), minimal (istiod only),
ambient (sidecar-less), remote
(MC workload cluster), demo (noisy). Canary via
--revision=1-31-1.
istioctl x precheck istioctl install --set profile=default \ --set hub=registry.example.com/istio # mirror
Traffic Management
Gateway
Binds ports/hosts/TLS at the edge proxy. servers[].port
+ tls.mode: SIMPLE, MUTUAL,
PASSTHROUGH, ISTIO_MUTUAL.
credentialName points to a Secret in the same namespace
as the gateway pod.
apiVersion: networking.istio.io/v1
kind: Gateway
metadata: {name: web-gw, namespace: istio-system}
spec:
selector: {istio: ingressgateway}
servers:
- port: {number: 443, name: https, protocol: HTTPS}
hosts: ["www.istio-quickref.de"]
tls:
mode: SIMPLE
credentialName: web-cert
VirtualService
Routing rules. Binds hosts to gateways
(or mesh for east-west). Match order is significant,
the first matching rule wins. Header names must be lowercase;
exact/prefix matches on header values and
URI are case-sensitive (URI: ignoreUriCase: true,
regex: (?i)).
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata: {name: reviews}
spec:
hosts: [reviews]
http:
- match:
- headers: {end-user: {exact: jason}}
route:
- destination: {host: reviews, subset: v2}
- route:
- destination: {host: reviews, subset: v1}
weight: 90
- destination: {host: reviews, subset: v3}
weight: 10
retries:
attempts: 3
perTryTimeout: 2s
retryOn: gateway-error,connect-failure,refused-stream
timeout: 10s
DestinationRule
Subsets (versions) + traffic policy (LB, connection pool,
outlier detection, TLS). Takes effect only after VirtualService
routing. host: FQDN or short name in the DR’s
namespace. 1.31+: meshConfig.defaultTrafficPolicy sets a
mesh-wide connectionPool/outlierDetection
baseline; a DR block overrides it per block.
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata: {name: reviews}
spec:
host: reviews
trafficPolicy:
connectionPool:
tcp: {maxConnections: 100}
http:
http1MaxPendingRequests: 64
http2MaxRequests: 1000
maxRequestsPerConnection: 10
outlierDetection:
consecutive5xxErrors: 5
interval: 30s
baseEjectionTime: 60s
loadBalancer:
consistentHash:
httpHeaderName: x-user-id
subsets:
- name: v1
labels: {version: v1}
- name: v2
labels: {version: v2}
ServiceEntry
Pull external hosts into the mesh registry (DB, SaaS, REST APIs).
MESH_EXTERNAL + DNS resolution is the clean
default combo. With outboundTrafficPolicy: REGISTRY_ONLY,
egress without a ServiceEntry lands in BlackHoleCluster.
1.31: mode ALLOW_ANY_DYNAMIC_DNS (MeshConfig only, not
settable per Sidecar resource; sidecar proxies only) resolves unknown
plaintext-HTTP hosts via Envoy DFP; TLS/TCP stays
PassthroughCluster.
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata: {name: stripe-api}
spec:
hosts: ["api.stripe.com"]
ports:
- {number: 443, name: https, protocol: HTTPS}
resolution: DNS
location: MESH_EXTERNAL
Sidecar
Limits what a workload sees of the mesh registry,
critical for memory footprint and push latency in
large clusters. Default: every sidecar gets config for all services.
1.31+: ~ns/* subtracts from the import set
(*/* + ~legacy/*).
apiVersion: networking.istio.io/v1
kind: Sidecar
metadata: {name: default, namespace: prod}
spec:
egress:
- hosts:
- "./*" # own namespace only
- "istio-system/*"
- "shared/*"
Security
PeerAuthentication
mTLS mode between sidecars. STRICT,
PERMISSIVE, DISABLE. Scope: mesh (in
istio-system), namespace, or workload via
selector. Migrating to STRICT: first
mesh-wide PERMISSIVE, then switch namespace by namespace.
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata: {name: default, namespace: prod}
spec:
mtls: {mode: STRICT}
---
# Port exception (spec only, own name): NLB check
spec:
selector: {matchLabels: {app: legacy}}
mtls: {mode: STRICT}
portLevelMtls:
8080: {mode: PERMISSIVE}
AuthorizationPolicy
L7 access control. Order: CUSTOM (ext authz) →
DENY → ALLOW (default action);
AUDIT only logs. No ALLOW policy on a workload = allow
all. spec: {} (ALLOW, no rules) denies everything;
rules: [{}] matches every request. Rules OR, fields in a
rule AND. 1.31+: source.trustDomains.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata: {name: reviews-allow, namespace: prod}
spec:
selector: {matchLabels: {app: reviews}}
action: ALLOW
rules:
- from:
- source:
principals:
- cluster.local/ns/prod/sa/productpage
to:
- operation:
methods: [GET]
paths: ["/reviews/*"]
when:
- key: request.auth.claims[groups]
values: ["reader", "admin"]
RequestAuthentication (JWT)
Validates the JWT and fills request.auth.* for the
AuthorizationPolicy. Important: without an
additional DENY policy with notRequestPrincipals=["*"],
unauthenticated requests can still get through.
apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata: {name: jwt, namespace: prod}
spec:
selector: {matchLabels: {app: api}}
jwtRules:
- issuer: "https://auth.example.com"
jwksUri: "https://auth.example.com/jwks"
audiences: ["api.example.com"]
forwardOriginalToken: true
Identity
SPIFFE URI spiffe://<trust>/ns/<ns>/sa/<sa>.
Trust domain defaults to cluster.local; all clusters of
one mesh share it (or list each other in
trustDomainAliases), only the cluster name is unique.
Workload certs: 24 h TTL (SECRET_TTL), istio-agent
renews at half-life (SECRET_GRACE_PERIOD_RATIO=0.5),
issued by the istiod CA.
Observability
Telemetry API
Metrics/logs/traces per workload or namespace. Replaces
EnvoyFilter telemetry mods and the old
values.telemetry.v2.* settings.
apiVersion: telemetry.istio.io/v1
kind: Telemetry
metadata: {name: trace-prod, namespace: prod}
spec:
tracing:
- providers: [{name: tempo}]
randomSamplingPercentage: 5.0
metrics:
- providers: [{name: prometheus}]
overrides:
- match: {metric: REQUEST_COUNT}
tagOverrides:
request_protocol: {operation: REMOVE}
accessLogging:
- providers: [{name: otel}]
filter:
expression: "response.code >= 400"
Metrics & tracing
RED counter: istio_requests_total, latency:
istio_request_duration_milliseconds_bucket. The
reporter label = source/destination, in
dashboards always aggregate on destination
(otherwise double counting).
Envoy creates spans and trace headers but cannot
link an inbound request to the app’s outbound calls. The app
must copy incoming trace headers (x-request-id,
x-b3-*, traceparent) onto its outgoing
requests.
Performance & Tuning
Sidecar sizing
Default requests 100m / 128Mi, limits 2 CPU / 1Gi: not prod-grade at high RPS.
Istio benchmark (1.24, 1000 RPS, 1 KB, 2 workers): sidecar ≈ 0.20 vCPU + 60 MB, waypoint ≈ 0.25 vCPU + 60 MB, ztunnel ≈ 0.06 vCPU + 12 MB.
Memory scales with the config the proxy holds:
Sidecar/exportTo trim it.
Pilot tuning
PILOT_PUSH_THROTTLE: default 0 = auto,
min(15 + 5 × GOMAXPROCS, 100).
PILOT_DEBOUNCE_AFTER: default 100 ms.
PILOT_DEBOUNCE_MAX: default 10 s. On push storms
(many pod restarts) raise debounce; for faster convergence raise
throttle together with istiod CPU.
Ambient mode (GA since 1.24)
No sidecar injection. ztunnel:
node DaemonSet, L4 mTLS (HBONE). Waypoint: optional
Envoy for L7, per namespace, service or pod
(istio.io/use-waypoint). Opt-in: label
istio.io/dataplane-mode=ambient (namespace or pod).
1.31: weighted waypoint canaries
(istio.io/use-waypoint-canary +
...-canary-weight).
Saves RAM at high pod counts, costs complexity when debugging.
Jobs & CronJobs (sidecar lifecycle)
App before proxy ready → connection refused. App done, sidecar still running → the job hangs.
Native sidecar (K8s 1.33+ GA): proxy as an
initContainer (restartPolicy: Always),
proper lifecycle. Default since Istio 1.27:
ENABLE_NATIVE_SIDECARS=auto, native only when every node
runs kubelet ≥ 1.33 (mixed clusters during a node upgrade get the
classic sidecar).
Per pod (1.24+, beats the mesh setting):
sidecar.istio.io/nativeSidecar: "false" forces the
classic sidecar, "true" forces native.
Pre-native:
holdApplicationUntilProxyStarts against the start race,
trap with POST :15020/quitquitquit
on EXIT against shutdown hangs (fires on
crash/signal too, not only on success).
# Pre-native fallback (without native sidecars):
metadata:
annotations:
proxy.istio.io/config: | # start race
{ "holdApplicationUntilProxyStarts": true }
spec:
containers:
- name: worker
command: ["/bin/sh","-c"]
args:
- |
trap 'curl -fsS -XPOST localhost:15020/quitquitquit||true' EXIT
./run-task
Graceful drain: EXIT_ON_ZERO_ACTIVE_CONNECTIONS
Classic sidecar: on SIGTERM the sidecar only drains for
terminationDrainDuration (default 5 s), then
hard-exits, long-lived connections (gRPC streams, WebSockets,
DB pools) are cut mid-flight (503/reset on rollout/scale-down).
Fix:
EXIT_ON_ZERO_ACTIVE_CONNECTIONS=true (via
proxyMetadata or proxy.istio.io/config):
pilot-agent polls active Envoy connections and stops the
proxy as soon as they reach 0 instead of on a fixed
timer.
Caveat: if a connection hangs (client never
closes), the proxy blocks until
terminationGracePeriodSeconds → SIGKILL. Raise the
grace period accordingly. Mesh-wide via
meshConfig.defaultConfig.proxyMetadata.
Native sidecar (default since 1.27): kubelet stops the proxy only after the app has exited, so the race mostly applies to the classic mode.
Multi-Cluster
Topologies & setup
Primary-remote: one istiod, several workload clusters. Multi-primary: istiod per cluster, shared root CA. External control plane: istiod on the outside.
Required: common root CA, shared
trustDomain (or trustDomainAliases), unique
cluster name, endpoint discovery via
istioctl create-remote-secret. Multi-network only: label
topology.istio.io/network on
istio-system.
istioctl create-remote-secret \ --context=cluster-b \ --name=cluster-b \ | kubectl apply --context=cluster-a -f -
Diagnostics
First istioctl tool during an incident
istioctl proxy-status shows the sync state of every
sidecar. SYNCED = Envoy ACKed the last push,
NOT SENT = istiod has nothing to send (often normal,
e.g. no routes), STALE = sent but not ACKed →
network istiod↔proxy or an Istio bug: check the istiod logs.
istioctl proxy-status istioctl proxy-config routes <pod>.<ns> -o json istioctl proxy-config clusters <pod>.<ns> istioctl proxy-config listeners <pod>.<ns> istioctl proxy-config endpoints <pod>.<ns> istioctl proxy-config secrets <pod>.<ns> # Static lint: VirtualService/DR conflicts etc. istioctl analyze -n prod # Bug report (not anonymised; secrets only w/ --full-secrets) istioctl bug-report
Live log of a sidecar
istioctl proxy-config log <pod> --level debug
sets the log level live without a pod restart. Component-specific,
e.g. --level rbac:debug,jwt:debug. After
diagnosis, set it back to warning.
Common failure modes
UF (503): upstream connection failure: wrong
targetPort, or mTLS conflict (DR tls.mode
DISABLE vs. STRICT server).
UC (503): upstream terminated the connection:
keep-alive/idle-timeout race, or pod-IP call without a route at the
server sidecar.
NR: no route configured: VirtualService host/match
order or DR subset (404 when no path matches).
UO (503): circuit breaker,
connectionPool limits of the DR.
OUTPUT_CERTS scraping: read: connection reset by peer
App-originated mTLS (Prometheus/Alloy with
proxyMetadata.OUTPUT_CERTS) gets an RST although the
target is PERMISSIVE.
Cause: the client sidecar wraps the app mTLS in a
second ISTIO_MUTUAL tunnel, typically via a DR
host: "*.local" + exportTo: ["*"] (STRICT
migration helper) that also matches direct pod-IP calls.
15006 strips only the outer layer, the app port receives
TLS bytes. PERMISSIVE only decides inbound.
Fix: excludeOutboundPorts (or
includeOutboundIPRanges: "") on the scraper pod, or a
more specific DR with tls.mode: DISABLE for the target.
1.31+ (opt-in): ENVOY_SECURE_METRICS_PORT /
ENVOY_SECURE_MERGED_METRICS_PORT add mTLS scrape
listeners.
# client wraps? sni=outbound_* + alpn istio* = proof
istioctl pc cluster <pod>.<ns> --fqdn <target> -o json \
| jq '.[].transportSocket.typedConfig
|{sni, alpn: .commonTlsContext.alpnProtocols}'
Gateway API (v1, kubernetes-sigs)
Status in 1.31
Istio 1.31 builds on Gateway API v1.6.0 (Gateway,
HTTPRoute, GRPCRoute, TLSRoute,
TCPRoute, ListenerSet), an equal
alternative to Gateway/VirtualService. CRDs
are not bundled: 1.31 expects v1.6.0 (TCPRoute minimum
v1.6.0, TLSRoute v1.5.0); CRDs below their minimum are
silently not processed, istioctl analyze reports IST0176. New
projects: Gateway API; networking.istio.io stays, both
coexist.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata: {name: web, namespace: istio-system}
spec:
gatewayClassName: istio
listeners:
- name: https
hostname: www.istio-quickref.de
port: 443
protocol: HTTPS
allowedRoutes: {namespaces: {from: All}} # prod: Selector
tls:
certificateRefs:
- {name: web-cert}
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: {name: site, namespace: web}
spec:
parentRefs: [{name: web, namespace: istio-system}]
hostnames: ["www.istio-quickref.de"]
rules:
- matches: [{path: {type: PathPrefix, value: /}}]
backendRefs: [{name: nginx, port: 80}]
Release & Support
Cadence & Support Window
Roughly one minor per quarter. A minor is supported until six weeks after the N+2 release, then no more security or critical-bug back-ports.
1.31: released 31 Aug 2026, latest patch 1.31.1
(21 Sep 2026), K8s 1.32–1.37.
1.30: latest patch 1.30.5 (21 Sep 2026), supported
until six weeks after 1.32.
1.29: EOL on 12 Oct 2026 (latest
1.29.8).
1.28: EOL since 28 Jun 2026.
Artifacts from 1.31: images only on
docker.io/istio, charts on blob.istio.io
and ghcr.io/istio/release/charts;
gcr.io/istio-release and registry.istio.io
go dark in Dec 2026 (scream tests 13 Oct, 17 Nov). 1.31.1+ signed
with istio-key-v2.pub. 1.30 defaults to
registry.istio.io/release: set
hub=docker.io/istio (or a mirror) even without
upgrading.
In-place upgrades one minor at a time; canary (revision) upgrades in sidecar mode may skip one (N→N+2); ambient (ztunnel/CNI tolerate only N+1) goes step by step. More than two minors behind means unpatched CVEs.
Anti-Patterns
What you should not do
Default sidecar resources in prod: 1Gi limit OOMs
in large meshes without Sidecar scoping, 100m CPU
request under-reserves at high RPS.
Mesh VirtualService without a Sidecar resource:
every sidecar gets every rule, push storm.
STRICT without migration:
non-injected workloads break (jobs, external health checks).
EnvoyFilter as the default tool: reach for the
Telemetry API, AuthorizationPolicy, TrafficExtension
(1.30+, v1alpha1; Wasm + Lua) or WasmPlugin first.
EnvoyFilter breaks between minors.
Multi-cluster without a shared root CA: mTLS fails.
License & redistribution
Not allowed: using the logo or trademarks, or creating the impression that the content originates from you or that OMNI52 GmbH sponsors the reuse.
Full license text: creativecommons.org/licenses/by-sa/4.0/.
Also from OMNI52:
istio-cheatsheet.de, Istio in depth (German)
service-mesh-cheatsheet.de, Istio + Linkerd + Cilium compared
cilium-cheatsheet.de, eBPF networking
kubernetes-cheatsheet.de, Kubernetes Core
Istio is a trademark of The Linux Foundation. OMNI52™ is a trademark of OMNI52 GmbH (filed, not yet registered). This website is operated by OMNI52 GmbH and is not affiliated with, endorsed by, or sponsored by The Linux Foundation, the CNCF, or the Istio project. “Istio” is used in a nominative/descriptive sense to indicate the technology this reference describes.