Service Mesh Istio · OSSM · Cloud Service Mesh
Quiz 0/0

Service Mesh, from zero to production

One page, four tracks: a course that builds Istio concepts step by step (networking basics included), an operational cheatsheet, and the two platform flavours you actually run — OpenShift Service Mesh (2.6 Maistra and 3.x Sail) and Google Cloud Service Mesh on GKE and on GDC for VMware.

APIs: networking/security/telemetry.istio.io v1 OSSM 2.6 · OSSM 3.x (sailoperator.io/v1) Cloud Service Mesh: TRAFFIC_DIRECTOR / in-cluster 1.2x–1.31 Sources: team Istio guide · DO328 (SM 2.0) · vendor docs
How to use itPress / to filter every section by keyword. The platform switch in the top bar (Istio · OSSM 2.6 · OSSM 3 · GKE CSM) flips every multi-platform block on the page at once. Every code block has a copy button; commands use placeholders in UPPER_CASE or <angle>.
Running example used throughout Namespace shop (in the mesh) · Service orders port 80 → container port 8080 · authorised client frontend (SA frontend) · unauthorised client reporting (SA reporting) · out-of-mesh client legacy-app in namespace legacy (no sidecar) · root namespace istio-system · trust domain cluster.local.
I

Course — Istio from the ground up

Each module: concept → diagram → annotated YAML → commands → expected result → what to remember → quiz. Upstream Istio first; platform deltas live in parts III and IV.

Module 1

Why a service mesh

In a microservice architecture dozens of services call each other. Without a mesh every team re-implements, in every language, the same cross-cutting concerns.

The problem

  • Security — is service-to-service traffic encrypted? Who is calling me?
  • Access control — frontend may call orders, reporting may not.
  • Reliability — what if a dependency is slow? How many retries? Which timeout?
  • Releases — send 10% of traffic to v2 without touching DNS or code.
  • Observability — who calls whom, with what latency and error rate?

The answer

A service mesh moves those functions out of application code into an infrastructure layer made of proxies. The app keeps doing plain HTTP/gRPC/TCP calls; the mesh adds encryption, identity, authorization, routing and telemetry.

Istio is the reference implementation. OpenShift Service Mesh (Red Hat) and Cloud Service Mesh (Google) are both Istio distributions.

AreaWhat it doesMain resources
SecurityAutomatic mTLS, workload identity, authorization, JWTPeerAuthentication, AuthorizationPolicy, RequestAuthentication
Traffic managementRouting, canary, mirroring, timeout, retry, circuit breakingVirtualService, DestinationRule, Gateway, ServiceEntry, Sidecar
ObservabilityMetrics, distributed tracing, access logsTelemetry + Prometheus, Grafana, Kiali, Jaeger/Tempo
ExtensibilityExternal authz, plugins, raw Envoy configAuthorizationPolicy CUSTOM, WasmPlugin, EnvoyFilter

Remember

  • Istio does not replace Kubernetes: it builds on Service, DNS, ServiceAccount and Deployment.
  • The application does not need to know Istio exists.
  • A mesh is a network-level feature: it intercepts and alters traffic between services (DO328 ch.1).
Which statement about a service mesh is correct?
The app speaks plain HTTP; the sidecar (or ztunnel/waypoint in ambient) does mTLS, routing and telemetry. Services and DNS are still Kubernetes.
Module 2 · basics before Istio

Networking first: how a request travels

You cannot debug a mesh without a clear picture of the path a packet takes. We build it in three steps: name → virtual IP → pod, then we add the sidecar.

Step 1 — the name: DNS is Kubernetes, not Istio

When frontend calls http://orders, the first thing that happens is a DNS lookup. Every pod has an /etc/resolv.conf written by the kubelet that points to the cluster DNS (CoreDNS) and carries search domains, so short names get completed automatically:

kubectl exec -n shop deploy/frontend -c frontend -- cat /etc/resolv.conf
# nameserver 172.30.0.10                      <- cluster DNS Service
# search shop.svc.cluster.local svc.cluster.local cluster.local
# options ndots:5

So orders, orders.shop and orders.shop.svc.cluster.local are the same thing. The answer is the ClusterIP of the Service — a virtual IP that no interface owns.

Step 2 — the virtual IP: a Service is a rule, not a box

A ClusterIP is not a machine. Each node has rules (iptables via kube-proxy, or OVN flows on OpenShift with OVN-Kubernetes) that say: "a packet to 172.30.12.5:80 must be rewritten to one of the pod IPs behind the Service, port 8080". That rewrite of the destination address is called DNAT (Destination NAT). The list of pod IPs comes from the Service's EndpointSlices.

kubectl get svc orders -n shop -o wide            # ClusterIP + port 80
kubectl get endpointslices -n shop -l kubernetes.io/service-name=orders -o wide   # pod IPs:8080
WhereWhich port
URL the client callsService port (80)
VirtualService → destination.portService port (80)
AuthorizationPolicy → to.operation.ports, PeerAuthentication → portLevelMtlsContainer port (8080) — the policy is evaluated on the receiving pod, after DNAT

Step 3 — add the sidecar: interception inside the pod

All containers of a pod share one network namespace (same IP, same localhost). Istio exploits this: at pod start, istio-init (or the Istio CNI plugin) writes iptables rules inside the pod's network namespace that REDIRECT traffic to Envoy:

  • everything the app sends out is redirected to Envoy on 15001;
  • everything arriving in for the app is redirected to Envoy on 15006, which then delivers it to the app on localhost:8080;
  • Envoy's own traffic (UID 1337) is excluded, otherwise it would loop.

Crucial consequence: the outbound Envoy already knows the endpoints of orders (istiod pushes them via EDS). It picks a pod itself and connects directly to the pod IP:8080 with mTLS — the node-level ClusterIP DNAT is effectively bypassed and load balancing is done by Envoy.

WITHOUT MESH Pod frontend · 10.128.2.7 app → http://orders ① CoreDNSorders.shop → 172.30.12.5 ② node dataplane (kube-proxy / OVN-K)DNAT 172.30.12.5:80 → 10.131.0.9:8080 Pod orders · 10.131.0.9 app :8080 plain HTTP,no identity,no policy WITH SIDECAR istiod · xDS + CA Pod frontend (shared netns) app Envoyoutbound :15001 iptables REDIRECT Pod orders Envoyinbound :15006 app :8080 localhost mTLS to Pod IP 10.131.0.9:8080 Envoy picks the endpoint itself (EDS) · SPIFFE identities exchanged
Top: plain Kubernetes path. Bottom: same call with sidecars. iptables rules live in the pod's network namespace, not on the node. istiod only configures — it is never on the request path.

See it for yourself

# iptables rules written by istio-init (not present with Istio CNI: then check the CNI plugin logs)
kubectl logs -n shop deploy/orders -c istio-init | grep -E 'ISTIO_(REDIRECT|IN_REDIRECT|OUTPUT)'

# what Envoy listens on (15001 / 15006 virtual listeners + one per service port)
istioctl proxy-config listener deploy/frontend -n shop --port 15001
# the endpoints Envoy will connect to directly (pod IPs, not the ClusterIP)
istioctl proxy-config endpoint deploy/frontend -n shop --cluster "outbound|80||orders.shop.svc.cluster.local"
Two layers, two failure domainsKubernetes answers "where is the service?" (DNS, Service, endpoints). Istio answers "who is calling, is it encrypted, is it allowed, where do I route it?". A pod stuck in Init:0/2 or ContainerCreating with sandbox/CNI events is a node/network problem, not an Istio one.

Remember

  • Name → DNS (CoreDNS) → ClusterIP → DNAT → pod IP. That is plain Kubernetes.
  • The sidecar is reached through iptables REDIRECT inside the pod: 15001 outbound, 15006 inbound.
  • With a sidecar, Envoy connects straight to the pod IP and does the load balancing.
  • Policies talk about the container port; routes talk about the Service port.
Where are the iptables rules that send app traffic to Envoy?
All containers of a pod share one netns; the redirect is local to it. istiod is never in the data path.
An AuthorizationPolicy should allow port traffic for Service orders (port 80 → targetPort 8080). Which port goes in to.operation.ports?
Authorization is evaluated by the receiving Envoy after DNAT, against the workload (container) port.
Module 3

Architecture: control plane & data plane

Kubernetes API Services · EndpointSlicesPods · ServiceAccountsIstio CRs (VS, DR, AP…) istiod • discovery + config translation (Pilot) • CA: signs SPIFFE X.509, rotates (Citadel) • validation webhook + sidecar injector :15012 xDS/CA (TLS) · :15014 metrics · :15017 webhook watch ingress gateway (standalone Envoy) sidecar istio-proxy (pilot-agent + Envoy) egress gateway (standalone Envoy) xDS (LDS/RDS/CDS/EDS/SDS) DATA PLANE application traffic: Envoy ⇄ Envoy only — istiod down = last config keeps working, no updates, no new certs CONTROL ↑
istiod configures, Envoy executes. In OSSM 2 (Maistra) and DO328 you will still see the historical names Pilot, Citadel and Galley — since Istio 1.5 they are all inside the single istiod binary.
ComponentRole
istiodReads Kubernetes + Istio resources, translates them into Envoy config and pushes it over xDS. Acts as Certificate Authority (issues and rotates workload certificates). Hosts the injection and validation webhooks.
EnvoyL4/L7 proxy that intercepts all pod traffic and applies routing, mTLS, policy, telemetry.
istio-proxyThe injected container: pilot-agent (bootstraps Envoy, fetches certs via SDS, serves health) + Envoy.
GatewaysStandalone Envoy deployments at the mesh edge (ingress/egress). Not sidecars.

Ports you will meet

PortWhereUse
15001sidecarOutbound capture (virtual listener)
15006sidecarInbound capture
15000sidecarEnvoy admin (localhost only) — config_dump, clusters, stats
15004sidecarDebug port (pilot-agent ↔ istiod)
15020sidecarMerged Prometheus metrics (app + Envoy + agent), health
15021sidecar / gatewayReadiness /healthz/ready
15090sidecarEnvoy Prometheus /stats/prometheus
15010 / 15012istiodxDS plaintext (legacy) / xDS + CA over mTLS
15014istiodControl plane metrics
15017 → 443istiodWebhooks (injection, validation)
15008ztunnel / waypointHBONE tunnel (ambient)

Sidecar mode vs ambient mode

SidecarAmbient
ProxyOne Envoy per podztunnel per node (L4 + mTLS) + optional waypoint Envoy per namespace/service (L7)
EnrolInjection label + pod restartistio.io/dataplane-mode=ambient on the namespace, no restart
CostCPU/RAM per podShared per node; L7 only where needed
StatusMature everywhereGA upstream since 1.24 and in OSSM 3.2+; on Cloud Service Mesh check the supported-features page before planning it

Remember

  • istiod configures, Envoy executes.
  • Every Istio resource you create becomes Envoy config pushed only to the proxies it concerns.
  • If istiod is down, traffic keeps flowing on the last config; new pods cannot get certs or be injected.
istiod crashes for 10 minutes. What happens to existing sidecar-to-sidecar traffic?
Envoy holds its config and certificates in memory. You lose updates, injection of new pods and cert rotation until istiod returns.
Module 4

Installing Istio & sidecar injection

Upstream Istio installs with istioctl or Helm on any conformant cluster. OpenShift uses an operator (part III), GKE uses fleet management or asmcli (part IV) — the concepts below are the same.

Method 1 — istioctl with a versioned file

# istio-install.yaml — keep it in Git
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator          # only the file format read by istioctl, no operator is installed
spec:
  profile: default           # istiod + ingress gateway
  revision: 1-26-3           # optional: revision-based (canary) control plane
  meshConfig:
    accessLogFile: /dev/stdout
    outboundTrafficPolicy:
      mode: ALLOW_ANY         # REGISTRY_ONLY to lock egress (module 8)
  values:
    global:
      proxy:
        resources:
          requests: {cpu: 50m, memory: 128Mi}
          limits:   {cpu: "1", memory: 512Mi}
istioctl x precheck                         # cluster prerequisites
istioctl install -f istio-install.yaml -y   # install / update
kubectl get pods -n istio-system           # istiod (+ gateway) Running
istioctl version                            # client / control plane / data plane versions
ProfileContent
defaultistiod + ingress gateway — recommended start
minimalistiod only; gateways deployed separately (best practice in production)
demoEverything on, high tracing — labs only
ambientistiod + istio-cni + ztunnel
openshiftUpstream profile with OpenShift tweaks (CNI, SCC) — on OCP prefer OSSM

Method 2 — Helm

helm repo add istio https://istio-release.storage.googleapis.com/charts && helm repo update
helm install istio-base istio/base -n istio-system --create-namespace            # CRDs
helm install istiod istio/istiod -n istio-system --wait                           # control plane
helm install istio-ingress istio/gateway -n istio-ingress --create-namespace      # gateway (optional)

Sidecar injection: a mutating webhook at pod creation

kubectl apply Deployment
        │
        ▼
API server ──► istiod webhook: "is injection enabled for this namespace/pod?"
        │              yes → add istio-init (or rely on CNI) + istio-proxy
        ▼
Pod created with the sidecar  (existing pods are NOT touched)
kubectl label namespace shop istio-injection=enabled            # default revision
kubectl label namespace shop istio.io/rev=1-26-3 --overwrite     # revision-based (remove istio-injection first)
kubectl get namespace -L istio-injection -L istio.io/rev
kubectl rollout restart deployment -n shop                       # label only affects NEW pods
# opt a single pod out (label on the pod template, preferred over the old annotation)
spec:
  template:
    metadata:
      labels:
        sidecar.istio.io/inject: "false"
# check containers: 2/2 = injected
kubectl get pods -n shop -o custom-columns='POD:.metadata.name,C:.spec.containers[*].name,INIT:.spec.initContainers[*].name'
# native sidecar (K8s >= 1.29): istio-proxy appears as initContainer with restartPolicy Always
kubectl get pod POD -n shop -o jsonpath='{range .spec.initContainers[*]}{.name}{" -> "}{.restartPolicy}{"\n"}{end}'
Revisions = canary upgrades of the control planeInstall the new istiod with a revision (1-27-1), relabel one namespace at a time with istio.io/rev=1-27-1, restart its workloads, verify, continue, then remove the old revision. Revision tags (istioctl tag set prod --revision 1-27-1) let namespaces point to a stable alias. The same pattern drives OSSM 3 RevisionBased updates and Cloud Service Mesh in-cluster upgrades.

Remember

  • Injection happens at pod creation — relabelling never touches running pods.
  • istio-init sets up iptables and exits; with the Istio CNI plugin there is no init container and no NET_ADMIN needed (mandatory on OpenShift).
  • Check the Istio ↔ Kubernetes support matrix before any install or upgrade.
