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.
UPPER_CASE or <angle>.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.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.
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 —
frontendmay callorders,reportingmay 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.
| Area | What it does | Main resources |
|---|---|---|
| Security | Automatic mTLS, workload identity, authorization, JWT | PeerAuthentication, AuthorizationPolicy, RequestAuthentication |
| Traffic management | Routing, canary, mirroring, timeout, retry, circuit breaking | VirtualService, DestinationRule, Gateway, ServiceEntry, Sidecar |
| Observability | Metrics, distributed tracing, access logs | Telemetry + Prometheus, Grafana, Kiali, Jaeger/Tempo |
| Extensibility | External authz, plugins, raw Envoy config | AuthorizationPolicy 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).
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:5So 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| Where | Which port |
|---|---|
| URL the client calls | Service port (80) |
VirtualService → destination.port | Service port (80) |
AuthorizationPolicy → to.operation.ports, PeerAuthentication → portLevelMtls | Container 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 onlocalhost: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.
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"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.
orders (port 80 → targetPort 8080). Which port goes in to.operation.ports?Architecture: control plane & data plane
istiod binary.| Component | Role |
|---|---|
| istiod | Reads 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. |
| Envoy | L4/L7 proxy that intercepts all pod traffic and applies routing, mTLS, policy, telemetry. |
| istio-proxy | The injected container: pilot-agent (bootstraps Envoy, fetches certs via SDS, serves health) + Envoy. |
| Gateways | Standalone Envoy deployments at the mesh edge (ingress/egress). Not sidecars. |
Ports you will meet
| Port | Where | Use |
|---|---|---|
15001 | sidecar | Outbound capture (virtual listener) |
15006 | sidecar | Inbound capture |
15000 | sidecar | Envoy admin (localhost only) — config_dump, clusters, stats |
15004 | sidecar | Debug port (pilot-agent ↔ istiod) |
15020 | sidecar | Merged Prometheus metrics (app + Envoy + agent), health |
15021 | sidecar / gateway | Readiness /healthz/ready |
15090 | sidecar | Envoy Prometheus /stats/prometheus |
15010 / 15012 | istiod | xDS plaintext (legacy) / xDS + CA over mTLS |
15014 | istiod | Control plane metrics |
15017 → 443 | istiod | Webhooks (injection, validation) |
15008 | ztunnel / waypoint | HBONE tunnel (ambient) |
Sidecar mode vs ambient mode
| Sidecar | Ambient | |
|---|---|---|
| Proxy | One Envoy per pod | ztunnel per node (L4 + mTLS) + optional waypoint Envoy per namespace/service (L7) |
| Enrol | Injection label + pod restart | istio.io/dataplane-mode=ambient on the namespace, no restart |
| Cost | CPU/RAM per pod | Shared per node; L7 only where needed |
| Status | Mature everywhere | GA 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.
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| Profile | Content |
|---|---|
default | istiod + ingress gateway — recommended start |
minimal | istiod only; gateways deployed separately (best practice in production) |
demo | Everything on, high tracing — labs only |
ambient | istiod + istio-cni + ztunnel |
openshift | Upstream 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}'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-initsets 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.
shop with istio-injection=enabled. Pods are still 1/1. Why?kubectl rollout restart deployment -n shop.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 as | Envoy behaviour |
|---|---|
http / http2 / grpc | HTTP connection manager: L7 routing, retries, HTTP metrics, L7 authz |
tcp | TCP proxy: only L4 routing (by port/SNI), L4 authz (principals, ports, IP) |
tls / https | Passthrough by SNI — Envoy cannot see paths (encrypted by the app) |
| not declared | Protocol sniffing: guessed on the first bytes — works, but fragile (server-first protocols like MySQL/SMTP break) |
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.
methods: ["GET"] on a Service port named db (no appProtocol) blocks every request. Most likely cause?Traffic management: VirtualService & DestinationRule
A plain Service spreads requests across all selected pods. Istio adds two resources that work as a pair.
| Resource | Question it answers |
|---|---|
| VirtualService | Where does this request go? — ordered routing rules (match → route), weights, timeouts, retries, faults, mirroring |
| DestinationRule | How is traffic treated once the destination is chosen? — subsets (versions), load balancing, connection pool, outlier detection, client TLS |
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 field | Meaning |
|---|---|
hosts | Name(s) the rule applies to (FQDN for mesh services, external hostnames at gateways) |
gateways | Absent = mesh sidecars only. [ns/gw] = that gateway only. Add mesh to cover both |
http[].match | uri (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 / tls | Equivalents for opaque TCP and SNI-based routing |
exportTo | Visibility (. = 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.
subset: v3, which is not in the DestinationRule. Result?istioctl analyze flags it (IST0101 ReferencedResourceNotFound).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: 10Typical 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 -cHeader-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}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)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.
Ingress & egress: Gateway, ServiceEntry, Gateway API
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/ordersserver: 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).
gatewaysdecides whether a VS applies to the mesh, to a gateway, or to both (mesh).- With
REGISTRY_ONLYevery 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.
gateways: [shop/shop-gateway] only. Does it affect calls from frontend to orders inside the mesh?gateways a VS applies to sidecars (mesh); with a list, only to what is listed.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-errorretryOn | Retries when |
|---|---|
5xx | Any 5xx or connection failure / reset |
gateway-error | 502, 503, 504 |
connect-failure | TCP connect fails |
reset | Upstream resets the connection mid-request |
retriable-4xx | 409 |
retriable-status-codes + 503 | Explicit list of codes |
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 × perTryTimeoutmust fit in it. UO= breaker tripped (fast fail).UH= no healthy endpoint (all ejected or not ready).
timeout: 2s, attempts: 3, perTryTimeout: 1s; upstream always takes 1.5s. What does the client see?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 policiesistiod (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.
| Mode | Inbound accepted | frontend (sidecar) | legacy-app (no sidecar) |
|---|---|---|---|
PERMISSIVE (default) | mTLS and plaintext | ✅ mTLS | ✅ plaintext |
STRICT | mTLS only | ✅ mTLS | ❌ connection reset (curl 000) |
DISABLE | plaintext | avoid | |
UNSET | inherit 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 scraperkubectl 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 -cMigration path (never flip the mesh to STRICT in one shot)
- Mesh stays PERMISSIVE.
- Inject sidecars in every workload that must communicate.
- Verify with metrics (
connection_security_policy="mutual_tls") and Kiali (padlock on edges). - Move to STRICT one namespace at a time.
- Finally STRICT at mesh level (root namespace).
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.
AuthorizationPolicy, dry-run, vs NetworkPolicy
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— JWTiss/subipBlocks— direct peer IPremoteIpBlocks— original client IP (X-Forwarded-For, ifnumTrustedProxiesset)
to.operation / when
methods,paths(exact,/api/*prefix,*.jsonsuffix) — HTTP onlyports— container porthosts— Host headerwhen: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"]}}]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 # restoreAuthorizationPolicy vs Kubernetes NetworkPolicy
| NetworkPolicy | AuthorizationPolicy | |
|---|---|---|
| Enforced by | CNI (OVN-K, Calico…) | Envoy on the receiver |
| Layer | L3/L4 — IP, pod/ns selectors, ports | L4 + L7 — identity, method, path, header, JWT claims |
| Caller identity | labels → IPs | cryptographic SPIFFE (mTLS) |
| Default | allow all | allow all |
| Model | additive allows only | ALLOW / DENY (wins) / CUSTOM / AUDIT |
| Needs mesh | no | yes (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.
allow-nothing plus an ALLOW for frontend on GET /api/*. frontend sends POST /api/orders. Result?orders (ns shop) be created?End-user authentication with JWT
| Identity | Represents | Resource |
|---|---|---|
| Peer | the calling workload | PeerAuthentication (mTLS) |
| Request | the end user / client holding a token | RequestAuthentication (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"]| Request | RequestAuthentication 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 withjwks:.
Observability: metrics, Kiali, tracing, access logs
| Tool | Question |
|---|---|
| Prometheus | Collects proxy + control-plane metrics |
| Grafana | How are services doing over time? |
| Kiali | Who talks to whom? mTLS padlocks, health, config validation, wizards |
| Jaeger / Tempo | Which path did this request take, where was time lost? |
| Access log | What 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 stdouttraceparent/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
reporterin PromQL. - Observability UIs expose sensitive topology: never publish them without auth.
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| Symptom | Probable cause | Check |
|---|---|---|
Pod 1/1 | Injection off / pod older than label | ns labels, rollout restart |
Pod 1/2, proxy not ready | No config/cert from istiod | proxy-status, istio-proxy + istiod logs, NetworkPolicy to 15012 |
403 RBAC: access denied | AuthorizationPolicy | x describe pod, rbac:debug |
401 Jwt verification fails | Bad token / unreachable JWKS | issuer, jwksUri, exp |
Reset / curl 000 | Plaintext client → STRICT | client sidecar? PeerAuthentication |
404 istio-envoy len 0 · NR | No route for Host/path | VS/Gateway hosts, proxy-config route |
503 UF | Connect failure / TLS mismatch | DR tls, target port, NetworkPolicy |
503 UH | No healthy endpoint / empty subset | pod labels vs subsets, proxy-config endpoint |
503 UO | Circuit breaker | DR connectionPool |
503 NC | Cluster (subset) not found | DR applied? subset name |
504 UT | Route timeout | VS timeout vs app latency |
502 / BlackHoleCluster | REGISTRY_ONLY, no ServiceEntry | proxy-config cluster | grep BlackHole |
| L7 policy ignored | Port seen as TCP | appProtocol/port name |
Envoy CrashLoop, EMFILE | Too few file descriptors (nofile 1024) | cat /proc/1/limits in istio-proxy, container runtime defaults |
Remember
istioctl analyzeandistioctl x describe podfirst.- Distinguish 403 (authz), 401 (JWT), reset (mTLS).
- A TCP timeout from inside the
istio-proxycontainer towards an external IP is a network/firewall problem, not a mesh problem.
503 UH right after a canary VS was applied. First check?Best practices & certification path
Identity & mTLS
- Dedicated ServiceAccount per app, never
default - PERMISSIVE → verify → STRICT per namespace → mesh
- No
tls: DISABLEin in-mesh DestinationRules
Authorization
allow-nothingin 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
versionlabels ↔ subsets - DestinationRule before VirtualService
- Explicit timeouts on critical paths; retries only idempotent
- Circuit breaking on services that can saturate
appProtocolon every port
Operations
istioctl analyzein 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 mTLS | PeerAuthentication |
| Decide who can call a service | AuthorizationPolicy |
| Validate / require JWT | RequestAuthentication (+ AuthorizationPolicy) |
| Route, canary, mirror, timeout, retry, fault | VirtualService (or HTTPRoute) |
| Subsets, LB, circuit breaker, client TLS | DestinationRule |
| Expose a service | Gateway + VirtualService (or Gateway API) |
| Declare an external service | ServiceEntry |
| Limit what a proxy sees / egress scope | Sidecar |
| Tracing, metrics, access logs | Telemetry |
Certification path
| DO328 chapter | Here |
|---|---|
| 1 Introducing OSSM · 2 Installing | Modules 1–4 · OSSM 2.6 · OSSM 3 |
| 3 Observing a Service Mesh | Module 13 · OSSM observability |
| 4 Controlling Service Traffic | Modules 6, 8 |
| 5 Releasing Applications | Module 7 |
| 6 Chaos Testing · 7 Resilient Services | Module 9 |
| 8 Securing OSSM | Modules 10–12 |
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.
Rosetta stone: same task, five flavours
| Task | Istio upstream | OSSM 2.6 Maistra | OSSM 3.x Sail | GKE · CSM managed TD | GDC VMware · CSM in-cluster asmcli |
|---|---|---|---|---|---|
| Install | istioctl install / Helm | OLM servicemeshoperator | OLM servicemeshoperator3 | gcloud container fleet mesh update --management automatic | asmcli install --platform multicloud |
| Control plane object | IstioOperator file / Helm values | ServiceMeshControlPlane (maistra.io/v2) | Istio, IstioRevision, IstioCNI (sailoperator.io/v1, cluster-scoped) | fleet feature + ControlPlaneRevision | istiod Deployment istiod-asm-1XXX-Y |
| Control plane runs | istiod in cluster | istiod-basic in istio-system | istiod in spec.namespace | Google-managed, outside the cluster | istiod in cluster |
| Mesh membership | all ns (or discoverySelectors) | ServiceMeshMemberRoll / ServiceMeshMember | discoverySelectors (optional) | whole fleet membership | all ns (or discoverySelectors) |
| Injection | ns istio-injection=enabled | member ns + pod sidecar.istio.io/inject: "true" | ns istio-injection=enabled or istio.io/rev | ns istio-injection=enabled | ns istio.io/rev=asm-1XXX-Y |
| Traffic capture | istio-init (or CNI) | CNI (always) | IstioCNI (required on OCP) | istio-cni (managed) | istio-init / CNI |
| Ingress | gateway chart / Gateway API | SMCP gateways + IOR Routes | gateway injection / Gateway API + explicit Route | gateway injection / GKE Gateway API | gateway injection |
| CA | istiod | istiod (or cert-manager istio-csr) | istiod (or cert-manager istio-csr) | CSM CA (Mesh CA) / CA Service | Mesh CA (default) / istiod CA |
| Metrics | Prometheus | SMCP-bundled Prometheus (or UWM) | OCP user-workload monitoring | Cloud Monitoring | Cloud Monitoring |
| Tracing | any OTel/Jaeger | Jaeger / Tempo | Tempo + OpenTelemetry | Cloud Trace | Cloud Trace |
| UI | Kiali | Kiali (SMCP addon) | Kiali CR + OSSM console plugin | CSM page in Cloud console | CSM page in Cloud console |
| Sidecar default resources | values.global.proxy.resources | spec.proxy.runtime.container.resources | spec.values.global.proxy.resources | pod annotations (managed defaults) | values.global.proxy.resources via overlay |
| Egress lock | meshConfig.outboundTrafficPolicy | spec.proxy.networking.trafficControl.outbound.policy | spec.values.meshConfig.outboundTrafficPolicy | mesh ConfigMap in istio-system (istio-asm-managed*) — check supported fields | meshConfig overlay |
| Upgrades | revisions / in-place | bump spec.version (in place) | InPlace or RevisionBased | release channel, managed data plane | new revision + relabel |
| GitOps | Argo CD / Flux | OpenShift 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) | ✅ |
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 -loc 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-systemcontrolPlaneManagement.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/revEnrol 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 shopspec.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 shopkubectl 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 shopistioctl: 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-forwardoutbound|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).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/memoryConnectivity 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 -datesEnvoy response flags
In access logs (after the response code) and in the response_flags metric label.
| Flag | Meaning | Typical Istio cause |
|---|---|---|
- | No anomaly | — |
NR | No route configured | Host not in any VS/Gateway, wrong port, missing mesh in VS gateways |
NC | No cluster found | Subset referenced but DR missing |
UH | No healthy upstream | No ready pods, subset labels match nothing, all ejected |
UF | Upstream connection failure | mTLS mismatch, wrong targetPort, NetworkPolicy, app not listening |
UO | Upstream overflow | Circuit breaker connectionPool limits |
URX | Retry / connect attempts exceeded | Every retry failed |
UT | Upstream request timeout | VS timeout expired |
UC | Upstream connection termination | App closed keep-alive connection (idle timeout mismatch) |
UR | Upstream remote reset | App/peer reset the stream |
DC | Downstream connection termination | Client gave up (its own timeout) |
LR | Local reset | Envoy reset (draining, overload) |
DI / FI | Delay / fault injected | VS fault |
RL | Rate limited | Local/global rate limit |
UAEX | Unauthorized external service | AuthorizationPolicy CUSTOM (ext_authz) denied |
DPE / UPE | Downstream / upstream protocol error | Wrong appProtocol, HTTP to a TLS port |
SI | Stream idle timeout | Long-poll / streaming without data |
DF | DNS resolution failed | ServiceEntry resolution: DNS host not resolvable |
Labels & annotations
| Key | On | Effect |
|---|---|---|
istio-injection=enabled|disabled | Namespace (label) | Default-revision injection |
istio.io/rev=REV | Namespace / 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, proxyCPULimit | Pod annotation | Sidecar CPU request / limit (e.g. "1300m") |
sidecar.istio.io/proxyMemory, proxyMemoryLimit | Pod annotation | Sidecar memory request / limit |
proxy.istio.io/config | Pod annotation | ProxyConfig JSON: {"holdApplicationUntilProxyStarts":true,"terminationDrainDuration":"30s","concurrency":2} |
sidecar.istio.io/logLevel | Pod annotation | Envoy log level at start |
traffic.sidecar.istio.io/excludeOutboundIPRanges | Pod annotation | CIDRs that bypass Envoy (no policy, no metrics) |
traffic.sidecar.istio.io/includeOutboundIPRanges | Pod annotation | Only these CIDRs are captured |
traffic.sidecar.istio.io/excludeOutboundPorts / excludeInboundPorts | Pod annotation | Ports that bypass Envoy (e.g. DB, JMX) |
sidecar.istio.io/rewriteAppHTTPProbers: "false" | Pod annotation | Disable probe rewrite |
inject.istio.io/templates: gateway | Pod annotation | Gateway injection (Deployment becomes a gateway) |
networking.istio.io/exportTo | Service annotation | Visibility of the Service to other namespaces |
istio.io/dry-run: "true" | AuthorizationPolicy annotation | Shadow mode |
istio.io/dataplane-mode=ambient · istio.io/use-waypoint | Namespace / service (label) | Ambient enrolment / waypoint binding |
maistra.io/member-of OSSM 2 | Namespace (set by operator) | Shows which SMCP owns the ns |
maistra.io/expose-route: "true" OSSM 2 | Pod template (label) | Allows OpenShift Router traffic through the operator-managed NetworkPolicies |
maistra.io/ignore-namespace: "true" OSSM 2 | Namespace (label) | 2.6 injector ignores the ns — used during migration to 3 |
mesh.cloud.google.com/proxy CSM | ControlPlaneRevision / ns / pod (annotation) | {"managed":"false"} disables managed data plane upgrades |
configmanagement.gke.io/managed · configsync.gke.io/manager Config Sync | Any managed object (annotation) | Object reconciled from Git (enabled/disabled) and by which RootSync/RepoSync — day-2 |
client.lifecycle.config.k8s.io/deletion: detach Config Sync | Object in Git (annotation) | Not pruned if removed from the repo |
config.kubernetes.io/depends-on Config Sync | Object in Git (annotation) | Apply order (e.g. VS after its DR) |
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 -APromQL 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]))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.
OSSM 2.6 vs OSSM 3 at a glance
| Topic | OSSM 2.6 final 2.x | OSSM 3.x Sail operator |
|---|---|---|
| Base | Maistra fork of Istio (2.6 ≈ Istio 1.20) | Clean upstream Istio, rebuilt by Red Hat (OpenSSL/FIPS kept) |
| Operator / package | servicemeshoperator | servicemeshoperator3 — both can run in parallel |
| Control plane API | ServiceMeshControlPlane (maistra.io/v2), Maistra-specific schema | Istio (sailoperator.io/v1), cluster-scoped, spec.values = upstream Helm values |
| Other CRs | ServiceMeshMemberRoll, ServiceMeshMember, ServiceMeshExtension (old) | IstioRevision, IstioRevisionTag, IstioCNI, ZTunnel (ambient) |
| Tenancy default | MultiTenant (members listed in SMMR) or ClusterWide | Cluster-wide; scope with discoverySelectors; multiple control planes optional |
| Injection | Member ns + per-pod sidecar.istio.io/inject: "true" | Upstream: ns istio-injection / istio.io/rev |
| Gateways | Declared inside the SMCP (gateways.ingress/egress/additionalIngress) | Gateway injection Deployments or Gateway API — owned by you |
| Routes | IOR creates Routes automatically from Gateway hosts | No automatic Routes — create them explicitly |
| NetworkPolicies | Operator creates them in members (manageNetworkPolicy) | None created — design your own |
| Observability | Bundled Prometheus, Grafana, Jaeger, Kiali as SMCP addons | Operator manages only Istio: user-workload monitoring, Tempo + OpenTelemetry, Kiali CR + console plugin |
| istioctl | Partially supported | Supported |
| Ambient | No | GA from 3.2 (OCP 4.18+); not on FIPS clusters |
| Upgrade | Change spec.version → in place | updateStrategy: InPlace or RevisionBased (canary) |
| Federation | Maistra federation | Not available — use upstream multi-cluster |
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 servicemeshA 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 basicsidecar.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 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-cniapiVersion: 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 -cGateways: 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-ingressgatewayClassName: istio (module 8).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.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: falseand 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 sidePhase 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_tlsoc 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
- Move each gateway Deployment namespace to the 3.x revision (same label pattern) and restart it; check Routes / VIPs.
- When
istioctl ps --revision basicis empty: remove members from the SMMR, delete SMMR and SMCP. - Uninstall
servicemeshoperator; remove Maistra CRDs only after nothing references them. - Remove
maistra.io/*labels from namespaces.
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.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"}'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.
| Boundary | What to know | Check |
|---|---|---|
| Route → gateway | Gateway 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 → NodePort | External 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 secret | credentialName resolves in the gateway pods' namespace; strip trailing spaces/CR when pasting PEM. | istioctl pc secret deploy/GW -n NS |
| NetworkPolicy | 2.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 |
| EgressIP | Applies 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 / UID | Sidecars 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 |
Field notes from production
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.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.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.kubectl describe probe events + app GC before touching mesh config.Team runbook: expose a microservice (FE/BE gateways)
- Ask Network to confirm the VIPs balance on the right node ports: FE
<svc>.fe.<domain>→ HTTPS30002, BE<svc>.be.<domain>→ HTTPS30012. Network raises the VIP RFC. - Certificate + private key always come from Network.
- Create the TLS secret in the gateway namespace (
istio-system), name =gwc-+ namespace. - Namespace enrolled in the mesh (injection label / membership).
- 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}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.
Cloud Service Mesh: implementations & where each applies
| Implementation | Where the control plane runs | Status | Use it for |
|---|---|---|---|
TRAFFIC_DIRECTOR | Google Cloud networking infra, global | Current default for new fleets (since 1 Jul 2024) | GKE on Google Cloud, incl. Autopilot |
Managed ISTIOD | Google-run istiod dedicated to one cluster | deprecated — must be modernised to TRAFFIC_DIRECTOR | Legacy fleets only |
In-cluster ISTIOD on Google Cloud | Your cluster (asmcli) | deprecated on Google Cloud — no in-place modernisation; migrate to managed on a new cluster | Legacy |
| In-cluster off Google Cloud | Your 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 |
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.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.statemesh.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}TRAFFIC_DIRECTOR: what is different
| Feature | On TRAFFIC_DIRECTOR |
|---|---|
IstioOperator API | Not supported (no in-cluster install to customise) |
EnvoyFilter | Partial — only fields/extensions listed in "Dataplane Extensibility" |
WasmPlugin | Not supported |
Telemetry API | Subset (access logs, traces) |
Sidecar resource | Subset of fields |
| Egress gateway | Optional feature, must be enabled |
| Multi-cluster | Multi-primary only, simple endpoint discovery |
Kubernetes Ingress, hostNetwork: true pods | Not supported in the mesh |
| DNS domain | .cluster.local only |
| Config propagation | Policy changes slower than in-cluster istiod; endpoint changes fast |
| Jaeger / Zipkin | Only 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)"istioctl analyze --use-kube=false --recursive manifests/ in CI: it catches typos and dangling references without a control plane.Observability on GKE
| Signal | Backend | Where |
|---|---|---|
| Service metrics (RPS, latency, errors) | Cloud Monitoring — istio.io/service/server/request_count, …/server/response_latencies, …/client/request_count | Console → Cloud Service Mesh (topology, services, SLOs) |
| Access logs | Cloud Logging (optional feature) | Logs Explorer |
| Traces | Cloud Trace (optional feature) | Trace Explorer |
| mTLS status per edge | CSM security view | Console → 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_IDGDC 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.
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-ingressCanary 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-systemGKE troubleshooting matrix
| Symptom | Where to look |
|---|---|
fleet mesh describe shows error / state not ACTIVE | Workload Identity, fleet membership, mesh.googleapis.com, IAM of the fleet service agent |
| Pods 1/1 after labelling | Label correct for the implementation (istio-injection vs istio.io/rev), conflicting both labels, rollout restart |
| Pods restarting by themselves during the day | Managed data plane upgrades (evictions) — check events, PDBs, opt-out annotation |
| Ingress 502/503 behind a GKE LB | LB health check to gateway status port 15021; firewall rules for health-check ranges; BackendConfig/HealthCheckPolicy |
| Feature works on GDC but not on GKE managed | Supported-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 timeouts | istiod logs, webhook Service endpoints, node ↔ control-plane reachability on 15017/443, Mesh CA reachability |
| Certificate errors after CA change | Mixed roots during Mesh CA ↔ citadel migration — follow the CA migration procedure, restart workloads in waves |
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.
| Object | Scope | What it does |
|---|---|---|
RootSync | cluster · ns config-management-system | Syncs 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. |
RepoSync | one namespace | Syncs a namespace repo with the rights you bind to its reconciler SA (ns-reconciler-<ns>). Typical for app-owned VS/DR/AuthorizationPolicy. |
ResourceGroup | per sync | Inventory of applied objects + their status. |
| Self-healing | always on | Server-side apply restores declared fields; undeclared fields are left alone. |
preventDrift | opt-in | Admission 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_TOKENTypical 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
| Rule | Why |
|---|---|
Injection labels, MDP opt-out, istio.io/rev live in the Namespace manifest | A 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=false | The Istio validation webhook rejects bad CRs at apply → sync stuck in KNV2009 for the whole batch |
DestinationRule before VirtualService: config.kubernetes.io/depends-on | Avoid a window where a VS references a missing subset (503 NC) |
Review deletions; guard critical objects with client.lifecycle.config.k8s.io/deletion: detach | Removing 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 manager | credentialName 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 policyDir | Same 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/prodOperating a Config Sync-managed mesh
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 / label | Meaning |
|---|---|
configmanagement.gke.io/managed: enabled | Reconciled 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-path | File in the repo that declares it — open the PR there |
configmanagement.gke.io/token | Commit currently applied |
configsync.gke.io/declared-fields | Fields Config Sync owns (and will restore) |
app.kubernetes.io/managed-by: configmanagement.gke.io | Label 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 bundleChange 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 logBreak-glass (agree it with the GitOps owner first)
- Preferred: revert or hot-fix commit in Git — Config Sync applies it within one sync period (~15 s by default).
- Git unavailable / fix cannot wait: set
stopSyncing: truefor that membership, apply the fix withkubectl, back-port it to Git, thenstopSyncing: false. While paused nothing is self-healed on that cluster. - Single object: commit
configmanagement.gke.io/managed: disabledon 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_NAMESymptoms
| Symptom | Cause | Check / fix |
|---|---|---|
| Manual label / policy change "comes back" after seconds | Self-healing on a declared field | declared-fields, change it in Git |
kubectl edit denied by admission-webhook.configsync.gke.io | preventDrift: true | Change in Git (or break-glass) |
Sync stuck, KNV2009 with validation.istio.io denied | Invalid Istio CR in the commit | Fix YAML; add istioctl analyze to CI |
KNV2002 / unknown kind (e.g. HTTPRoute) | CRD missing on that cluster | Install CRDs first (root repo) or depends-on |
RepoSync error: forbidden on authorizationpolicies | Namespace reconciler lacks RBAC for Istio APIs | RoleBinding to ns-reconciler-<ns> |
| Object declared in two repos | Root and namespace repo both own it | Single owner per object; check configsync.gke.io/manager |
| Gateway replicas flapping | replicas declared + HPA | Remove replicas from Git |
| Traffic opened up after a PR | allow-nothing deleted from Git | Restore; add deletion: detach |
| Namespace not injected after migration to a new revision | Label changed by hand, reverted by Git | Update the Namespace manifest |
kubectl label ns shop istio-injection-. A minute later the label is back. Why?allow-nothing.yaml from the namespace repo. What happens after sync?client.lifecycle.config.k8s.io/deletion: detach.