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.11).
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)). Without a retries block: 2
retries on connect-failure, refused-stream,
unavailable, cancelled (no longer on a
plain 503 since 1.24); attempts: 0 disables them.
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 marks requests for an audit plugin. 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.
Ambient: without targetRefs (selector
or namespace-wide), ztunnel enforces it, L4 only; L7 rules need a
waypoint and targetRefs. ztunnel skips L7 attributes:
ALLOW with them denies, DENY with only them blocks
nothing. Behind a waypoint, the destination ztunnel
sees the waypoint’s identity.
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.
Ambient: attach both to a waypoint via
targetRefs; ztunnel ignores this L7-only DENY, so
without a waypoint it blocks nothing.
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). Ambient: L7 metrics
come only from the waypoint (reporter="waypoint",
aggregate on that); without one, only istio_tcp_*.
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).
Not in ambient: EnvoyFilter on waypoints, the
Sidecar resource, PeerAuthentication
DISABLE (ignored). VirtualService on waypoints is alpha:
use HTTPRoute, never mix both.
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 waits MINIMUM_DRAIN_DURATION (default
5 s), then polls active Envoy connections and stops the
proxy as soon as they reach 0 instead of on a fixed
timer; terminationDrainDuration no longer applies.
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 lists every proxy with its
istiod and version; since 1.27 the sync state per xDS type only shows
with -v 1 (or -o json). 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 -v 1 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> # Ambient (also: policy, certificate) istioctl ztunnel-config workload istioctl waypoint status -n prod # 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.
UH (503): no healthy upstream host (all ejected by
outlier detection or none ready).
UT (504): upstream request timeout (route
timeout or perTryTimeout).
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.3
(21 Oct 2026), K8s 1.32–1.37.
1.30: latest patch 1.30.7 (21 Oct 2026), supported
until six weeks after 1.32 (1.32.0-rc.0 on 28 Oct 2026, 1.32.0 not
yet out).
1.29: EOL on 15 Oct 2026, latest
patch 1.29.10 (21 Oct 2026), an exceptional release after EOL for the
second bulletin below.
1.28: EOL since 1 Jul 2026.
Patch to 1.31.3 / 1.30.7: they carry two
bulletins. ISTIO-SECURITY-2026-007 (1.31.2 / 1.30.6), CVSS 9.2 since
26 Oct: Envoy Brotli decompressor overflow (CVE-2026-111497) where
Brotli decompression is enabled, e.g. via EnvoyFilter; also
AuthorizationPolicy DENY rules with
methods and ALLOW rules with
notMethods are bypassed over HTTP/2 by changing the case
of the method. ISTIO-SECURITY-2026-008 (eMudhra intermediate CAs,
CVE-2026-111943): the images no longer trust
emSign ECC Root CA - G3; istiod, istioctl and
pilot-agent (Go 1.26.10) and ztunnel (rustls-webpki 0.103.16) reject
chains with the compromised keys. 1.29.10 has the new CA bundle and
the ztunnel fix but no Go fix. Istio’s own mTLS certificates
are not affected. Affected is what trusts the public CA bundle, e.g.
a DR with tls.mode: SIMPLE and no
caCertificates or credentialName (egress to
external HTTPS), the jwksUri of a RequestAuthentication
(fetched by istiod) or XDS_ROOT_CA=SYSTEM. Until you
upgrade, set caCertificates (or caCrl) on
the DR.
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 (next scream tests 17 Nov and 8 Dec). 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.