You label namespace shop with istio-injection=enabled. Pods are still 1/1. Why?
The webhook runs at pod admission. kubectl rollout restart deployment -n shop.
Module 5

Service ports & protocol selection

Istio must know whether a port carries HTTP, HTTP/2, gRPC or opaque TCP: L7 features (routing by path/header, retries, AuthorizationPolicy on method/path, HTTP metrics) only work on L7 protocols. Declare it explicitly:

apiVersion: v1
kind: Service
metadata:
  name: orders
  namespace: shop
spec:
  selector:
    app: orders              # one Service, selects ALL versions (v1, v2…)
  ports:
  - name: http-api           # name prefix: http, http2, grpc, grpc-web, tcp, tls, mongo, redis, mysql
    appProtocol: http        # explicit field, preferred over the name prefix
    port: 80
    targetPort: 8080
Declared asEnvoy behaviour
http / http2 / grpcHTTP connection manager: L7 routing, retries, HTTP metrics, L7 authz
tcpTCP proxy: only L4 routing (by port/SNI), L4 authz (principals, ports, IP)
tls / httpsPassthrough by SNI — Envoy cannot see paths (encrypted by the app)
not declaredProtocol sniffing: guessed on the first bytes — works, but fragile (server-first protocols like MySQL/SMTP break)
Classic trapAn app that does TLS itself on port 8443 declared as http → 503/NR or garbled responses. Declare it tls (or https) and route with tls.match.sniHosts. Same for gateways in TLS PASSTHROUGH: filter chains match on SNI, so a client sending the wrong SNI gets a TLS reset, not a 404.

Remember

  • Always declare appProtocol (or the name prefix) on every Service port.
  • An L7 policy silently ignored usually means the port is treated as TCP.
  • Use FQDNs (orders.shop.svc.cluster.local) in all Istio resources.
An ALLOW policy with methods: ["GET"] on a Service port named db (no appProtocol) blocks every request. Most likely cause?
No protocol prefix / appProtocol → sniffed or TCP. For ALLOW rules, unmatched HTTP fields make the rule not match; for DENY rules they are treated as matching (dangerous — see module 11).
Module 6 · DO328 ch.4

Traffic management: VirtualService & DestinationRule

A plain Service spreads requests across all selected pods. Istio adds two resources that work as a pair.

ResourceQuestion it answers
VirtualServiceWhere does this request go? — ordered routing rules (match → route), weights, timeouts, retries, faults, mirroring
DestinationRuleHow is traffic treated once the destination is chosen? — subsets (versions), load balancing, connection pool, outlier detection, client TLS
client EnvoyGET /api · x-canary? VirtualService orders rule 1 · match header x-canary: "true"→ subset v2 rule 2 · default (no match) · last→ v1 weight 90→ v2 weight 10 · timeout 3s DestinationRule subset v1 ⇢ version=v1subset v2 ⇢ version=v2LEAST_REQUESTconnectionPooloutlierDetectiontls: ISTIO_MUTUAL (default) orders-v1pods version=v1 orders-v2pods version=v2 Both are configuration compiled into the client's Envoy — no request ever "passes through" a VirtualService object.
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: orders
  namespace: shop
spec:
  host: orders.shop.svc.cluster.local
  trafficPolicy:                # applies to all subsets unless overridden
    loadBalancer:
      simple: LEAST_REQUEST     # ROUND_ROBIN | RANDOM | PASSTHROUGH | consistentHash
  subsets:
  - name: v1
    labels: {version: v1}       # pod labels, NOT Service labels
  - name: v2
    labels: {version: v2}
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: orders
  namespace: shop
spec:
  hosts:
  - orders.shop.svc.cluster.local   # the name clients call
  http:                              # evaluated top-down, first match wins
  - route:
    - destination:
        host: orders.shop.svc.cluster.local
        subset: v1                   # must exist in the DestinationRule
        port: {number: 80}           # Service port
VirtualService fieldMeaning
hostsName(s) the rule applies to (FQDN for mesh services, external hostnames at gateways)
gatewaysAbsent = mesh sidecars only. [ns/gw] = that gateway only. Add mesh to cover both
http[].matchuri (exact/prefix/regex), headers, method, queryParams, port, sourceLabels. Items in one match block = AND; multiple blocks = OR
http[].route[]destinations with optional weight (must sum to 100)
tcp / tlsEquivalents for opaque TCP and SNI-based routing
exportToVisibility (. = this namespace only, * = all)

Remember

  • Apply the DestinationRule first, then the VirtualService that references its subsets — otherwise 503 NC/UH.
  • Most specific rules on top, catch-all (no match) at the bottom.
  • Routing never bypasses security: AuthorizationPolicy still runs on the destination.
A VirtualService references subset: v3, which is not in the DestinationRule. Result?
The route points to an Envoy cluster that does not exist. istioctl analyze flags it (IST0101 ReferencedResourceNotFound).
Module 7 · DO328 ch.5

Release strategies: canary, header routing, mirroring

Weighted canary

http:
- route:
  - destination: {host: orders.shop.svc.cluster.local, subset: v1}
    weight: 90
  - destination: {host: orders.shop.svc.cluster.local, subset: v2}
    weight: 10

Typical progression 100/0 → 90/10 → 50/50 → 0/100, checking errors and latency at every step (Kiali, Grafana). Split is independent of replica count: 10 v1 pods and 1 v2 pod still give v2 exactly 10%.

for i in $(seq 1 100); do
  kubectl exec -n shop deploy/frontend -c frontend -- curl -s http://orders.shop/version; echo
done | sort | uniq -c

Header-based routing (testers, dark users)

http:
- name: testers-to-v2              # specific rule first
  match:
  - headers:
      x-canary: {exact: "true"}
  route:
  - destination: {host: orders.shop.svc.cluster.local, subset: v2}
- name: default-v1                 # catch-all last
  route:
  - destination: {host: orders.shop.svc.cluster.local, subset: v1}
Header propagationHeader routing works on the first hop. If orders calls other services, the header reaches them only if the application copies it into its outbound calls.

Traffic mirroring (dark launch)

Send a copy of live requests to v2 while users keep getting v1's response. Responses from the mirror are discarded (fire-and-forget); mirrored requests carry a -shadow suffix in the Host header.

http:
- route:
  - destination: {host: orders.shop.svc.cluster.local, subset: v1}
    weight: 100
  mirror:
    host: orders.shop.svc.cluster.local
    subset: v2
  mirrorPercentage:
    value: 10.0          # mirror 10% (DO328 uses the deprecated mirror_percent field)
Mirroring is real trafficThe mirror executes writes. Mirror only idempotent/read paths, or point v2 at a non-production datastore.

Remember

  • Weights for progressive delivery, match for targeted groups, mirror for production-load testing with zero user impact.
  • Kiali can create weighted routing wizards — but versioned YAML in Git is the source of truth.
With mirroring to v2, who receives v2's response?
Mirror is fire-and-forget. Observe v2 via metrics/logs/traces.
Module 8 · DO328 ch.4

Ingress & egress: Gateway, ServiceEntry, Gateway API

clientshop.example.com LB / VIP / RouteL4 or passthrough ingress gateway (Envoy)Gateway: port 443 · host · TLS certVirtualService (gateways: [ns/gw])/api → orders:80 Pod ordersEnvoyapp egress gatewaysingle exit point · audit api.payment.exampleServiceEntry MESH_EXTERNAL direct (sidecar) Egress default: ALLOW_ANY (unknown hosts pass through).REGISTRY_ONLY: only hosts known to the mesh (Services + ServiceEntry) — others get 502 / BlackHoleCluster.

Ingress with Istio APIs

apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: shop-gateway
  namespace: shop
spec:
  selector:
    istio: ingressgateway          # labels of the gateway PODS — verify on your cluster
  servers:
  - port: {number: 443, name: https, protocol: HTTPS}
    hosts: ["shop.example.com"]
    tls:
      mode: SIMPLE                 # SIMPLE | MUTUAL | PASSTHROUGH | ISTIO_MUTUAL
      credentialName: shop-tls     # kubernetes.io/tls Secret in the GATEWAY POD namespace
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: shop-ingress
  namespace: shop
spec:
  hosts: ["shop.example.com"]      # must match a Gateway server host
  gateways: ["shop/shop-gateway"]  # binds this VS to the gateway
  http:
  - match: [{uri: {prefix: /api}}]
    route:
    - destination: {host: orders.shop.svc.cluster.local, port: {number: 80}}
kubectl create secret tls shop-tls -n istio-system --cert=shop.crt --key=shop.key   # namespace of the gateway pods
istioctl proxy-config listener deploy/istio-ingressgateway -n istio-system
istioctl proxy-config route deploy/istio-ingressgateway -n istio-system --name https.443.https.shop-gateway.shop -o json
curl -sv --resolve shop.example.com:443:GW_IP https://shop.example.com/api/orders
404 with server: istio-envoy and content-length: 0The gateway answered, but no route matched the Host (or path) — flag NR. Check that the Host header after any upstream rewrite (F5 iRule, Route) is listed in both Gateway.servers.hosts and VirtualService.hosts.

Ingress with the Kubernetes Gateway API

Istio implements gateway.networking.k8s.io. With gatewayClassName: istio, Istio deploys the gateway Deployment + Service for you in the Gateway's namespace. This is the strategic direction for OSSM 3 and GKE.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata: {name: shop-gw, namespace: shop}
spec:
  gatewayClassName: istio
  listeners:
  - name: https
    hostname: shop.example.com
    port: 443
    protocol: HTTPS
    tls: {mode: Terminate, certificateRefs: [{name: shop-tls}]}
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: {name: orders, namespace: shop}
spec:
  parentRefs: [{name: shop-gw}]
  hostnames: ["shop.example.com"]
  rules:
  - matches: [{path: {type: PathPrefix, value: /api}}]
    backendRefs: [{name: orders, port: 80}]

Egress: ServiceEntry, REGISTRY_ONLY, egress gateway

apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata: {name: payment, namespace: shop}
spec:
  hosts: ["api.payment.example"]
  location: MESH_EXTERNAL
  resolution: DNS                  # NONE | STATIC | DNS | DNS_ROUND_ROBIN
  ports:
  - {number: 443, name: tls, protocol: TLS}
---
# Force it through the egress gateway (TLS passthrough, SNI routing)
apiVersion: networking.istio.io/v1
kind: Gateway
metadata: {name: egress-payment, namespace: istio-system}
spec:
  selector: {istio: egressgateway}
  servers:
  - port: {number: 443, name: tls, protocol: TLS}
    hosts: ["api.payment.example"]
    tls: {mode: PASSTHROUGH}
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata: {name: payment-via-egress, namespace: shop}
spec:
  hosts: ["api.payment.example"]
  gateways: ["mesh", "istio-system/egress-payment"]
  tls:
  - match: [{gateways: ["mesh"], port: 443, sniHosts: ["api.payment.example"]}]
    route: [{destination: {host: istio-egressgateway.istio-system.svc.cluster.local, port: {number: 443}}}]
  - match: [{gateways: ["istio-system/egress-payment"], port: 443, sniHosts: ["api.payment.example"]}]
    route: [{destination: {host: api.payment.example, port: {number: 443}}}]
# is a destination known to the mesh? (BlackHoleCluster / PassthroughCluster tell you the policy)
istioctl proxy-config cluster deploy/frontend -n shop | grep -E 'payment|BlackHole|Passthrough'
# bypass the proxy for an IP range (pod annotation) — traffic leaves without mesh policy/telemetry
#   traffic.sidecar.istio.io/excludeOutboundIPRanges: "10.0.0.0/24"

Remember

  • Gateway = port/host/TLS. VirtualService = routing. You need both (or Gateway API Gateway + HTTPRoute).
  • gateways decides whether a VS applies to the mesh, to a gateway, or to both (mesh).
  • With REGISTRY_ONLY every external host needs a ServiceEntry.
  • The egress gateway is the source pod of outgoing traffic: node SNAT / EgressIP rules of its namespace apply, not the app's.
A VirtualService has gateways: [shop/shop-gateway] only. Does it affect calls from frontend to orders inside the mesh?
Without gateways a VS applies to sidecars (mesh); with a list, only to what is listed.
Module 9 · DO328 ch.6–7

Resilience & chaos testing

Timeouts and retries live in the VirtualService (client side, per route). Circuit breaking lives in the DestinationRule (per destination). Fault injection lets you prove that both actually work.

Timeout

http:
- route: [{destination: {host: orders.shop.svc.cluster.local, subset: v1}}]
  timeout: 3s          # disabled by default for HTTP routes → client gets 504 "upstream request timeout" (flag UT)

Retry

http:
- route: [{destination: {host: orders.shop.svc.cluster.local}}]
  timeout: 5s                   # covers ALL attempts
  retries:
    attempts: 3                 # extra attempts
    perTryTimeout: 1s
    retryOn: 5xx,connect-failure,reset,gateway-error
retryOnRetries when
5xxAny 5xx or connection failure / reset
gateway-error502, 503, 504
connect-failureTCP connect fails
resetUpstream resets the connection mid-request
retriable-4xx409
retriable-status-codes + 503Explicit list of codes
Retry hygieneRetry only idempotent operations (a retried payment POST = duplicate). Istio already retries twice by default on connect-failure/refused-stream/503 — declaring retries replaces that default. Retries multiply load on a struggling service: pair them with circuit breaking.

Fault injection (chaos testing)

http:
- match: [{headers: {x-chaos: {exact: "on"}}}]   # only test traffic
  fault:
    delay:
      percentage: {value: 50}
      fixedDelay: 5s
    abort:
      percentage: {value: 10}
      httpStatus: 503
  route: [{destination: {host: orders.shop.svc.cluster.local}}]
- route: [{destination: {host: orders.shop.svc.cluster.local}}]

Injected faults are visible as flags DI (delay injected) and FI (fault injected) in access logs. DO328 procedure: inject delay → verify the caller's timeout fires → inject 503 → verify retries absorb it → remove the fault.

Circuit breaking & outlier detection

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata: {name: orders, namespace: shop}
spec:
  host: orders.shop.svc.cluster.local
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
        connectTimeout: 2s
      http:
        http1MaxPendingRequests: 50     # queue; overflow → 503 UO
        http2MaxRequests: 200
        maxRequestsPerConnection: 10
    outlierDetection:
      consecutive5xxErrors: 5           # eject a pod after 5 consecutive 5xx…
      interval: 10s                     # …evaluated every 10s…
      baseEjectionTime: 30s             # …for >= 30s (grows with each ejection)
      maxEjectionPercent: 50            # never eject more than half the pool
  subsets:
  - {name: v1, labels: {version: v1}}
# load test to trip the breaker (fortio is the Istio sample client)
kubectl exec -n shop deploy/fortio -c fortio -- fortio load -c 20 -qps 0 -n 400 -loglevel Warning http://orders.shop/
# counters on the CLIENT proxy
kubectl exec -n shop deploy/frontend -c istio-proxy -- pilot-agent request GET stats | grep -E 'orders.*(upstream_rq_pending_overflow|outlier_detection)'

Remember

  • Timeout + retry → VirtualService. Circuit breaker → DestinationRule.
  • The route timeout is the total budget: attempts × perTryTimeout must fit in it.
  • UO = breaker tripped (fast fail). UH = no healthy endpoint (all ejected or not ready).
Route has timeout: 2s, attempts: 3, perTryTimeout: 1s; upstream always takes 1.5s. What does the client see?
Each try dies at 1s; the overall 2s route timeout cuts the sequence. Size budgets consistently.
Module 10 · DO328 ch.8

Workload identity & mTLS

Identity = ServiceAccount, written as SPIFFE

spiffe://<trust-domain>/ns/<namespace>/sa/<serviceaccount>
spiffe://cluster.local/ns/shop/sa/frontend        <- SPIFFE ID inside the X.509 SAN
cluster.local/ns/shop/sa/frontend                 <- "principal" as written in policies

istiod (the CA) signs an X.509 certificate for every proxy carrying its SPIFFE ID; it is delivered over SDS and rotated automatically (24h default lifetime). In mTLS both proxies present certificates, so each side knows the other's identity — independent of IPs.

kubectl get deploy frontend -n shop -o jsonpath='{.spec.template.spec.serviceAccountName}{"\n"}'
istioctl proxy-config secret deploy/frontend -n shop                     # cert + root, validity
istioctl proxy-config secret deploy/frontend -n shop -o json | jq -r '.dynamicActiveSecrets[0].secret.tlsCertificate.certificateChain.inlineBytes' | base64 -d | openssl x509 -noout -text | grep -A1 'Subject Alternative'

Auto mTLS vs enforced mTLS

When both sides have a sidecar, Istio uses mTLS automatically (auto mTLS). Using mTLS is not requiring it: PeerAuthentication decides what a workload accepts inbound.

ModeInbound acceptedfrontend (sidecar)legacy-app (no sidecar)
PERMISSIVE (default)mTLS and plaintext✅ mTLS✅ plaintext
STRICTmTLS only✅ mTLS❌ connection reset (curl 000)
DISABLEplaintextavoid
UNSETinherit from parent scope

Scope: root namespace without selector = whole mesh · app namespace without selector = namespace · with selector = those workloads. Most specific wins (workload > namespace > mesh).

apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata: {name: default, namespace: shop}     # whole namespace
spec:
  mtls: {mode: STRICT}
---
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata: {name: orders-metrics, namespace: shop}
spec:
  selector: {matchLabels: {app: orders}}        # required with portLevelMtls
  mtls: {mode: STRICT}
  portLevelMtls:
    "9090": {mode: PERMISSIVE}                  # container port, e.g. external scraper
kubectl exec -n shop deploy/frontend -c frontend -- curl -s -o /dev/null -w '%{http_code}\n' http://orders.shop/      # 200
kubectl exec -n legacy deploy/legacy-app -- curl -s -o /dev/null -w '%{http_code}\n' http://orders.shop/             # 000 reset
# proof that traffic is mTLS: destination-side metrics
kubectl exec -n shop deploy/orders -c istio-proxy -- pilot-agent request GET stats/prometheus | grep istio_requests_total | grep -o 'connection_security_policy="[a-z_]*"' | sort | uniq -c

Migration path (never flip the mesh to STRICT in one shot)

  1. Mesh stays PERMISSIVE.
  2. Inject sidecars in every workload that must communicate.
  3. Verify with metrics (connection_security_policy="mutual_tls") and Kiali (padlock on edges).
  4. Move to STRICT one namespace at a time.
  5. Finally STRICT at mesh level (root namespace).
DestinationRule overrides auto mTLSA trafficPolicy.tls section replaces auto mTLS for that host. mode: DISABLE towards a STRICT service → 503 UF. For in-mesh hosts omit tls or use ISTIO_MUTUAL.

Remember

  • Auto mTLS = use it when possible. STRICT = refuse whoever doesn't.
  • PeerAuthentication controls how you connect, not who is allowed.
  • One dedicated ServiceAccount per app — shared SAs are indistinguishable in policies.
  • Kubelet HTTP probes keep working under STRICT: Istio rewrites them to the agent on 15020.
Under STRICT, a client without sidecar calls the service. What does it get?
The inbound listener expects a TLS ClientHello with an Istio cert; plaintext never reaches the HTTP layer.
Module 11 · DO328 ch.8

AuthorizationPolicy, dry-run, vs NetworkPolicy

NetworkPolicyCNI · L3/L4 PeerAuthenticationmTLS required? RequestAuthenticationJWT present → valid? AuthorizationPolicy (on the receiver) CUSTOM DENY ALLOW app drop / timeout reset · curl 000 401 Jwt verification fails 403 RBAC: access denied No policy at all → allow · at least one ALLOW on the workload → only what an ALLOW matches A matching DENY always wins over any ALLOW · AUDIT never changes the decision
The failure symptom tells you which layer refused the request.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: NAME
  namespace: NS                 # namespace of the RECEIVER (root ns = whole mesh)
spec:
  selector:                     # optional: which workloads
    matchLabels: {app: orders}
  action: ALLOW                 # ALLOW | DENY | CUSTOM | AUDIT
  rules:                        # OR between rules
  - from:                       # WHO   } from AND to AND when
    - source: {principals: ["cluster.local/ns/shop/sa/frontend"]}
    to:                         # WHAT  }
    - operation: {methods: ["GET"], paths: ["/api/orders*"], ports: ["8080"]}
    when:                       # EXTRA }
    - key: request.headers[x-tenant]
      values: ["acme"]

from.source

  • principals — workload identity (needs mTLS)
  • namespaces — caller namespace (needs mTLS)
  • requestPrincipals — JWT iss/sub
  • ipBlocks — direct peer IP
  • remoteIpBlocks — original client IP (X-Forwarded-For, if numTrustedProxies set)

to.operation / when

  • methods, paths (exact, /api/* prefix, *.json suffix) — HTTP only
  • ports — container port
  • hosts — Host header
  • when: request.headers[...], request.auth.claims[...], source.namespace, destination.port…
  • Every field has a not… twin

The recommended pattern

# 1) default deny for the namespace: an EMPTY ALLOW (not a DENY!)
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata: {name: allow-nothing, namespace: shop}
spec: {}
---
# 2) targeted ALLOW per flow: who → to whom → what
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata: {name: orders-allow-frontend, namespace: shop}
spec:
  selector: {matchLabels: {app: orders}}
  action: ALLOW
  rules:
  - from: [{source: {principals: ["cluster.local/ns/shop/sa/frontend"]}}]
    to: [{operation: {methods: ["GET"], paths: ["/api/orders", "/api/orders/*"]}}]
---
# 3) allow the ingress gateway towards exposed services (SA name depends on install — check it)
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata: {name: frontend-allow-ingress, namespace: shop}
spec:
  selector: {matchLabels: {app: frontend}}
  rules:
  - from: [{source: {principals: ["cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account"]}}]
---
# 4) DENY only for absolute bans — ALWAYS scope to an HTTP port
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata: {name: orders-deny-admin, namespace: shop}
spec:
  selector: {matchLabels: {app: orders}}
  action: DENY
  rules:
  - to: [{operation: {paths: ["/admin", "/admin/*"], ports: ["8080"]}}]
DENY + HTTP fields on TCP trafficWhen a DENY rule contains HTTP-only attributes and hits a TCP port, the attributes cannot be evaluated and are treated as matching → the DENY blocks all TCP traffic. Always add ports to such rules.

Dry-run before enforcing

metadata:
  annotations:
    istio.io/dry-run: "true"     # evaluated + logged, never enforced (experimental)
istioctl proxy-config log deploy/orders -n shop --level rbac:debug
kubectl logs -n shop deploy/orders -c istio-proxy --since=5m | grep "shadow denied"
istioctl proxy-config log deploy/orders -n shop --level rbac:warning     # restore

AuthorizationPolicy vs Kubernetes NetworkPolicy

NetworkPolicyAuthorizationPolicy
Enforced byCNI (OVN-K, Calico…)Envoy on the receiver
LayerL3/L4 — IP, pod/ns selectors, portsL4 + L7 — identity, method, path, header, JWT claims
Caller identitylabels → IPscryptographic SPIFFE (mTLS)
Defaultallow allallow all
Modeladditive allows onlyALLOW / DENY (wins) / CUSTOM / AUDIT
Needs meshnoyes (proxy on the workload)

They are cumulative: a packet must pass the NetworkPolicy first, then the AuthorizationPolicy. With default-deny NetworkPolicies remember mesh traffic: sidecars → istiod 15012, ingress gateway → app ports, Prometheus → 15020/15090.

Remember

  • No policy = all allowed · one ALLOW = only what is allowed · a matching DENY = always denied.
  • spec: {} is an empty ALLOW, not a DENY — later ALLOWs can open exceptions.
  • Policies go in the namespace of the receiver.
  • Test ALLOW-by-identity with a second client with sidecar but a different SA; a sidecar-less client under STRICT proves nothing about authz.
Namespace has allow-nothing plus an ALLOW for frontend on GET /api/*. frontend sends POST /api/orders. Result?
Once an ALLOW exists, only matching requests pass. from AND to must both match.
Where must a policy protecting orders (ns shop) be created?
Authorization is enforced by the receiving proxy.
Module 12

End-user authentication with JWT

IdentityRepresentsResource
Peerthe calling workloadPeerAuthentication (mTLS)
Requestthe end user / client holding a tokenRequestAuthentication (JWT)
apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata: {name: orders-jwt, namespace: shop}
spec:
  selector: {matchLabels: {app: orders}}
  jwtRules:
  - issuer: "https://idp.example.com"
    jwksUri: "https://idp.example.com/.well-known/jwks.json"   # istiod fetches it: egress/proxy needed!
    forwardOriginalToken: true
    outputClaimToHeaders:
    - {header: x-user-group, claim: groups}
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata: {name: orders-require-jwt, namespace: shop}
spec:
  selector: {matchLabels: {app: orders}}
  action: ALLOW
  rules:
  - from: [{source: {requestPrincipals: ["https://idp.example.com/*"]}}]
    when:
    - key: request.auth.claims[groups]
      values: ["orders-admin"]
RequestRequestAuthentication only+ AuthorizationPolicy requestPrincipals
Valid token✅ 200✅ 200
Invalid token (sig, exp, iss)❌ 401❌ 401
No token✅ 200 (surprise!)❌ 403
TOKEN="<jwt>"
kubectl exec -n shop deploy/frontend -c frontend -- curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN" http://orders.shop/api/orders
# decode claims locally
echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .

Remember

  • RequestAuthentication validates, AuthorizationPolicy makes the token mandatory.
  • 401 = token present but invalid · 403 = token missing or not authorised.
  • In air-gapped clusters istiod must reach jwksUri — or inline the keys with jwks:.
Only a RequestAuthentication is applied. A request arrives with no Authorization header. Result?
RequestAuthentication rejects only invalid tokens; requiring one is the AuthorizationPolicy's job.
Module 13 · DO328 ch.3

Observability: metrics, Kiali, tracing, access logs

ToolQuestion
PrometheusCollects proxy + control-plane metrics
GrafanaHow are services doing over time?
KialiWho talks to whom? mTLS padlocks, health, config validation, wizards
Jaeger / TempoWhich path did this request take, where was time lost?
Access logWhat exactly happened to each request on one proxy (code + flags)

Standard metrics

istio_requests_total, istio_request_duration_milliseconds (histogram), istio_request_bytes/istio_response_bytes, istio_tcp_sent_bytes_total, istio_tcp_connections_opened_total. Key labels: reporter (source|destination — always filter on one, or you count twice), source_workload, destination_workload, response_code, response_flags, connection_security_policy.

# RPS by response code, measured by the server
sum by (response_code) (rate(istio_requests_total{reporter="destination",destination_workload="orders"}[5m]))

# 5xx ratio
sum(rate(istio_requests_total{reporter="destination",destination_workload="orders",response_code=~"5.."}[5m]))
/ sum(rate(istio_requests_total{reporter="destination",destination_workload="orders"}[5m]))

# p95 latency
histogram_quantile(0.95, sum by (le) (rate(istio_request_duration_milliseconds_bucket{reporter="destination",destination_workload="orders"}[5m])))

# non-mTLS traffic still in the mesh (migration to STRICT)
sum by (source_workload, destination_workload) (rate(istio_requests_total{reporter="destination",connection_security_policy!="mutual_tls"}[10m]))

# who returns breaker / no-healthy-upstream
sum by (destination_workload, response_flags) (rate(istio_requests_total{response_flags=~"UO|UH|UF|NR"}[5m]))

Tracing: three ingredients

# 1) provider in meshConfig (or Istio CR values.meshConfig on OSSM 3)
meshConfig:
  extensionProviders:
  - name: otel-tracing
    opentelemetry:
      service: otel-collector.observability.svc.cluster.local
      port: 4317
---
# 2) Telemetry in the root namespace: ONE default resource, no selector
apiVersion: telemetry.istio.io/v1
kind: Telemetry
metadata: {name: mesh-default, namespace: istio-system}
spec:
  tracing:
  - providers: [{name: otel-tracing}]
    randomSamplingPercentage: 1.0
  accessLogging:
  - providers: [{name: envoy}]          # built-in: proxy stdout
3) Header propagation is the app's jobEnvoy creates spans but cannot link an inbound request to the outbound calls the app makes. Apps must copy traceparent/tracestate (W3C) or x-b3-*, plus x-request-id. DO328 shows this with OpenTracing/Jaeger libraries in Quarkus — today use OpenTelemetry SDKs. Without it you get fragmented traces.
istioctl dashboard kiali        # also grafana | prometheus | jaeger — temporary port-forward
kubectl logs -n shop deploy/orders -c istio-proxy --tail=20
# per-workload access log only (Telemetry with selector)

Remember

  • Metrics for trends, traces for one request's path, logs for per-request detail.
  • Always pin reporter in PromQL.
  • Observability UIs expose sensitive topology: never publish them without auth.
Traces show each service as a separate one-span trace. Cause?
Envoy can't correlate in/out requests inside the app — propagation is mandatory.
Module 14

Troubleshooting method

Bottom-up, one change at a time. HTTP code + Envoy response flag almost always point to the right layer.

1. Pod healthy?               kubectl get pods / describe / events        (Init stuck = node/CNI, not Istio)
2. Sidecar present & ready?   2/2, istio-proxy Ready, injection labels
3. Proxy in sync?             istioctl proxy-status   (SYNCED / STALE / NOT SENT)
4. Config valid?              istioctl analyze -n NS  |  -A
5. What applies to the pod?   istioctl x describe pod POD -n NS
6. What does Envoy say?       access log + response_flags, proxy-config route/cluster/endpoint
7. Who refused?               403 authz · 401 JWT · reset mTLS · 404 NR · 503 UH/UF/UO/NC · 504 UT
SymptomProbable causeCheck
Pod 1/1Injection off / pod older than labelns labels, rollout restart
Pod 1/2, proxy not readyNo config/cert from istiodproxy-status, istio-proxy + istiod logs, NetworkPolicy to 15012
403 RBAC: access deniedAuthorizationPolicyx describe pod, rbac:debug
401 Jwt verification failsBad token / unreachable JWKSissuer, jwksUri, exp
Reset / curl 000Plaintext client → STRICTclient sidecar? PeerAuthentication
404 istio-envoy len 0 · NRNo route for Host/pathVS/Gateway hosts, proxy-config route
503 UFConnect failure / TLS mismatchDR tls, target port, NetworkPolicy
503 UHNo healthy endpoint / empty subsetpod labels vs subsets, proxy-config endpoint
503 UOCircuit breakerDR connectionPool
503 NCCluster (subset) not foundDR applied? subset name
504 UTRoute timeoutVS timeout vs app latency
502 / BlackHoleClusterREGISTRY_ONLY, no ServiceEntryproxy-config cluster | grep BlackHole
L7 policy ignoredPort seen as TCPappProtocol/port name
Envoy CrashLoop, EMFILEToo few file descriptors (nofile 1024)cat /proc/1/limits in istio-proxy, container runtime defaults

Remember

  • istioctl analyze and istioctl x describe pod first.
  • Distinguish 403 (authz), 401 (JWT), reset (mTLS).
  • A TCP timeout from inside the istio-proxy container towards an external IP is a network/firewall problem, not a mesh problem.
Access log shows 503 UH right after a canary VS was applied. First check?
UH = the cluster exists but has no healthy endpoints: subset labels match no ready pod.
Module 15

Best practices & certification path

Identity & mTLS

  • Dedicated ServiceAccount per app, never default
  • PERMISSIVE → verify → STRICT per namespace → mesh
  • No tls: DISABLE in in-mesh DestinationRules

Authorization

  • allow-nothing in every app namespace
  • ALLOW by principals, not IPs
  • DENY only for absolute bans, always port-scoped
  • Allow the ingress gateway SA to exposed services
  • Dry-run new policies on live services
  • Policies in Git, applied by pipeline

Traffic

  • FQDNs in hosts/host
  • Consistent version labels ↔ subsets
  • DestinationRule before VirtualService
  • Explicit timeouts on critical paths; retries only idempotent
  • Circuit breaking on services that can saturate
  • appProtocol on every port

Operations

  • istioctl analyze in CI before apply
  • Version matrix checked before every upgrade
  • Revision-based upgrades in production
  • Sidecar resources sized centrally (mesh defaults), not per Deployment
  • Access logs on in test, sampled tracing in prod
  • Sidecar resource (egress.hosts) to shrink Envoy config in big meshes

Which resource do I need?

I want to…Resource
Enforce mTLSPeerAuthentication
Decide who can call a serviceAuthorizationPolicy
Validate / require JWTRequestAuthentication (+ AuthorizationPolicy)
Route, canary, mirror, timeout, retry, faultVirtualService (or HTTPRoute)
Subsets, LB, circuit breaker, client TLSDestinationRule
Expose a serviceGateway + VirtualService (or Gateway API)
Declare an external serviceServiceEntry
Limit what a proxy sees / egress scopeSidecar
Tracing, metrics, access logsTelemetry

Certification path

Red Hat DO328 → EX328DO328 "Building Resilient Microservices with Istio and Red Hat OpenShift Service Mesh" (the attached student guide is edition SM 2.0, 2023) covers: install (SMCP/SMMR), observability (Jaeger, Prometheus/Grafana, Kiali), traffic control, canary & mirror, chaos testing, timeouts/retries/circuit breaker, mTLS & authorization. Together with DO180/DO288 it prepares EX328 (Red Hat Certified Specialist in Building Resilient Microservices). Course modules 3–13 above map 1:1 to its chapters; DO328 labs use OSSM 2 APIs — part III shows the OSSM 3 equivalents. Upstream alternative: Istio Certified Associate (ICA) by the Linux Foundation.
DO328 chapterHere
1 Introducing OSSM · 2 InstallingModules 1–4 · OSSM 2.6 · OSSM 3
3 Observing a Service MeshModule 13 · OSSM observability
4 Controlling Service TrafficModules 6, 8
5 Releasing ApplicationsModule 7
6 Chaos Testing · 7 Resilient ServicesModule 9
8 Securing OSSMModules 10–12
II

Cheatsheet — operate any Istio flavour

Paste-ready commands. Multi-platform blocks follow the switch in the top bar. On a Windows VDI (cmd) replace grep with findstr and use double quotes in jsonpath.

Cheatsheet

Rosetta stone: same task, five flavours

TaskIstio upstreamOSSM 2.6 MaistraOSSM 3.x SailGKE · CSM managed TDGDC VMware · CSM in-cluster asmcli
Installistioctl install / HelmOLM servicemeshoperatorOLM servicemeshoperator3gcloud container fleet mesh update --management automaticasmcli install --platform multicloud
Control plane objectIstioOperator file / Helm valuesServiceMeshControlPlane (maistra.io/v2)Istio, IstioRevision, IstioCNI (sailoperator.io/v1, cluster-scoped)fleet feature + ControlPlaneRevisionistiod Deployment istiod-asm-1XXX-Y
Control plane runsistiod in clusteristiod-basic in istio-systemistiod in spec.namespaceGoogle-managed, outside the clusteristiod in cluster
Mesh membershipall ns (or discoverySelectors)ServiceMeshMemberRoll / ServiceMeshMemberdiscoverySelectors (optional)whole fleet membershipall ns (or discoverySelectors)
Injectionns istio-injection=enabledmember ns + pod sidecar.istio.io/inject: "true"ns istio-injection=enabled or istio.io/revns istio-injection=enabledns istio.io/rev=asm-1XXX-Y
Traffic captureistio-init (or CNI)CNI (always)IstioCNI (required on OCP)istio-cni (managed)istio-init / CNI
Ingressgateway chart / Gateway APISMCP gateways + IOR Routesgateway injection / Gateway API + explicit Routegateway injection / GKE Gateway APIgateway injection
CAistiodistiod (or cert-manager istio-csr)istiod (or cert-manager istio-csr)CSM CA (Mesh CA) / CA ServiceMesh CA (default) / istiod CA
MetricsPrometheusSMCP-bundled Prometheus (or UWM)OCP user-workload monitoringCloud MonitoringCloud Monitoring
Tracingany OTel/JaegerJaeger / TempoTempo + OpenTelemetryCloud TraceCloud Trace
UIKialiKiali (SMCP addon)Kiali CR + OSSM console pluginCSM page in Cloud consoleCSM page in Cloud console
Sidecar default resourcesvalues.global.proxy.resourcesspec.proxy.runtime.container.resourcesspec.values.global.proxy.resourcespod annotations (managed defaults)values.global.proxy.resources via overlay
Egress lockmeshConfig.outboundTrafficPolicyspec.proxy.networking.trafficControl.outbound.policyspec.values.meshConfig.outboundTrafficPolicymesh ConfigMap in istio-system (istio-asm-managed*) — check supported fieldsmeshConfig overlay
Upgradesrevisions / in-placebump spec.version (in place)InPlace or RevisionBasedrelease channel, managed data planenew revision + relabel
GitOpsArgo CD / FluxOpenShift GitOps (Argo CD)OpenShift GitOps (Argo CD)Config Sync (RootSync / RepoSync, fleet)Config Sync (fleet member)
istioctl ps✅✅ -i istio-system --revision basic✅❌ (no in-cluster istiod)✅
Cheatsheet

Status & inventory

istioctl version
kubectl get pods -n istio-system -o wide
istioctl proxy-status
istioctl analyze -A
istioctl tag list
kubectl get mutatingwebhookconfigurations,validatingwebhookconfigurations | grep -i istio
kubectl get crd | grep -c 'istio.io'
oc get csv -n openshift-operators | grep -Ei 'servicemesh|kiali|jaeger|tempo|opentelemetry|elasticsearch'
oc get smcp -A -o wide                                          # READY n/n, STATUS ComponentsReady, VERSION v2.6
oc get smcp basic -n istio-system -o jsonpath='{.status.readiness.components}{"\n"}'
oc get smmr default -n istio-system -o jsonpath='{.status.configuredMembers}{"\n"}'
oc get smmr default -n istio-system -o jsonpath='{.status.pendingMembers}{"\n"}'
oc get smm -A                                                   # ServiceMeshMember per namespace
oc get pods -n istio-system
oc get route -n istio-system                                    # IOR / addon routes
istioctl proxy-status -i istio-system --revision basic
oc get ns -l maistra.io/member-of=istio-system --no-headers | wc -l
oc get csv -n openshift-operators | grep -Ei 'servicemeshoperator3|kiali|tempo|opentelemetry'
oc get istio,istiorevision,istiorevisiontag,istiocni            # cluster-scoped
oc get istio default -o jsonpath='{.spec.version}{"  "}{.status.state}{"\n"}'
oc get istiorevision                                            # READY · STATUS · IN USE · VERSION
oc get pods -n istio-system ; oc get pods -n istio-cni
istioctl proxy-status -i istio-system
istioctl analyze -A
oc get ns -L istio-injection -L istio.io/rev | grep -Ev '<none>\s+<none>$'
gcloud container fleet mesh describe --project FLEET_PROJECT_ID
gcloud container fleet memberships list --project FLEET_PROJECT_ID
kubectl get controlplanerevision -n istio-system
kubectl get dataplanecontrols -o custom-columns=REV:.spec.revision,STATUS:.status.state
kubectl get mutatingwebhookconfigurations | grep -i istio
kubectl get ns -L istio-injection -L istio.io/rev
kubectl get pods -n istio-system
Read the describe outputPer membership: controlPlaneManagement.state: ACTIVE, implementation: TRAFFIC_DIRECTOR (current) or ISTIOD (deprecated), dataPlaneManagement.state: ACTIVE. With TRAFFIC_DIRECTOR there is no istiod pod — that is expected.
kubectl --kubeconfig USER_CLUSTER_KUBECONFIG get pods -n istio-system --show-labels
kubectl --kubeconfig USER_CLUSTER_KUBECONFIG get deploy -n istio-system -l app=istiod -o custom-columns=NAME:.metadata.name,REV:.metadata.labels.istio\.io/rev
OUTPUT_DIR/istio-*/bin/istioctl version --kubeconfig USER_CLUSTER_KUBECONFIG
OUTPUT_DIR/istio-*/bin/istioctl proxy-status --kubeconfig USER_CLUSTER_KUBECONFIG
kubectl --kubeconfig USER_CLUSTER_KUBECONFIG get ns -L istio-injection -L istio.io/rev
Cheatsheet

Enrol a namespace / inject sidecars

kubectl label namespace shop istio-injection=enabled --overwrite
# or revision-based
kubectl label namespace shop istio-injection- istio.io/rev=1-27-1 --overwrite
kubectl rollout restart deployment -n shop
kubectl get pods -n shop      # READY 2/2
# 1) membership — append to the SMMR (json patch keeps the existing list)
oc patch smmr default -n istio-system --type json -p '[{"op":"add","path":"/spec/members/-","value":"shop"}]'
#    or, from the app namespace (needs only ns-level rights):
oc apply -n shop -f - <<'EOF'
apiVersion: maistra.io/v1
kind: ServiceMeshMember
metadata:
  name: default
spec:
  controlPlaneRef: {namespace: istio-system, name: basic}
EOF
oc get ns shop --show-labels | grep maistra.io/member-of
# 2) injection is opt-in PER POD TEMPLATE in OSSM 2
oc patch deploy orders -n shop --type merge -p '{"spec":{"template":{"metadata":{"annotations":{"sidecar.istio.io/inject":"true"}}}}}'
oc get pods -n shop
ClusterWide SMCPWith spec.mode: ClusterWide membership is driven by SMMR.spec.memberSelectors (often matchLabels: {istio-injection: enabled}). Check your actual SMMR: oc get smmr default -n istio-system -o yaml.
# Istio CR named "default" (InPlace) or an IstioRevisionTag "default" → istio-injection works
oc label namespace shop istio-injection=enabled --overwrite
# otherwise point to a specific revision
oc label namespace shop istio-injection- istio.io/rev=REVISION_NAME --overwrite
# if the Istio CR uses discoverySelectors, the ns must match them too
oc label namespace shop istio-discovery=enabled --overwrite
oc rollout restart deployment -n shop
istioctl proxy-status -i istio-system | grep shop
kubectl label namespace shop istio.io/rev- istio-injection=enabled --overwrite
kubectl rollout restart deployment -n shop
kubectl get pods -n shop
# managed data plane: opt a namespace out of automatic proxy upgrades
kubectl annotate --overwrite namespace shop mesh.cloud.google.com/proxy={\"managed\":\"false\"}
REV=$(kubectl -n istio-system get deploy -l app=istiod -o jsonpath='{.items[0].metadata.labels.istio\.io/rev}')
kubectl label namespace shop istio-injection- istio.io/rev=$REV --overwrite
kubectl rollout restart deployment -n shop
Cheatsheet

istioctl: config, describe, proxy-config

istioctl analyze -n shop                              # static config validation (IST0xxx codes)
istioctl analyze -A --output-threshold Info
istioctl analyze my-vs.yaml -n shop                   # validate a file before apply (CI)
istioctl x describe pod POD -n shop                   # what applies: mTLS, VS, DR, authz
istioctl x describe svc orders -n shop
istioctl proxy-status                                 # SYNCED / STALE / NOT SENT per proxy
istioctl proxy-status deploy/orders -n shop           # diff istiod vs proxy config
istioctl proxy-config listener deploy/orders -n shop
istioctl proxy-config listener deploy/orders -n shop --port 15006 -o json
istioctl proxy-config route    deploy/frontend -n shop --name 80 -o json
istioctl proxy-config cluster  deploy/frontend -n shop --fqdn orders.shop.svc.cluster.local
istioctl proxy-config endpoint deploy/frontend -n shop --cluster "outbound|80|v2|orders.shop.svc.cluster.local"
istioctl proxy-config secret   deploy/orders -n shop
istioctl proxy-config bootstrap deploy/orders -n shop -o json | jq '.bootstrap.node.metadata | {ISTIO_VERSION,CLUSTER_ID,MESH_ID}'
istioctl proxy-config all deploy/orders -n shop -o json > orders-envoy.json
istioctl proxy-config log deploy/orders -n shop --level rbac:debug,jwt:debug,http:debug
istioctl proxy-config log deploy/orders -n shop --level warning              # reset
istioctl admin log -i istio-system                    # istiod log levels
istioctl bug-report --include shop                    # full support bundle
istioctl x precheck
istioctl dashboard envoy deploy/orders -n shop        # Envoy admin UI via port-forward
Envoy cluster namingoutbound|PORT|SUBSET|FQDN for client-side clusters, inbound|PORT|| for the local app. PassthroughCluster = unknown host allowed (ALLOW_ANY), BlackHoleCluster = unknown host blocked (REGISTRY_ONLY).
Cheatsheet

Envoy admin through pilot-agent

Works even when istioctl cannot reach istiod (Traffic Director, air-gapped bastion with old istioctl). Run inside the istio-proxy container.

P="kubectl exec -n shop deploy/orders -c istio-proxy --"
$P pilot-agent request GET server_info | jq -r '.state, .command_line_options.concurrency'
$P pilot-agent request GET clusters | grep -E 'orders|health_flags'
$P pilot-agent request GET 'stats?filter=upstream_rq_(5xx|timeout|retry|pending_overflow)'
$P pilot-agent request GET stats/prometheus | grep istio_requests_total | head
$P pilot-agent request GET 'config_dump?resource=dynamic_active_clusters' > clusters.json
$P pilot-agent request GET listeners
$P pilot-agent request GET certs | jq '.certificates[].cert_chain[0] | {subject_alt_names, expiration_time}'
$P pilot-agent request POST 'logging?level=debug'          # all loggers (noisy!)
$P pilot-agent request POST 'logging?rbac=debug'           # one logger
$P cat /proc/1/limits | grep -i 'open files'               # nofile of Envoy
$P curl -s localhost:15000/memory
Cheatsheet

Connectivity tests

# HTTP code from a meshed client
kubectl exec -n shop deploy/frontend -c frontend -- curl -s -o /dev/null -w '%{http_code}\n' http://orders.shop:80/api/orders
# with method + header + timing breakdown
kubectl exec -n shop deploy/frontend -c frontend -- curl -s -o /dev/null -w 'code=%{http_code} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n' -X POST -H 'x-canary: true' http://orders.shop/api/orders
# authorisation matrix: several clients → one target
for c in frontend reporting; do printf '%-10s ' $c; kubectl exec -n shop deploy/$c -c $c -- curl -s -o /dev/null -w '%{http_code}\n' http://orders.shop/api/orders; done
# raw TCP connect test from the SIDECAR (no bash needed) — isolates mesh vs network
kubectl exec -n shop deploy/orders -c istio-proxy -- curl -sv --max-time 5 telnet://10.0.0.10:443 2>&1 | grep -E 'Connected|timed out|refused'
# DNS as the app sees it
kubectl exec -n shop deploy/frontend -c istio-proxy -- getent hosts orders.shop.svc.cluster.local
# ingress: force Host + SNI to a gateway IP / NodePort
curl -skv --resolve shop.example.com:443:GW_IP https://shop.example.com/api/orders -o /dev/null
curl -sv -H 'Host: shop.example.com' http://NODE_IP:HTTP_NODEPORT/api/orders -o /dev/null
# which cert does the gateway present for an SNI?
openssl s_client -connect GW_IP:443 -servername shop.example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates
Cheatsheet

Envoy response flags

In access logs (after the response code) and in the response_flags metric label.

FlagMeaningTypical Istio cause
-No anomaly—
NRNo route configuredHost not in any VS/Gateway, wrong port, missing mesh in VS gateways
NCNo cluster foundSubset referenced but DR missing
UHNo healthy upstreamNo ready pods, subset labels match nothing, all ejected
UFUpstream connection failuremTLS mismatch, wrong targetPort, NetworkPolicy, app not listening
UOUpstream overflowCircuit breaker connectionPool limits
URXRetry / connect attempts exceededEvery retry failed
UTUpstream request timeoutVS timeout expired
UCUpstream connection terminationApp closed keep-alive connection (idle timeout mismatch)
URUpstream remote resetApp/peer reset the stream
DCDownstream connection terminationClient gave up (its own timeout)
LRLocal resetEnvoy reset (draining, overload)
DI / FIDelay / fault injectedVS fault
RLRate limitedLocal/global rate limit
UAEXUnauthorized external serviceAuthorizationPolicy CUSTOM (ext_authz) denied
DPE / UPEDownstream / upstream protocol errorWrong appProtocol, HTTP to a TLS port
SIStream idle timeoutLong-poll / streaming without data
DFDNS resolution failedServiceEntry resolution: DNS host not resolvable
Cheatsheet

Labels & annotations

KeyOnEffect
istio-injection=enabled|disabledNamespace (label)Default-revision injection
istio.io/rev=REVNamespace / pod (label)Inject from a specific revision / tag
sidecar.istio.io/inject: "true|false"Pod template (label; annotation in OSSM 2)Per-pod opt-in/out
sidecar.istio.io/proxyCPU, proxyCPULimitPod annotationSidecar CPU request / limit (e.g. "1300m")
sidecar.istio.io/proxyMemory, proxyMemoryLimitPod annotationSidecar memory request / limit
proxy.istio.io/configPod annotationProxyConfig JSON: {"holdApplicationUntilProxyStarts":true,"terminationDrainDuration":"30s","concurrency":2}
sidecar.istio.io/logLevelPod annotationEnvoy log level at start
traffic.sidecar.istio.io/excludeOutboundIPRangesPod annotationCIDRs that bypass Envoy (no policy, no metrics)
traffic.sidecar.istio.io/includeOutboundIPRangesPod annotationOnly these CIDRs are captured
traffic.sidecar.istio.io/excludeOutboundPorts / excludeInboundPortsPod annotationPorts that bypass Envoy (e.g. DB, JMX)
sidecar.istio.io/rewriteAppHTTPProbers: "false"Pod annotationDisable probe rewrite
inject.istio.io/templates: gatewayPod annotationGateway injection (Deployment becomes a gateway)
networking.istio.io/exportToService annotationVisibility of the Service to other namespaces
istio.io/dry-run: "true"AuthorizationPolicy annotationShadow mode
istio.io/dataplane-mode=ambient · istio.io/use-waypointNamespace / service (label)Ambient enrolment / waypoint binding
maistra.io/member-of OSSM 2Namespace (set by operator)Shows which SMCP owns the ns
maistra.io/expose-route: "true" OSSM 2Pod template (label)Allows OpenShift Router traffic through the operator-managed NetworkPolicies
maistra.io/ignore-namespace: "true" OSSM 2Namespace (label)2.6 injector ignores the ns — used during migration to 3
mesh.cloud.google.com/proxy CSMControlPlaneRevision / ns / pod (annotation){"managed":"false"} disables managed data plane upgrades
configmanagement.gke.io/managed · configsync.gke.io/manager Config SyncAny managed object (annotation)Object reconciled from Git (enabled/disabled) and by which RootSync/RepoSync — day-2
client.lifecycle.config.k8s.io/deletion: detach Config SyncObject in Git (annotation)Not pruned if removed from the repo
config.kubernetes.io/depends-on Config SyncObject in Git (annotation)Apply order (e.g. VS after its DR)
Cheatsheet

Apply-ready policy snippets

NS=shop
# STRICT mTLS for the namespace
kubectl apply -n $NS -f - <<'EOF'
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata: {name: default}
spec: {mtls: {mode: STRICT}}
EOF
# default deny (empty ALLOW)
kubectl apply -n $NS -f - <<'EOF'
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata: {name: allow-nothing}
spec: {}
EOF
# allow everything from the same namespace
kubectl apply -n $NS -f - <<EOF
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata: {name: allow-same-namespace}
spec:
  action: ALLOW
  rules: [{from: [{source: {namespaces: ["$NS"]}}]}]
EOF
# who is allowed? list policies + selectors
kubectl get authorizationpolicy -n $NS -o custom-columns=NAME:.metadata.name,ACTION:.spec.action,SELECTOR:.spec.selector.matchLabels,DRYRUN:.metadata.annotations.istio\.io/dry-run
kubectl get peerauthentication,requestauthentication,authorizationpolicy -A
kubectl get virtualservice,destinationrule,gateway,serviceentry,sidecar,envoyfilter,telemetry -A
Cheatsheet

PromQL for mesh & sidecar health

# connected proxies per istiod
sum by (pod) (pilot_xds{type="ads"})
# config pushes / rejected configs (bad EnvoyFilter, conflicts)
sum(rate(pilot_xds_pushes[5m])) by (type)
sum(pilot_total_xds_rejects) by (type)
# p99 time for a config change to reach proxies
histogram_quantile(0.99, sum by (le) (rate(pilot_proxy_convergence_time_bucket[10m])))
# sidecar CPU vs limit and throttling (sizing istio-proxy)
sum by (namespace,pod) (rate(container_cpu_usage_seconds_total{container="istio-proxy"}[5m]))
sum by (namespace,pod) (rate(container_cpu_cfs_throttled_periods_total{container="istio-proxy"}[5m]))
/ sum by (namespace,pod) (rate(container_cpu_cfs_periods_total{container="istio-proxy"}[5m]))
# sidecar memory working set top 10
topk(10, max by (namespace,pod) (container_memory_working_set_bytes{container="istio-proxy"}))
# gateway: 5xx by route host
sum by (destination_service_name, response_code) (rate(istio_requests_total{reporter="source",source_workload=~"istio-ingressgateway.*",response_code=~"5.."}[5m]))
III

OpenShift Service Mesh — 2.6 (Maistra) and 3.x (Sail)

What changes on OCP: operators instead of istioctl install, SMCP/SMMR in 2.x, upstream-shaped Istio CR in 3.x, CNI mandatory, Routes, OVN-Kubernetes EgressIP, platform monitoring.

OpenShift

OSSM 2.6 vs OSSM 3 at a glance

TopicOSSM 2.6 final 2.xOSSM 3.x Sail operator
BaseMaistra fork of Istio (2.6 ≈ Istio 1.20)Clean upstream Istio, rebuilt by Red Hat (OpenSSL/FIPS kept)
Operator / packageservicemeshoperatorservicemeshoperator3 — both can run in parallel
Control plane APIServiceMeshControlPlane (maistra.io/v2), Maistra-specific schemaIstio (sailoperator.io/v1), cluster-scoped, spec.values = upstream Helm values
Other CRsServiceMeshMemberRoll, ServiceMeshMember, ServiceMeshExtension (old)IstioRevision, IstioRevisionTag, IstioCNI, ZTunnel (ambient)
Tenancy defaultMultiTenant (members listed in SMMR) or ClusterWideCluster-wide; scope with discoverySelectors; multiple control planes optional
InjectionMember ns + per-pod sidecar.istio.io/inject: "true"Upstream: ns istio-injection / istio.io/rev
GatewaysDeclared inside the SMCP (gateways.ingress/egress/additionalIngress)Gateway injection Deployments or Gateway API — owned by you
RoutesIOR creates Routes automatically from Gateway hostsNo automatic Routes — create them explicitly
NetworkPoliciesOperator creates them in members (manageNetworkPolicy)None created — design your own
ObservabilityBundled Prometheus, Grafana, Jaeger, Kiali as SMCP addonsOperator manages only Istio: user-workload monitoring, Tempo + OpenTelemetry, Kiali CR + console plugin
istioctlPartially supportedSupported
AmbientNoGA from 3.2 (OCP 4.18+); not on FIPS clusters
UpgradeChange spec.version → in placeupdateStrategy: InPlace or RevisionBased (canary)
FederationMaistra federationNot available — use upstream multi-cluster
Lifecycle2.6 is the last 2.x release; Red Hat committed to at least one year of overlap with 3.0. Upgrades inside 3.x must be sequential (e.g. 3.0 → 3.1 → 3.2). Each minor is supported ~18 months — check the Red Hat OSSM life-cycle page before planning, and confirm the OCP range of the exact z-stream (3.2.x supports OCP 4.18+).
OpenShift · OSSM 2.6

OSSM 2.6: SMCP, SMMR, SMM

Operators (DO328 install flow, updated channels)

oc apply -f - <<'EOF'
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata: {name: servicemeshoperator, namespace: openshift-operators}
spec:
  channel: stable
  name: servicemeshoperator
  source: redhat-operators
  sourceNamespace: openshift-marketplace
  installPlanApproval: Manual        # production: approve InstallPlans explicitly
EOF
oc get installplan -n openshift-operators
oc get csv -n openshift-operators | grep servicemesh

A production-shaped SMCP

apiVersion: maistra.io/v2
kind: ServiceMeshControlPlane
metadata: {name: basic, namespace: istio-system}
spec:
  version: v2.6
  mode: ClusterWide                    # default: MultiTenant
  security:
    dataPlane: {mtls: true}            # mesh-wide STRICT
    controlPlane: {mtls: true}
    manageNetworkPolicy: true          # operator writes NetworkPolicies in member ns
  proxy:
    runtime:
      container:
        resources:                     # DEFAULT for every sidecar in the mesh
          requests: {cpu: 100m, memory: 128Mi}
          limits:   {cpu: 1300m, memory: 1Gi}
    networking:
      trafficControl:
        outbound: {policy: ALLOW_ANY}  # REGISTRY_ONLY to lock egress
    accessLogging:
      file: {name: /dev/stdout}
  gateways:
    openshiftRoute: {enabled: false}   # IOR off: Routes are managed explicitly
    ingress:
      enabled: true
      service: {type: NodePort}        # VIP on the load balancer → NodePort
      runtime:
        deployment: {replicas: 2, autoScaling: {enabled: false}}
    egress: {enabled: true}
    additionalIngress:                 # second gateway, e.g. for back-end traffic
      backend-ingressgateway:
        enabled: true
        service: {type: NodePort}
  tracing:
    type: Jaeger                       # None when using Tempo/OTel via meshConfig
    sampling: 100                      # unit = 0.01% → 100 = 1%, 10000 = 100%
  addons:
    kiali: {enabled: true}
    prometheus: {enabled: true}
    grafana: {enabled: false}
# MultiTenant: list member projects
apiVersion: maistra.io/v1
kind: ServiceMeshMemberRoll
metadata: {name: default, namespace: istio-system}
spec:
  members: [shop, payments]
  # ClusterWide alternative:
  # memberSelectors:
  # - matchLabels: {istio-injection: enabled}

Day-2 commands

oc get smcp -n istio-system -o wide                                   # READY 9/9 · ComponentsReady
oc describe smcp basic -n istio-system | sed -n '/Conditions/,$p'
oc get smmr default -n istio-system -o yaml | yq '.status'
oc get ns -l maistra.io/member-of=istio-system
# change mesh-wide sidecar limits (all NEW pods; restart workloads to apply)
oc patch smcp basic -n istio-system --type merge -p '{"spec":{"proxy":{"runtime":{"container":{"resources":{"limits":{"cpu":"1300m"}}}}}}}'
# lock egress
oc patch smcp basic -n istio-system --type merge -p '{"spec":{"proxy":{"networking":{"trafficControl":{"outbound":{"policy":"REGISTRY_ONLY"}}}}}}'
# capture only some IP ranges mesh-wide (DO328 ch.4)
oc patch smcp basic -n istio-system --type merge -p '{"spec":{"proxy":{"networking":{"trafficControl":{"outbound":{"includedIPRanges":["10.0.0.0/8"]}}}}}}'
# operator-managed NetworkPolicies in a member namespace
oc get networkpolicy -n shop -l maistra.io/member-of=istio-system
# effective rendered config
oc get cm istio-basic -n istio-system -o jsonpath='{.data.mesh}' | head -50
istioctl proxy-status -i istio-system --revision basic
Field note — sizing sidecarsChanging istio-proxy CPU limits via per-Deployment annotations (sidecar.istio.io/proxyCPULimit) collides with CI/CD-owned manifests: the next pipeline run reverts them. Change the mesh default in the SMCP (or spec.values.global.proxy.resources in OSSM 3) and roll workloads in a controlled window; keep annotations only for documented exceptions.
OSSM 2.6, namespace is in the SMMR, pods still 1/1. Most likely missing piece?
OSSM 2 injection is opt-in per pod; membership alone is not enough (DO328 troubleshooting tips).
OpenShift · OSSM 3

OSSM 3: operator, IstioCNI, Istio CR, revisions

oc apply -f - <<'EOF'
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata: {name: servicemeshoperator3, namespace: openshift-operators}
spec:
  channel: stable
  name: servicemeshoperator3
  source: redhat-operators
  sourceNamespace: openshift-marketplace
  installPlanApproval: Manual
EOF
oc new-project istio-system ; oc new-project istio-cni
apiVersion: sailoperator.io/v1
kind: IstioCNI                 # mandatory on OpenShift: no istio-init, no NET_ADMIN
metadata: {name: default}
spec:
  namespace: istio-cni
  version: v1.28.10            # keep aligned with the Istio CR
---
apiVersion: sailoperator.io/v1
kind: Istio
metadata: {name: default}      # cluster-scoped
spec:
  namespace: istio-system      # where istiod runs
  version: v1.28.10            # one of the versions supported by the installed operator
  updateStrategy:
    type: RevisionBased        # InPlace (default) | RevisionBased → revisions default-v1-28-10…
    inactiveRevisionDeletionGracePeriodSeconds: 30
  values:                      # == upstream istiod Helm values
    meshConfig:
      accessLogFile: /dev/stdout
      outboundTrafficPolicy: {mode: ALLOW_ANY}
      discoverySelectors:      # optional: only these namespaces are seen by this control plane
      - matchLabels: {istio-discovery: enabled}
      extensionProviders:
      - name: otel
        opentelemetry: {service: otel-collector.istio-system.svc.cluster.local, port: 4317}
    global:
      proxy:
        resources:
          requests: {cpu: 100m, memory: 128Mi}
          limits:   {cpu: 1300m, memory: 1Gi}
    pilot:
      resources: {requests: {cpu: 500m, memory: 2Gi}}
---
# keep "istio-injection=enabled" working across RevisionBased upgrades
apiVersion: sailoperator.io/v1
kind: IstioRevisionTag
metadata: {name: default}
spec:
  targetRef: {kind: Istio, name: default}
oc get istio default -o jsonpath='{.status.state}{"  active="}{.status.activeRevisionName}{"\n"}'
oc get istiorevision
oc get istiorevisiontag
# upgrade (RevisionBased): bump version → new revision appears, namespaces on the tag follow after restart
oc patch istio default --type merge -p '{"spec":{"version":"v1.28.11"}}'
oc get istiorevision -w
oc rollout restart deployment -n shop
istioctl proxy-status -i istio-system | awk '{print $1, $NF}' | sort -k2 | uniq -f1 -c

Gateways: gateway injection (replaces SMCP gateways)

# namespace must be injection-enabled: oc label ns shop-ingress istio-injection=enabled
apiVersion: v1
kind: ServiceAccount
metadata: {name: shop-ingressgateway, namespace: shop-ingress}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: shop-ingressgateway, namespace: shop-ingress}
spec:
  replicas: 2
  selector: {matchLabels: {istio: shop-ingressgateway}}
  template:
    metadata:
      annotations: {inject.istio.io/templates: gateway}
      labels: {istio: shop-ingressgateway, sidecar.istio.io/inject: "true"}
    spec:
      serviceAccountName: shop-ingressgateway
      containers:
      - name: istio-proxy
        image: auto                       # replaced by the injector
        ports: [{containerPort: 8443, name: https}]
        securityContext: {allowPrivilegeEscalation: false, capabilities: {drop: [ALL]}, runAsNonRoot: true}
        resources: {requests: {cpu: 100m, memory: 128Mi}, limits: {cpu: "2", memory: 1Gi}}
---
apiVersion: v1
kind: Service
metadata: {name: shop-ingressgateway, namespace: shop-ingress}
spec:
  type: ClusterIP                         # NodePort if an external VIP balances on node ports
  selector: {istio: shop-ingressgateway}
  ports: [{name: https, port: 443, targetPort: 8443}]
---
# gateway reads TLS Secrets of its own namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: {name: shop-ingressgateway-sds, namespace: shop-ingress}
rules: [{apiGroups: [""], resources: [secrets], verbs: [get, watch, list]}]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: {name: shop-ingressgateway-sds, namespace: shop-ingress}
roleRef: {apiGroup: rbac.authorization.k8s.io, kind: Role, name: shop-ingressgateway-sds}
subjects: [{kind: ServiceAccount, name: shop-ingressgateway}]
# explicit Route (no IOR in 3.x): TLS terminated by the Istio gateway → passthrough
oc create route passthrough shop --service=shop-ingressgateway --port=https --hostname=shop.apps.example.com -n shop-ingress
Gateway API on OpenShiftOn OCP ≥ 4.19 the Gateway API CRDs are installed and owned by the platform — never apply your own versions. On 4.18 install the standard CRDs matching your Istio release, then use gatewayClassName: istio (module 8).
Ambient (3.2+)Create IstioCNI and Istio with profile: ambient, add a ZTunnel CR (sailoperator.io/v1), label namespaces istio.io/dataplane-mode=ambient, add waypoints only where L7 is needed. Not supported on FIPS clusters.
OpenShift · migration

Migration runbook OSSM 2.6 → 3

Both operators coexist; workloads move namespace by namespace with a rolling restart, staying available. Red Hat publishes separate guides for ClusterWide and MultiTenant SMCPs — follow the one matching oc get smcp -o jsonpath='{..spec.mode}'.

Phase 0 — prepare on 2.6 (pre-migration checklist)

  • SMCP on the latest 2.6.z, ComponentsReady
  • IOR off: gateways.openshiftRoute.enabled: false, explicit Routes recreated and tested
  • Gateways moved out of the SMCP to gateway-injection Deployments (they keep working on 2.6 and can be re-pointed later)
  • Addons off: metrics → user-workload monitoring, tracing → Tempo + OpenTelemetry (meshConfig.extensionProviders + Telemetry), Kiali → standalone Kiali CR
  • manageNetworkPolicy: false and your own NetworkPolicies in place (they disappear otherwise)
  • No Maistra-only features left: federation, ServiceMeshExtension (→ WasmPlugin), deprecated fields
  • Inventory: oc get smmr default -n istio-system -o jsonpath='{.status.configuredMembers}'

Phase 1 — install 3.x next to 2.6

# operator + IstioCNI + Istio CR (see previous section). For MultiTenant SMCPs Red Hat places the 3.x
# control plane with discoverySelectors matching the members, e.g.:
#   discoverySelectors: [{matchLabels: {maistra.io/member-of: istio-system}}]   (or a custom tenant label)
oc get istio,istiorevision,istiocni
oc get pods -n istio-system      # istiod-basic-…(2.6) and istiod-…(3.x) side by side

Phase 2 — migrate one namespace

NS=shop ; REV=$(oc get istio default -o jsonpath='{.status.activeRevisionName}')
# 1) point the namespace to 3.x and tell the 2.6 injector to ignore it
oc label ns $NS istio.io/rev=$REV maistra.io/ignore-namespace="true" --overwrite=true
# 2) restart workloads (one by one for critical apps)
oc rollout restart deployment -n $NS
oc rollout status deployment -n $NS --timeout=10m
# 3) verify which control plane each proxy talks to
istioctl ps -i istio-system --revision basic | grep $NS      # should shrink to nothing
istioctl ps -i istio-system --revision $REV  | grep $NS      # should list every pod
# 4) smoke tests: traffic in Kiali, 5xx in metrics, mTLS still mutual_tls
Rollback for one namespaceoc label ns $NS istio.io/rev- maistra.io/ignore-namespace- then oc rollout restart deployment -n $NS → pods are injected by 2.6 again (namespace must still be a member).

Phase 3 — gateways, then cleanup

  1. Move each gateway Deployment namespace to the 3.x revision (same label pattern) and restart it; check Routes / VIPs.
  2. When istioctl ps --revision basic is empty: remove members from the SMMR, delete SMMR and SMCP.
  3. Uninstall servicemeshoperator; remove Maistra CRDs only after nothing references them.
  4. Remove maistra.io/* labels from namespaces.
Typical starting state"OSSM 2.6 SMCP basic with ~100+ member namespaces, OSSM 3 Istio default already Ready, no namespace carrying istio.io/rev yet" = Phase 1 done. Next: pick a low-risk namespace, run Phase 2, keep a rollback window, then batch the rest by application owner.
OpenShift · observability

Observability on OpenShift

for r in kiali grafana prometheus jaeger; do printf '%-11s ' $r; oc get route $r -n istio-system -o jsonpath='{.spec.host}{"\n"}' 2>/dev/null || echo '-'; done
GATEWAY_URL=$(oc get route istio-ingressgateway -n istio-system -o jsonpath='{.spec.host}')   # only with IOR on
oc get pods -n istio-system -l app=prometheus
# 1) user-workload monitoring
apiVersion: v1
kind: ConfigMap
metadata: {name: cluster-monitoring-config, namespace: openshift-monitoring}
data:
  config.yaml: |
    enableUserWorkload: true
---
# 2) scrape istiod
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata: {name: istiod-monitor, namespace: istio-system}
spec:
  targetLabels: [app]
  selector: {matchLabels: {istio: pilot}}
  endpoints: [{port: http-monitoring, interval: 30s}]
---
# 3) scrape sidecars — one PodMonitor PER mesh namespace (trimmed; full relabel set in Red Hat docs)
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata: {name: istio-proxies-monitor, namespace: shop}
spec:
  selector: {matchExpressions: [{key: istio-prometheus-ignore, operator: DoesNotExist}]}
  podMetricsEndpoints:
  - path: /stats/prometheus
    interval: 30s
    relabelings:
    - {action: keep, sourceLabels: [__meta_kubernetes_pod_container_name], regex: istio-proxy}
    - {action: keep, sourceLabels: [__meta_kubernetes_pod_annotationpresent_prometheus_io_scrape]}
    - action: replace
      regex: (\d+);((([0-9]+?)(\.|$)){4})
      replacement: '$2:$1'
      sourceLabels: [__meta_kubernetes_pod_annotation_prometheus_io_port, __meta_kubernetes_pod_ip]
      targetLabel: __address__
    - {action: replace, sourceLabels: [__meta_kubernetes_namespace], targetLabel: namespace}
---
# 4) enable Istio metrics through the Telemetry API
apiVersion: telemetry.istio.io/v1
kind: Telemetry
metadata: {name: enable-prometheus-metrics, namespace: istio-system}
spec:
  metrics: [{providers: [{name: prometheus}]}]
---
# 5) Kiali reading Thanos querier
apiVersion: kiali.io/v1alpha1
kind: Kiali
metadata: {name: kiali, namespace: istio-system}
spec:
  external_services:
    prometheus:
      auth: {type: bearer, use_kiali_token: true}
      thanos_proxy: {enabled: true, retention_period: 7d, scrape_interval: 30s}
      url: https://thanos-querier.openshift-monitoring.svc.cluster.local:9091
---
# 6) Service Mesh tab in the OpenShift console
apiVersion: kiali.io/v1alpha1
kind: OSSMConsole
metadata: {name: ossmconsole, namespace: openshift-operators}
oc adm policy add-cluster-role-to-user cluster-monitoring-view -z kiali-service-account -n istio-system
oc get route kiali -n istio-system -o jsonpath='{.spec.host}{"\n"}'
OpenShift · networking

Where the mesh meets OpenShift networking

Most production incidents sit at the boundary between Istio and the platform network. Read the path left to right.

external LB / F5VIP · iRule · SNI node :NodePortor Router (Routepassthrough) ingress gateway podsGateway hosts + credentialNameSecret in gateway namespace app pod (ns shop)Envoyapp Host rewritten?SNI must match filter chain404 NR = Host/VS mismatchNetworkPolicy must allow gw EGRESS — WHICH SOURCE IP LEAVES THE CLUSTER? app pod (ns shop)sidecar → direct egress gateway podns istio-system / gw ns external systemfirewall allow-list EgressIP of ns shop via egress gateway → EgressIP / node IP of the GATEWAY namespace applies
OVN-Kubernetes EgressIP matches the source pod's namespace/labels. Routing external calls through an egress gateway changes the source pod — and therefore the egress IP the firewall sees.
BoundaryWhat to knowCheck
Route → gatewayGateway terminates TLS → Route passthrough; Router terminates → reencrypt/edge to gateway HTTP port. IOR (2.6) creates Routes from Gateway hosts — disable before 3.x.oc get route -n istio-system -o wide
VIP → NodePortExternal LB balances on node ports of the gateway Service; the gateway must have a Gateway server for the Host the LB forwards.oc get svc -n istio-system -o wide
Gateway TLS secretcredentialName resolves in the gateway pods' namespace; strip trailing spaces/CR when pasting PEM.istioctl pc secret deploy/GW -n NS
NetworkPolicy2.6 creates istio-mesh/istio-expose-route-style policies in members; 3.x creates none. Allow: gateway → app ports, sidecar → istiod 15012, monitoring → 15020/15090.oc get netpol -n NS
EgressIPApplies to the pod that opens the connection (app or egress gateway). A missing SNAT for the EgressIP on the node's gateway router = OVN reconciliation issue, not mesh.oc get egressip -o wide
SCC / UIDSidecars run with the namespace's UID range under restricted-v2; CNI removes NET_ADMIN needs. No traceroute/ping (no NET_RAW) — use curl telnet://.oc get pod POD -o yaml | grep scc
OpenShift · field notes

Field notes from production

404 from istio-envoy, content-length 0, via an F5 VIPSame API answers 401 when called on the gateway with full path → it exists. HTTP NodePort with the expected Host returns the same 404 → route mismatch on the gateway (NR), not the backend. HTTPS NodePort with correct SNI works; wrong SNI → SSL_ERROR_SYSCALL (no filter chain in PASSTHROUGH). Fix lives where the Host/SNI is rewritten (iRule) or in Gateway/VS hosts.
Read timeout through a micro-gateway, direct call OKFrom the istio-proxy container, curl telnet://IP:443 times out with zero bytes and the same IP is blocked from the bastion and the node → firewall/route, not mesh. EgressIP allow-listed ≠ destination reachable.
503 on fe → mgw → be with EgressIPDeleting/recreating the EgressIP object fixes it, no SNAT entry for the EgressIP on the node's gateway router → OVN-K reconciliation problem. Kiali/Jaeger gaps were a side effect, not the cause.
Ingress gateway CrashLoop after a container runtime upgrade (non-OCP Kubernetes)Runtime default soft nofile dropped from 1048576 to 1024 → Envoy EMFILE under load. Fix at runtime level (default ulimits), then recreate pods. Newer Envoy raises its own soft limit, which hides the issue for gateways but not for the apps.
Liveness failures on meshed Java podsProbes are rewritten to pilot-agent (15020) — failures usually come from the app (GC pauses, DB degradation), not the sidecar. Check kubectl describe probe events + app GC before touching mesh config.
OpenShift · runbook

Team runbook: expose a microservice (FE/BE gateways)

  1. Ask Network to confirm the VIPs balance on the right node ports: FE <svc>.fe.<domain> → HTTPS 30002, BE <svc>.be.<domain> → HTTPS 30012. Network raises the VIP RFC.
  2. Certificate + private key always come from Network.
  3. Create the TLS secret in the gateway namespace (istio-system), name = gwc- + namespace.
  4. Namespace enrolled in the mesh (injection label / membership).
  5. Gateways are generated by the pipeline — the infra team only verifies; wrong Gateway → ask the pipeline owners to fix the template.
NS=r0000-myapp-be ; GWNS=istio-system
oc create secret tls gwc-$NS -n $GWNS --cert=<(sed 's/[[:space:]]*$//' tls.crt) --key=<(sed 's/[[:space:]]*$//' tls.key)
oc label ns $NS istio-injection=enabled --overwrite
oc get gateway,virtualservice -n $NS -o yaml | grep -E 'selector|istio:|number|protocol|credentialName|mode|hosts' -A1
istioctl pc secret deploy/istio-ingressgateway -n $GWNS | grep gwc-$NS
# reference shapes the pipeline must produce
# FE: selector istio: ingressgateway · 443 HTTPS · SIMPLE · credentialName gwc-<ns>
# BE: selector istio: backend-ingressgateway · 80 HTTP (or 443 HTTPS if the app requires https calls)
apiVersion: networking.istio.io/v1
kind: Gateway
metadata: {name: myapp-fe-gw, namespace: r0000-myapp-fe}
spec:
  selector: {istio: ingressgateway}
  servers:
  - port: {number: 443, name: https, protocol: HTTPS}
    hosts: ["myapp.fe.example.local"]
    tls: {mode: SIMPLE, credentialName: gwc-r0000-myapp-fe}
IV

GKE — Google Cloud Service Mesh

Cloud Service Mesh (CSM) = the former Anthos Service Mesh + Traffic Director. Same Istio APIs, but who runs the control plane, the CA and the telemetry backend changes. Two worlds: GKE on Google Cloud (managed) and GDC software-only for VMware (in-cluster) — both typically driven by Git through Config Sync.

GKE

Cloud Service Mesh: implementations & where each applies

GKE on Google Cloud · TRAFFIC_DIRECTOR (managed) Google-managed global control planereads Istio CRs from the cluster · pushes xDS · CSM CA certsno istiod pod in istio-system GKE cluster (fleet member) Envoymanaged DP gatewayinjected Cloud MonitoringCloud LoggingCloud TraceCSM console pages GDC (software only) for VMware · in-cluster user cluster (registered to fleet) istiod-asm-1XXX-Yinstalled by asmcli · revision label Envoysidecar gatewayinjected Mesh CA (default)or --ca citadel(istiod CA, own root) Cloud MonitoringLogging / Tracevia Connect / fleet
ImplementationWhere the control plane runsStatusUse it for
TRAFFIC_DIRECTORGoogle Cloud networking infra, globalCurrent default for new fleets (since 1 Jul 2024)GKE on Google Cloud, incl. Autopilot
Managed ISTIODGoogle-run istiod dedicated to one clusterdeprecated — must be modernised to TRAFFIC_DIRECTORLegacy fleets only
In-cluster ISTIOD on Google CloudYour cluster (asmcli)deprecated on Google Cloud — no in-place modernisation; migrate to managed on a new clusterLegacy
In-cluster off Google CloudYour cluster (asmcli --platform multicloud)Supported (1.28 → 1.31 lines; GDC VMware/bare metal K8s 1.34–1.36 for 1.31)GDC software-only for VMware / bare metal
Dates to keep an eye onGoogle announced the deprecation of both ISTIOD implementations on 28 Sep 2026; new managed-ISTIOD fleets stopped on 8 Sep 2024. GKE on AWS/Azure and CSM support for EKS/AKS end on 17 Mar 2027. Check implementation on every fleet you take over.
Infra K8s perimeter on CSMTypical demarcation: the infra team installs/upgrades the mesh, enrols namespaces, operates gateways and troubleshoots; it applies NetworkPolicy and Istio AuthorizationPolicy provided by application owners but does not design them, and does not own GitOps/Config Sync or application releases — see Config Sync for how to work inside that model.
GKE · managed

GKE on Google Cloud: provision managed CSM

Prerequisites: Workload Identity on cluster and node pools, cluster registered to a fleet, mesh.googleapis.com enabled. Commands below are single-line so they paste into a Windows cmd as well.

gcloud config set project FLEET_PROJECT_ID
gcloud services enable mesh.googleapis.com --project=FLEET_PROJECT_ID
gcloud container fleet mesh enable --project FLEET_PROJECT_ID
gcloud container clusters update CLUSTER_NAME --location CLUSTER_LOCATION --fleet-project FLEET_PROJECT_ID
gcloud container fleet memberships list --project FLEET_PROJECT_ID
gcloud container fleet mesh update --management automatic --memberships MEMBERSHIP_NAME --project FLEET_PROJECT_ID --location MEMBERSHIP_LOCATION
gcloud container fleet mesh describe --project FLEET_PROJECT_ID
gcloud container clusters get-credentials CLUSTER_NAME --location CLUSTER_LOCATION --project FLEET_PROJECT_ID
# expected in "fleet mesh describe" (after a few minutes)
membershipStates:
  projects/NNN/locations/LOCATION/memberships/MEMBERSHIP_NAME:
    servicemesh:
      controlPlaneManagement:
        implementation: TRAFFIC_DIRECTOR     # ISTIOD = deprecated, plan modernisation
        state: ACTIVE
      dataPlaneManagement:
        state: ACTIVE                        # managed data plane (automatic proxy upgrades)
    state: {code: OK}

Enrol workloads

kubectl create namespace shop
kubectl label namespace shop istio.io/rev- istio-injection=enabled --overwrite
kubectl rollout restart deployment -n shop
kubectl get pods -n shop
kubectl get controlplanerevision -n istio-system          # NAME = revision / release channel
kubectl get dataplanecontrols -o custom-columns=REV:.spec.revision,STATUS:.status.state
Managed data plane evicts podsTo upgrade proxies, MDP evicts pods running an older proxy (respecting PodDisruptionBudgets). Single-replica workloads without PDB will blip. Opt out per cluster, namespace or pod with mesh.cloud.google.com/proxy={"managed":"false"} on the ControlPlaneRevision / namespace / pod.

Gateways

No ingress gateway is deployed with the managed control plane. Two options:

Istio gateway (injection)

Same Deployment/Service/Role pattern as OSSM 3 (inject.istio.io/templates: gateway, image: auto) with a type: LoadBalancer Service (internal LB: annotation networking.gke.io/load-balancer-type: "Internal"). Configure with Istio Gateway + VirtualService.

GKE Gateway controller

Google-managed L7 load balancers via Gateway API classes such as gke-l7-global-external-managed, gke-l7-regional-external-managed, gke-l7-rilb. The LB sits outside the mesh; send traffic to an Istio gateway or to services with appropriate mTLS mode.

apiVersion: v1
kind: Service
metadata:
  name: shop-ingressgateway
  namespace: shop-ingress
  annotations:
    networking.gke.io/load-balancer-type: "Internal"   # omit for external
spec:
  type: LoadBalancer
  selector: {istio: shop-ingressgateway}
  ports:
  - {name: status-port, port: 15021, targetPort: 15021}
  - {name: https, port: 443, targetPort: 8443}
GKE · managed

TRAFFIC_DIRECTOR: what is different

FeatureOn TRAFFIC_DIRECTOR
IstioOperator APINot supported (no in-cluster install to customise)
EnvoyFilterPartial — only fields/extensions listed in "Dataplane Extensibility"
WasmPluginNot supported
Telemetry APISubset (access logs, traces)
Sidecar resourceSubset of fields
Egress gatewayOptional feature, must be enabled
Multi-clusterMulti-primary only, simple endpoint discovery
Kubernetes Ingress, hostNetwork: true podsNot supported in the mesh
DNS domain.cluster.local only
Config propagationPolicy changes slower than in-cluster istiod; endpoint changes fast
Jaeger / ZipkinOnly for (deprecated) ISTIOD; TD → Cloud Trace

Troubleshooting without istiod

# istioctl proxy-status / analyze need an istiod → not meaningful on TRAFFIC_DIRECTOR
# Envoy itself is still local: proxy-config and pilot-agent work
istioctl proxy-config cluster deploy/frontend -n shop --fqdn orders.shop.svc.cluster.local
istioctl proxy-config endpoint deploy/frontend -n shop --cluster "outbound|80||orders.shop.svc.cluster.local"
kubectl exec -n shop deploy/orders -c istio-proxy -- pilot-agent request GET certs
kubectl logs -n shop deploy/orders -c istio-proxy --tail=50
# mesh-level status and per-membership errors
gcloud container fleet mesh describe --project FLEET_PROJECT_ID --format="yaml(membershipStates)"
Validate YAML anywayRun a standalone istioctl analyze --use-kube=false --recursive manifests/ in CI: it catches typos and dangling references without a control plane.
GKE · observability

Observability on GKE

SignalBackendWhere
Service metrics (RPS, latency, errors)Cloud Monitoring — istio.io/service/server/request_count, …/server/response_latencies, …/client/request_countConsole → Cloud Service Mesh (topology, services, SLOs)
Access logsCloud Logging (optional feature)Logs Explorer
TracesCloud Trace (optional feature)Trace Explorer
mTLS status per edgeCSM security viewConsole → Cloud Service Mesh → Security
# Logs Explorer — sidecar/gateway access logs for one service (adapt resource labels)
resource.type="k8s_container"
resource.labels.namespace_name="shop"
resource.labels.container_name="istio-proxy"
jsonPayload.response_code>=500
# same from CLI (cmd-safe: double quotes outside, single inside)
gcloud logging read "resource.type=k8s_container AND resource.labels.namespace_name=shop AND resource.labels.container_name=istio-proxy" --limit 20 --freshness 1h --project FLEET_PROJECT_ID
GKE · GDC VMware

GDC for VMware: in-cluster CSM with asmcli

Off Google Cloud the control plane is an istiod inside the user cluster, installed and upgraded with asmcli. It behaves like upstream Istio with revisions; certificates come from Mesh CA by default.

asmcli is a bash toolRun it from a Linux admin workstation/bastion with kubectl, gcloud, jq, kpt and network access to Google APIs — not from a Windows cmd. Day-2 kubectl/istioctl work fine from the VDI.
curl -sSL https://storage.googleapis.com/csm-artifacts/asm/asmcli > asmcli && chmod +x asmcli
kubectl config use-context USER_CLUSTER_CONTEXT
./asmcli validate --fleet_id FLEET_PROJECT_ID --kubeconfig USER_CLUSTER_KUBECONFIG --output_dir ./asm-out --platform multicloud
./asmcli install --fleet_id FLEET_PROJECT_ID --kubeconfig USER_CLUSTER_KUBECONFIG --output_dir ./asm-out --platform multicloud --enable_all --ca mesh_ca
#   own CA instead of Mesh CA:
#   --ca citadel --ca_cert ca-cert.pem --ca_key ca-key.pem --root_cert root-cert.pem --cert_chain cert-chain.pem
#   mesh-config tweaks: --custom_overlay overlay.yaml   (IstioOperator overlay)
kubectl -n istio-system get pods -l app=istiod --show-labels      # istio.io/rev=asm-1XXX-Y
# overlay.yaml — example: lock egress, access logs, sidecar defaults
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
  meshConfig:
    accessLogFile: /dev/stdout
    outboundTrafficPolicy: {mode: REGISTRY_ONLY}
  values:
    global:
      proxy:
        resources:
          requests: {cpu: 100m, memory: 128Mi}
          limits:   {cpu: "1", memory: 1Gi}

Gateways (not installed by asmcli)

kubectl create namespace shop-ingress
kubectl label namespace shop-ingress istio-injection- istio.io/rev=asm-1XXX-Y --overwrite
kubectl apply -n shop-ingress -f ./asm-out/samples/gateways/istio-ingressgateway
kubectl get svc,pods -n shop-ingress

Canary upgrade (new revision side by side)

# 1) run the newer asmcli install → new istiod-asm-1NEW-Z next to the old one
./asmcli install --fleet_id FLEET_PROJECT_ID --kubeconfig USER_CLUSTER_KUBECONFIG --output_dir ./asm-new --platform multicloud --enable_all --ca mesh_ca
# 2) move namespaces (gateways first or last — be consistent) and restart
kubectl label namespace shop istio-injection- istio.io/rev=asm-1NEW-Z --overwrite
kubectl rollout restart deployment -n shop
./asm-new/istio-*/bin/istioctl proxy-status | grep -c asm-1XXX-Y        # old revision count → 0
# 3) remove the old control plane only when nothing points to it
kubectl delete Service,Deployment,HorizontalPodAutoscaler,PodDisruptionBudget istiod-asm-1XXX-Y -n istio-system --ignore-not-found=true
kubectl delete IstioOperator installed-state-asm-1XXX-Y -n istio-system
On a GDC for VMware user cluster, how is the CSM control plane delivered?
Managed implementations are for GKE on Google Cloud; off Google Cloud CSM is in-cluster.
GKE · troubleshooting

GKE troubleshooting matrix

SymptomWhere to look
fleet mesh describe shows error / state not ACTIVEWorkload Identity, fleet membership, mesh.googleapis.com, IAM of the fleet service agent
Pods 1/1 after labellingLabel correct for the implementation (istio-injection vs istio.io/rev), conflicting both labels, rollout restart
Pods restarting by themselves during the dayManaged data plane upgrades (evictions) — check events, PDBs, opt-out annotation
Ingress 502/503 behind a GKE LBLB health check to gateway status port 15021; firewall rules for health-check ranges; BackendConfig/HealthCheckPolicy
Feature works on GDC but not on GKE managedSupported-features page for TRAFFIC_DIRECTOR (EnvoyFilter, WasmPlugin, Telemetry subset)
Policy change takes long to apply (managed)Expected propagation on TRAFFIC_DIRECTOR; verify in Envoy with proxy-config
GDC: istiod NotReady / injection webhook timeoutsistiod logs, webhook Service endpoints, node ↔ control-plane reachability on 15017/443, Mesh CA reachability
Certificate errors after CA changeMixed roots during Mesh CA ↔ citadel migration — follow the CA migration procedure, restart workloads in waves
GKE · Config Sync

GitOps with Config Sync: how the mesh is managed

On fleets managed by Google tooling, mesh configuration (namespaces with injection labels, PeerAuthentication, AuthorizationPolicy, VirtualService, gateways…) is usually not applied with kubectl: it is declared in Git (or an OCI image) and continuously reconciled by Config Sync. Same mechanism on GKE and on GDC for VMware — both are fleet members.

Git / OCI · root repoNamespaces + injection labelsmesh-wide PeerAuthenticationTelemetry · gatewaysRepoSync + RoleBindingowner: platform / GitOps team Git · namespace repoVirtualService · DestinationRuleAuthorizationPolicy (app-owned)owner: application team Fleet: config-managementapply-spec.yaml → RootSync specgcloud … fleet config-management config-management-systemroot-reconciler (RootSync)ns-reconciler-shop (RepoSync)server-side apply every ~15sself-healing: always onpreventDrift webhook: optional kube-apiserverIstio validation webhookrejects bad CRs → KNV2009 TRAFFIC_DIRECTOR / istiodreads Istio CRs → xDS Envoy sidecars / gatewaysverify with istioctl proxy-config kubectl edit / labeldeclared fields reverted Git is the source of truth: a manual change either gets overwritten (declared field) or becomes silent drift (field not in Git).
ObjectScopeWhat it does
RootSynccluster · ns config-management-systemSyncs the root repo with cluster-admin-like rights (cluster-scoped objects, any namespace). With fleet install, root-sync is generated from apply-spec.yaml — change it via gcloud, not kubectl.
RepoSyncone namespaceSyncs a namespace repo with the rights you bind to its reconciler SA (ns-reconciler-<ns>). Typical for app-owned VS/DR/AuthorizationPolicy.
ResourceGroupper syncInventory of applied objects + their status.
Self-healingalways onServer-side apply restores declared fields; undeclared fields are left alone.
preventDriftopt-inAdmission webhook admission-webhook.configsync.gke.io rejects changes to managed objects. Google advises against it in large clusters (memory/OOM).

Fleet-level install / inspection

# apply-spec.yaml (per membership or as fleet default)
applySpecVersion: 1
spec:
  configSync:
    enabled: true
    sourceType: git                # git | oci
    sourceFormat: unstructured
    syncRepo: https://GIT_HOST/platform/mesh-config.git
    syncBranch: main               # syncRev (tag/hash) wins if set
    policyDir: clusters/prod
    secretType: token              # none | ssh | token | gcpserviceaccount | …
    preventDrift: false
    stopSyncing: false             # true = pause syncing on this cluster (break-glass)
gcloud beta container fleet config-management status --project FLEET_PROJECT_ID
gcloud beta container fleet config-management fetch-for-apply --membership MEMBERSHIP_NAME --project FLEET_PROJECT_ID > current-spec.yaml
gcloud beta container fleet config-management apply --membership MEMBERSHIP_NAME --config apply-spec.yaml --project FLEET_PROJECT_ID
gcloud beta container fleet config-management enable --fleet-default-member-config apply-spec.yaml --project FLEET_PROJECT_ID
gcloud beta container fleet config-management apply --origin FLEET --membership MEMBERSHIP_NAME --project FLEET_PROJECT_ID
kubectl create secret generic git-creds -n config-management-system --from-literal=username=GIT_USER --from-literal=token=GIT_TOKEN

Typical repo layout for the mesh (unstructured)

mesh-config/                                  # RootSync (policyDir: clusters/prod)
└── clusters/prod/
    ├── namespaces/
    │   ├── shop.yaml                         # Namespace + istio-injection label (+ MDP opt-out)
    │   └── shop-ingress.yaml
    ├── istio-system/
    │   ├── peerauthentication-default.yaml   # mesh-wide STRICT
    │   └── telemetry-mesh-default.yaml
    ├── shop-ingress/                         # gateway Deployment/Service/Role/RoleBinding
    └── reposync/
        ├── shop-reposync.yaml
        └── shop-reconciler-rolebinding.yaml

apps-shop/                                    # namespace repo → RepoSync in ns shop
└── prod/
    ├── allow-nothing.yaml
    ├── authorizationpolicy-orders.yaml
    ├── destinationrule-orders.yaml
    └── virtualservice-orders.yaml
# clusters/prod/namespaces/shop.yaml — injection is declared, not kubectl-labelled
apiVersion: v1
kind: Namespace
metadata:
  name: shop
  labels:
    istio-injection: enabled
  annotations:
    mesh.cloud.google.com/proxy: '{"managed":"false"}'   # optional: opt out of MDP evictions
---
# clusters/prod/reposync/shop-reposync.yaml
apiVersion: configsync.gke.io/v1beta1
kind: RepoSync
metadata: {name: repo-sync, namespace: shop}
spec:
  sourceFormat: unstructured
  sourceType: git
  git:
    repo: https://GIT_HOST/apps/apps-shop.git
    branch: main
    dir: prod
    auth: token
    secretRef: {name: git-creds}          # Secret in ns shop
---
# what the namespace reconciler may touch: Istio APIs only
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata: {name: istio-config-editor}
rules:
- apiGroups: [networking.istio.io, security.istio.io, telemetry.istio.io]
  resources: ["*"]
  verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: {name: syncs-istio-config, namespace: shop}
roleRef: {apiGroup: rbac.authorization.k8s.io, kind: ClusterRole, name: istio-config-editor}
subjects:
- {kind: ServiceAccount, name: ns-reconciler-shop, namespace: config-management-system}

Mesh-specific GitOps rules

RuleWhy
Injection labels, MDP opt-out, istio.io/rev live in the Namespace manifestA kubectl label is either reverted (field declared) or silent drift (not declared) — a revision migration done by hand is lost or invisible
Validate before merge: nomos vet + istioctl analyze --use-kube=falseThe Istio validation webhook rejects bad CRs at apply → sync stuck in KNV2009 for the whole batch
DestinationRule before VirtualService: config.kubernetes.io/depends-onAvoid a window where a VS references a missing subset (503 NC)
Review deletions; guard critical objects with client.lifecycle.config.k8s.io/deletion: detachRemoving allow-nothing from Git deletes it → the namespace opens. Removing an ALLOW → 403 storm
Never declare fields owned by controllers (gateway replicas under HPA)Config Sync and the HPA fight: replicas flap at every sync
No TLS keys in Git — gateway Secrets come from a secret managercredentialName only needs the Secret to exist in the gateway namespace
Leave Google-managed objects out of Git (ControlPlaneRevision, managed webhooks, istio-system internals)Adopting them makes Config Sync fight the managed control plane
Prod vs DR differences via ClusterSelector / separate policyDirSame repo, site-specific hosts, VIPs, egress
# ordering + deletion guard on mesh objects
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: orders
  namespace: shop
  annotations:
    config.kubernetes.io/depends-on: networking.istio.io/namespaces/shop/DestinationRule/orders
spec: {}   # …
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: allow-nothing
  namespace: shop
  annotations:
    client.lifecycle.config.k8s.io/deletion: detach   # stays if removed from Git by mistake
spec: {}
# CI gate (pre-merge)
nomos vet --source-format unstructured --no-api-server-check --path clusters/prod
istioctl analyze --use-kube=false --recursive clusters/prod apps-shop/prod
GKE · Config Sync · day 2

Operating a Config Sync-managed mesh

PerimeterThe GitOps repos and RootSync/RepoSync belong to the platform/GitOps owner, VS/DR/AuthorizationPolicy content to the application teams. The infra K8s team reads sync status, troubleshoots the mesh and proposes changes through pull requests — never by kubectl apply on managed objects.

Is this object managed by Config Sync?

kubectl get authorizationpolicy orders-allow-frontend -n shop -o jsonpath='{.metadata.annotations.configmanagement\.gke\.io/managed}{"  "}{.metadata.annotations.configsync\.gke\.io/manager}{"  "}{.metadata.annotations.configmanagement\.gke\.io/source-path}{"  "}{.metadata.annotations.configmanagement\.gke\.io/token}{"\n"}'
# enabled  :root | shop_repo-sync  clusters/prod/... | apps/...  <commit sha>
kubectl get ns,peerauthentication,authorizationpolicy,virtualservice,destinationrule,gateway -A -l app.kubernetes.io/managed-by=configmanagement.gke.io
kubectl get authorizationpolicy -n shop -o custom-columns=NAME:.metadata.name,MANAGER:.metadata.annotations.configsync\.gke\.io/manager,SOURCE:.metadata.annotations.configmanagement\.gke\.io/source-path
Annotation / labelMeaning
configmanagement.gke.io/managed: enabledReconciled by Config Sync (disabled in Git = stop managing, object left in place)
configsync.gke.io/manager:root = RootSync · NS_REPOSYNC = a namespace repo
configmanagement.gke.io/source-pathFile in the repo that declares it — open the PR there
configmanagement.gke.io/tokenCommit currently applied
configsync.gke.io/declared-fieldsFields Config Sync owns (and will restore)
app.kubernetes.io/managed-by: configmanagement.gke.ioLabel on every managed object (use it as selector)

Sync status

nomos status --contexts CONTEXT_NAME                     # per cluster: SYNCED commit, errors, resources
kubectl get rootsync,reposync -A
kubectl get rootsync root-sync -n config-management-system -o jsonpath='{.status.source.commit}{"  synced="}{.status.sync.commit}{"\n"}'
kubectl get rootsync root-sync -n config-management-system -o jsonpath='{.status.source.errors}{.status.rendering.errors}{.status.sync.errors}{"\n"}'
kubectl get reposync repo-sync -n shop -o yaml | yq '.status'
kubectl get resourcegroup root-sync -n config-management-system      # RECONCILING / STALLED
kubectl get resourcegroup repo-sync -n shop
kubectl logs -n config-management-system deploy/root-reconciler -c reconciler --tail=100
kubectl logs -n config-management-system deploy/ns-reconciler-shop -c reconciler --tail=100
nomos bugreport                                          # support bundle

Change workflow for the mesh

1. Find the source file      → annotation configmanagement.gke.io/source-path
2. Branch + change YAML      → run nomos vet + istioctl analyze locally / in CI
3. PR to the repo owner      → attach expected effect (e.g. "403 → 200 for sa/frontend")
4. After merge               → nomos status: SYNCED to the new commit, no errors
5. Verify on the data plane  → istioctl proxy-config / x describe pod / access log

Break-glass (agree it with the GitOps owner first)

  1. Preferred: revert or hot-fix commit in Git — Config Sync applies it within one sync period (~15 s by default).
  2. Git unavailable / fix cannot wait: set stopSyncing: true for that membership, apply the fix with kubectl, back-port it to Git, then stopSyncing: false. While paused nothing is self-healed on that cluster.
  3. Single object: commit configmanagement.gke.io/managed: disabled on it, then operate it by hand until it is re-adopted.
gcloud beta container fleet config-management fetch-for-apply --membership MEMBERSHIP_NAME --project FLEET_PROJECT_ID > spec.yaml
# edit spec.yaml: spec.configSync.stopSyncing: true
gcloud beta container fleet config-management apply --membership MEMBERSHIP_NAME --config spec.yaml --project FLEET_PROJECT_ID
nomos status --contexts CONTEXT_NAME

Symptoms

SymptomCauseCheck / fix
Manual label / policy change "comes back" after secondsSelf-healing on a declared fielddeclared-fields, change it in Git
kubectl edit denied by admission-webhook.configsync.gke.iopreventDrift: trueChange in Git (or break-glass)
Sync stuck, KNV2009 with validation.istio.io deniedInvalid Istio CR in the commitFix YAML; add istioctl analyze to CI
KNV2002 / unknown kind (e.g. HTTPRoute)CRD missing on that clusterInstall CRDs first (root repo) or depends-on
RepoSync error: forbidden on authorizationpoliciesNamespace reconciler lacks RBAC for Istio APIsRoleBinding to ns-reconciler-<ns>
Object declared in two reposRoot and namespace repo both own itSingle owner per object; check configsync.gke.io/manager
Gateway replicas flappingreplicas declared + HPARemove replicas from Git
Traffic opened up after a PRallow-nothing deleted from GitRestore; add deletion: detach
Namespace not injected after migration to a new revisionLabel changed by hand, reverted by GitUpdate the Namespace manifest
On a Config Sync-managed cluster you run kubectl label ns shop istio-injection-. A minute later the label is back. Why?
Self-healing restores declared fields with server-side apply. Change the Namespace manifest via PR.
A PR removes allow-nothing.yaml from the namespace repo. What happens after sync?
Objects removed from the source of truth are pruned unless annotated client.lifecycle.config.k8s.io/deletion: detach.