Tools · Istio

Istio

The ICA blueprint puts 60% of its weight on two things — 35% on Traffic Management, 25% on Securing Workloads — and this page turns both into running configuration. Istio is the CNCF-graduated, Envoy-based service mesh that takes encryption, identity, retries, timeouts and traffic shifting out of every application and makes them things the platform provides instead: a control plane called istiod that computes what every proxy should do, and a data plane — an Envoy sidecar riding in every pod, or a lighter ztunnel-plus-waypoint ambient mesh — that carries the bytes. Below: both data-plane shapes, the VirtualService/DestinationRule pair and the security trio you'll write for real, the istioctl verbs worth muscle memory, the failure modes that catch a happy-path-only read, and where Istio sits against Linkerd, Cilium's own mesh, and no mesh at all. This assumes the Kubernetes fluency the five-exam Kubestronaut foundation already gave you, from the sibling Kubernetes course; why any mesh looks the way it does has its own page.

☺ Explain it like I'm 10

Picture Mission Control running an enormous airfield where hundreds of small aircraft take off every second. One office in the tower never touches a single plane — it just radios every pilot their exact flight path, and hands out tamper-proof ID badges that ground crews check before anyone reaches a runway. The airfield can run two ways. In the busy season, every aircraft gets its own personal co-pilot riding along, handling the radio and the badge check for that one plane alone. In the quiet season, a smaller ground crew stationed at each runway handles the badge checks for everyone taking off from there instead, and only calls in a specialist co-pilot for the rare flight on an unusually complicated route. Istio is the tower plus both flight crews — the office in the middle never carries a single passenger, it only ever radios instructions.

🦉Your host for this topic: Professor Owl — he draws the reference architecture before anyone argues about one field in a VirtualService, and he hosts the ICA blueprint and the service mesh architecture deep-dive for the same reason he hosts this page.

Architecture: istiod, and two ways to carry traffic

☺ Like you're 10: One tower office computes every flight path and prints every badge; the planes themselves carry either their own dedicated co-pilot, or share a ground crew stationed at the runway instead.

Modern Istio collapsed what used to be three separate components — Pilot, Citadel, Galley — into one binary, istiod, running as a single Deployment in the istio-system namespace. It does four jobs and none of them involve a single byte of your traffic. It watches the Kubernetes API for Services, EndpointSlices, Pods and Istio's own custom resources. It translates that state into Envoy configuration — clusters, listeners, routes, endpoints, secrets — and pushes it to every proxy over the xDS protocol. It is the mesh's certificate authority, issuing every workload a short-lived X.509 certificate whose identity is a SPIFFE URI of the shape spiffe://cluster.local/ns/<namespace>/sa/<serviceaccount>, minted from something Kubernetes already vouches for: the pod's own ServiceAccount. And it runs the admission webhooks that validate your CRs and inject sidecars.

Sidecar mode: a co-pilot on every plane

In the classic model, an istio-proxy container — Envoy — is injected into every pod, and an init container programs iptables so inbound traffic redirects to port 15006 and outbound to 15001. The application never knows. Injection is opt-in per namespace, by label — either the simple istio-injection=enabled or, on a revisioned install, istio.io/rev=<revision>, which is what lets two control-plane versions run side by side while you migrate namespace by namespace. Injection only happens at pod creation, so existing pods are never retrofitted — they need a rollout restart before any of this applies to them.

Ambient mode: a shared ground crew at the runway

Ambient splits the mesh by layer instead of by pod. A per-node DaemonSet called ztunnel handles L4 only — mTLS, identity and basic telemetry for every ambient-enrolled pod on that node, tunnelling over HBONE (HTTP/2 CONNECT over mTLS). Only if a namespace or workload needs L7 features — HTTP routing rules, header matching, method/path-level AuthorizationPolicy — do you add a waypoint, an ordinary Envoy deployment that traffic for that scope is routed through. You enrol a namespace with the label istio.io/dataplane-mode=ambient, and — unlike sidecar mode — no restart is required, because there's no per-pod proxy to inject. The payoff is real: no per-pod memory tax, no pod restarts to upgrade the mesh, no sidecar lifecycle problems. You pay for L7 only where you actually deploy a waypoint.

Kubernetes API Services · Istio CRs istiod xDS push · CA (SPIFFE) · webhooks single control plane — never on the data path watch Sidecar mode Pod · checkout app + envoy :15001 / :15006 Pod · payments app + envoy :15001 / :15006 mTLS, envoy → envoy proxy per pod — cost and a hop, everywhere, whether it's used or not Ambient mode Pod · payments app only — no proxy ztunnel per node · L4 mTLS over HBONE waypoint optional · per namespace · L7 only if L7 needed no per-pod proxy — pay for L7 only where a waypoint exists xDS xDS istiod computes; the proxies enforce. Kill the control plane and existing connections keep flowing — new config just stops arriving.
◆ Key idea

Two sentences carry almost this entire domain. istiod computes what every proxy should do and pushes it out — it never touches a data byte itself, so a control-plane outage stalls new config, not traffic already flowing. And sidecar mode charges every pod for a proxy whether that pod ever needs one, while ambient mode charges only for L4 by default and lets you pay for L7 exactly where you add a waypoint. Nearly everything else on this page is one of those two ideas in a different shape.

One more thing every proxy does for free, sidecar or waypoint alike: because it already terminates and re-issues each hop, it emits uniform access logs and golden-signal metrics without asking the app for anything, and it propagates whatever trace headers the app already forwards — B3 or the W3C traceparent format — so a single request stitches into one distributed trace spanning every hop it crosses. That pipeline is what feeds Prometheus, Grafana, and the OpenTelemetry Collector the moment a namespace is enrolled, and it's exactly what Dot means below by "I didn't instrument a single span."

🦆 Dot's-eye view

"Honestly, from where I sit nothing changed. I didn't add a TLS library, I didn't write retry code, I didn't instrument a single span. One day the platform team labelled our namespace, and suddenly our dashboard had per-route latency on it and the security review stopped asking whether our internal calls were encrypted. The only thing I ever write myself is a VirtualService when I want a canary."

The VirtualService and DestinationRule pair you'll write constantly

☺ Like you're 10: One file decides where a request goes and how many copies get a shot; a second file names the delivery teams that first file is actually talking about.

A DestinationRule defines subsets — named groups of pods picked out by label — plus a trafficPolicy: load-balancer choice, connection pools, and outlierDetection, which is Istio's circuit breaker. A VirtualService then decides where a request actually goes: match on host, header, path or method, then route, split by weight, retry, time out, or inject a fault. Weights within one route must sum to 100, and match blocks are evaluated in order — first match wins.

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: payments
  namespace: prod
spec:
  host: payments.prod.svc.cluster.local
  subsets:
    - name: v1
      labels: { version: v1 }           # matches POD labels, not the Service's
    - name: v2
      labels: { version: v2 }
  trafficPolicy:
    loadBalancer: { simple: LEAST_REQUEST }
    connectionPool:                     # ---- circuit breaking ----
      tcp:  { maxConnections: 100 }
      http:
        http2MaxRequests: 200
        maxRequestsPerConnection: 10
    outlierDetection:                   # ---- eject unhealthy endpoints ----
      consecutive5xxErrors: 5
      interval: 10s
      baseEjectionTime: 30s
      maxEjectionPercent: 50
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: payments
  namespace: prod
spec:
  hosts: ["payments"]                   # short name = in-mesh (east-west) traffic
  http:
    - match:                            # first match wins — order is meaning
        - headers:
            x-canary: { exact: "true" }
      route:
        - destination: { host: payments, subset: v2 }
    - fault:                            # ---- fault injection ----
        delay: { percentage: { value: 5 }, fixedDelay: 3s }
      timeout: 2s                       # ---- timeout ----
      retries:                          # ---- retries ----
        attempts: 3
        perTryTimeout: 500ms
        retryOn: 5xx,reset,connect-failure
      route:                            # ---- traffic shifting: must total 100 ----
        - destination: { host: payments, subset: v1 }
          weight: 90
        - destination: { host: payments, subset: v2 }
          weight: 10
Request GET /api/cart VirtualService · payments match: header x-canary=true → route to subset v2 default route → v1 weight 90 / v2 weight 10 first match wins DestinationRule · payments subsets: v1 {version:v1} · v2 {version:v2} Pods · v1 label version=v1 gets weight 90 (default) Pods · v2 label version=v2 weight 10, or 100% if canary header set
⚠ A subset your DestinationRule never defines is a 503

The commonest self-inflicted wound in this whole domain: a VirtualService routes to subset: v2, but nobody ever added a v2 entry to the matching DestinationRule. Istio has nowhere to send the request, and the caller sees a bare 503 with no clue why. istioctl analyze — covered below — catches this before it ships; it is the single fastest habit to build.

The front door and reaching outward: Gateway, the Gateway API, and ServiceEntry

Istio's own Gateway binds a listener to an ingress or egress gateway deployment; a VirtualService then attaches routes to it with the gateways field — omit that field and the rule applies only to in-mesh traffic, the classic "why is my route being ignored?" The project's own stated direction is the vendor-neutral Gateway API instead (Gateway + HTTPRoute, gatewayClassName: istio), which new platforms should prefer. Reaching out of the mesh works the opposite way: a ServiceEntry registers an external host so policy can apply to it at all — pair it with outboundTrafficPolicy.mode: REGISTRY_ONLY, below, so anything not declared this way simply cannot leave.

apiVersion: networking.istio.io/v1
kind: ServiceEntry                      # teach the mesh about the outside world
metadata:
  name: stripe
  namespace: prod
spec:
  hosts: ["api.stripe.com"]
  ports: [{ number: 443, name: https, protocol: HTTPS }]
  resolution: DNS
  location: MESH_EXTERNAL

Every weight in that first code block is also the mechanism under a mesh-driven canary — Argo Rollouts watches metrics and rewrites exactly that weight field automatically, which is the separation of deploy from release this course keeps coming back to.

Locking it down: PeerAuthentication, RequestAuthentication, AuthorizationPolicy

☺ Like you're 10: One rule asks whether the plane itself is who it claims to be; a second asks whether the passenger's ticket is genuine; a third decides who's actually allowed to land.

Three resources, three different questions, and the exam rewards knowing exactly which one answers which. PeerAuthentication asks "must the caller present a mesh certificate?" — workload authentication, with mode: STRICT as the zero-trust setting; placed in istio-system with no selector it applies mesh-wide. RequestAuthentication asks "is this JWT valid, and who issued it?" — end-user authentication, which on its own rejects only invalid tokens, never absent ones. AuthorizationPolicy asks "is this caller allowed to do this?" — matching on SPIFFE principals (never an IP), requestPrincipals, HTTP method and path.

apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: prod             # in istio-system with no selector = mesh-wide
spec:
  mtls:
    mode: STRICT               # STRICT | PERMISSIVE | DISABLE | UNSET
---
apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata:
  name: jwt-issuer
  namespace: prod
spec:
  selector:
    matchLabels: { app: payments }
  jwtRules:
    - issuer: "https://accounts.example.com"
      jwksUri: "https://accounts.example.com/.well-known/jwks.json"
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: deny-all
  namespace: prod
spec: {}                       # ALLOW with zero rules => nothing is permitted
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: checkout-may-call-payments
  namespace: prod
spec:
  selector:
    matchLabels: { app: payments }
  action: ALLOW
  rules:
    - from:
        # keys INSIDE one source are ANDed: identity AND a valid token, together
        - source:
            principals: ["cluster.local/ns/prod/sa/checkout"]
            requestPrincipals: ["https://accounts.example.com/*"]
      to:
        - operation:
            methods: ["GET", "POST"]
            paths: ["/api/cart*"]

Two evaluation rules matter more than any single field. DENY policies are evaluated before ALLOW policies, so a matching DENY wins outright over any ALLOW anywhere in the namespace. And the moment any ALLOW policy selects a workload, that workload becomes default-deny for everything else — which is exactly why the empty-spec policy above locks a namespace down with nothing else written. action: AUDIT logs what would have been denied without denying it, and is the correct way to roll a new policy out — PERMISSIVE before STRICT, AUDIT before DENY, every time.

⚠ Splitting one source into two is a silent AND → OR flip

Inside one - source: block, every key listed is ANDed — the example above requires the checkout ServiceAccount identity and a valid token. Write principals and requestPrincipals as two separate - source: list entries instead — an easy typo, and a common one — and the meaning silently flips to OR: "this identity, or any valid token," which is a hole, not a policy. Separate entries under from, to, and rules genuinely are ORed; only the keys inside one block are ANDed. Read that boundary twice before you trust a policy you wrote under time pressure.

AuthorizationPolicy governs which already-admitted requests may flow between live workloads; it's a different plane entirely from what Kyverno governs — which manifests get admitted to the cluster in the first place — and the two are complementary halves of the same zero-trust argument, not substitutes for each other. Kyverno, next in this section, picks up exactly that other half.

Installing, upgrading, and enrolling namespaces with istioctl

☺ Like you're 10: One command checks the airfield can even take Istio; a couple more install it, put a sticker on a namespace, and move a whole runway to a newer tower one step at a time.

istioctl install is the fast route and takes built-in profilesdefault, demo, minimal, ambient — customized with --set or an IstioOperator-shaped file. Helm is the composable route, and what most GitOps setups use instead: three charts, in order — base (CRDs and cluster roles), istiod, then gateway per ingress or egress gateway — which means the whole datapath config becomes one reviewable values file, deliverable through Argo CD or Flux like anything else in this course's GitOps section. Ambient adds the cni and ztunnel charts on top.

The upgrade path names two strategies worth separating in one sentence each. In-place replaces the running control plane directly — simple, all-or-nothing. Canary installs a second istiod under a revision, moves namespaces onto it one at a time by changing the istio.io/rev label and restarting their pods, and rolls back by relabelling; a revision tag — a stable alias like prod-stable — moves a whole fleet at once by repointing the tag instead.

# Will this cluster even take Istio? Always run this first.
istioctl x precheck

# --- Path A: istioctl, with a profile ---
istioctl install --set profile=default -y
istioctl install --set profile=ambient -y          # ztunnel + CNI node agent

# --- Path B: Helm, three charts, in this order ---
helm repo add istio https://istio-release.storage.googleapis.com/charts
helm install istio-base istio/base -n istio-system --create-namespace
helm install istiod     istio/istiod -n istio-system --wait
helm install istio-ingressgateway istio/gateway -n istio-ingress --create-namespace

# --- Enrol a namespace — pick ONE, they are mutually exclusive ---
kubectl label namespace prod istio-injection=enabled        # sidecar, default rev
kubectl label namespace prod istio.io/rev=1-27-0             # sidecar, revisioned
kubectl label namespace prod istio.io/dataplane-mode=ambient # ambient — no restart needed
kubectl rollout restart deployment -n prod        # sidecar pods aren't retrofitted

# --- Canary upgrade: a second control plane, moved namespace by namespace ---
istioctl install --set revision=1-27-0 -y
istioctl tag set prod-stable --revision 1-27-0     # stable alias, moves a whole fleet
kubectl label ns prod istio.io/rev=1-27-0 --overwrite
kubectl rollout restart deployment -n prod
istioctl uninstall --revision 1-26-0 -y            # retire the old control plane

istioctl upgrade -y            # in-place: replaces everything, all at once
istioctl version                # client, istiod, and every data-plane proxy
⚠ If both injection labels are present, the plain one wins

Applying istio.io/rev=<revision> to a namespace that still carries istio-injection=enabled does not move it to the revisioned control plane — the plain label takes priority, silently. Remove it first: kubectl label ns prod istio-injection-. And after any canary upgrade, istioctl proxy-status tells you exactly which proxies still talk to the old control plane before you retire it.

✎ Try it

On a throwaway kind cluster: istioctl install --set profile=demo -y, label a namespace for injection, deploy a two-version payments app and restart it. Write the DestinationRule + VirtualService pair above and curl in a loop until you see roughly a 90/10 split — then run istioctl proxy-config route <pod> -o json and find your own weights sitting inside real Envoy config, the moment the mesh stops feeling like magic. Apply the empty-spec AuthorizationPolicy and watch every call return RBAC: access denied, then add the ALLOW policy and watch it come back. Finally, delete the v2 subset from the DestinationRule but leave the VirtualService pointing at it, and run istioctl analyze before you read the error — that gap between your guess and the tool's answer is most of the Troubleshooting domain. The mesh-namespace drill on this course is a guided version of the security half of this exercise.

Troubleshooting: the ladder from config to control plane to proxy

☺ Like you're 10: Instead of arguing about whether the tower or a pilot made the mistake, you just ask each one in turn — and one of them always answers with the exact reason.

The curriculum names the failure surface in a genuinely useful order — configuration, then the control plane, then the data plane — and working it in that order finds most failures fast. istioctl analyze is a real linter, and it catches missing subsets, conflicting policies and unreferenced gateways before they become a 503. For the control plane, ask whether istiod is healthy and whether its config actually reached the proxies. For the data plane, stop guessing and ask one proxy what it believes.

# 1. CONFIGURATION — lint before you blame anything else
istioctl analyze -n prod
istioctl analyze virtualservice.yaml destinationrule.yaml   # even before committing

# 2. CONTROL PLANE — is istiod healthy, and did the push land?
kubectl -n istio-system get pods
istioctl proxy-status              # alias: ps. CDS/LDS/EDS/RDS all SYNCED?
                                    # STALE = push stuck · NOT SENT = nothing to send

# 3. DATA PLANE — what does ONE proxy actually believe?
istioctl proxy-config route    payments-7d9f-abcde.prod --name 8080 -o json
istioctl proxy-config cluster  payments-7d9f-abcde.prod
istioctl proxy-config endpoint payments-7d9f-abcde.prod \
  --cluster "outbound|8080|v2|payments.prod.svc.cluster.local"
istioctl proxy-config secret   payments-7d9f-abcde.prod   # did the cert arrive?

# Plain-English summary: mTLS mode, policies and routes affecting one pod
istioctl x describe pod payments-7d9f-abcde.prod

# Turn up Envoy logging, then read what the proxy recorded
istioctl proxy-config log payments-7d9f-abcde.prod --level rbac:debug,router:debug
kubectl logs payments-7d9f-abcde -n prod -c istio-proxy

Learn the symptoms that map to a single cause, because recognition beats deduction under time pressure. RBAC: access denied is an AuthorizationPolicy, not a network problem. A 503 right after a routing change is almost always a subset the VirtualService names and no DestinationRule defines. Failures right after enabling STRICT mean something outside the mesh is calling in. And a pod with no sidecar at all is almost always a namespace label that got applied after the pod already existed.

Gotchas and failure modes

☺ Like you're 10: Most surprises come from forgetting who's still outside the mesh, a co-pilot that starts or stops at the wrong moment, or handing every messenger the whole flight schedule when they only needed three numbers.

STRICT mTLS cuts off everything unmeshed

The classic outage: flip mtls.mode: STRICT mesh-wide on day one and you sever every client the mesh doesn't know about — a monitoring scraper living outside the namespace, a legacy VM, a CronJob pod in a namespace nobody remembered to label, a cloud load balancer health check hitting a pod IP directly. Istio rewrites plain httpGet kubelet probes to route through the pilot-agent on port 15020 so they're exempt, but not every probe style qualifies that way, and headless Services and raw databases often need their ports explicitly named (http, grpc, tcp-mysql) or an appProtocol set before protocol detection stops guessing wrong. The safe sequence never changes: install PERMISSIVE, enrol and restart everything, confirm with istioctl x describe pod that traffic is genuinely mTLS, and only then flip to STRICT — one namespace at a time, never mesh-wide first.

⚠ Four things STRICT usually breaks first

1. A Prometheus instance scraping a meshed namespace from outside the mesh. 2. Kubelet probes that aren't a plain httpGet. 3. Headless Services and databases where protocol detection guesses wrong. 4. Anything reached by pod IP instead of a Service name. Roll out with PERMISSIVE and AUDIT first — they exist for exactly this.

Sidecar lifecycle races

Historically the ugliest bug class in the sidecar model. If the app container starts before istio-proxy is ready, its first outbound calls fail — a migration Job that runs at boot dies mysteriously. In the other direction, a Job or CronJob pod never reaches Completed, because the app exits but the sidecar keeps running forever. The old workarounds — holdApplicationUntilProxyStarts, or the app calling the proxy's /quitquitquit endpoint on exit — are largely superseded now by Kubernetes native sidecars: initContainers entries carrying restartPolicy: Always, on by default since Kubernetes 1.29 and GA in 1.33, which Istio uses to guarantee start-before, stop-after ordering. Ambient mode sidesteps the whole category — there is no sidecar to race against.

The cost of a proxy per pod, and config that grows with the mesh

Every sidecar costs memory, CPU, and a hop of latency in each direction — real money and real p99 at a few hundred pods. Worse, by default istiod tells every proxy about every service in the mesh, so each proxy's config grows with the size of the whole estate, and pushes get slow as the mesh does. The two levers are a Sidecar resource, restricting a namespace's proxies to the hosts they actually call, and exportTo/discoverySelectors to scope visibility further. If none of that appeals, that's precisely the argument for ambient mode, or for a lighter mesh entirely.

apiVersion: networking.istio.io/v1
kind: Sidecar
metadata:
  name: default
  namespace: prod          # applies to every proxy in this namespace
spec:
  egress:
    - hosts:
        - "./*"            # everything in my own namespace
        - "istio-system/*" # plus the control plane
        - "payments/*"     # plus the one namespace I genuinely call
  outboundTrafficPolicy:
    mode: REGISTRY_ONLY    # block egress to anything without a ServiceEntry

Istio vs the alternatives

☺ Like you're 10: A few different flight-crew arrangements exist. They trade off how much gear rides on every plane, how much the tower has to run, and how much you have to learn to operate one.

OptionData plane & policy reachOperational weightChoose it when…
IstioEnvoy sidecar, or ztunnel+waypoint (ambient); deepest L7 routing, JWT auth, per-subset traffic policyHigh — the price of the feature set; mature multi-cluster identityYou need advanced routing, JWT-aware authorization, or a mature multi-cluster mesh identity, and can staff the operations
LinkerdPurpose-built Rust micro-proxy, sidecar only; automatic mTLS, deliberately fewer L7 knobsLow — closer to "it just runs" than any other option hereYou want mTLS and golden-signal metrics with the smallest possible operations bill
Cilium (mesh mode)eBPF datapath plus a shared per-node Envoy for L7; Gateway API-centricMedium, and close to free if Cilium is already your CNICilium already carries your packets and a second datapath isn't worth running
No meshKubernetes NetworkPolicy plus per-app TLS and client-library retriesLowest — but every one of those concerns becomes your application's jobA handful of services with simple needs; a mesh would be a distributed system you don't need yet

The honest version of that decision: like Cilium's eBPF commitment, adopting Istio is strategic, not a checkbox — it puts real weight on your platform team's plate in exchange for guarantees no app team could deliver by discipline alone, and it is genuinely hard to walk back once workloads depend on JWT-aware authorization and per-subset traffic policy. That's most of why the ICA exists as its own specialist credential, examining Istio by name the way the CCA examines Cilium by name, rather than a vendor-neutral spec the way CGOA does. Two other pages cover the same ground from different angles: Service Mesh Architecture compares Istio's own sidecar and ambient shapes against Linkerd's and Cilium's from first principles, and Platform Engineering's deeper operational reference goes further into multi-cluster identity and production-scale gotchas for anyone continuing on to the CNPE afterward.

🎬 At Mission Control
🦊

Foxy: Mesh is installed. I'm going straight to STRICT, mesh-wide, day one. Zero trust. Bold. Decisive.

🐢

Timmy the Turtle: Bold, yes. Also the batch namespace isn't labelled, the Prometheus scraper lives outside the mesh, and one database is headless. You'll break three teams before lunch.

🦉

Professor Owl: PERMISSIVE first, Foxy. Enrol everything, restart everything, confirm with istioctl x describe pod that every hop is actually mTLS — then flip. Same discipline on authorization: AUDIT before DENY.

🦫

Benny the Beaver: And has anyone here actually run a canary upgrade with revisions and tags, or has this mesh only ever been upgraded in place, at 2am, by whoever was on call?

👺

Gizmo: Or — hear me out — skip the mesh, give every pod a public IP, let the apps handle their own TLS. Whoever remembers. 🤑

🦉

Professor Owl: Please don't. The entire argument for a mesh is that nobody has to remember — the platform enforces it below the app, for every service, the same way, without asking anyone nicely.

🐢 Timmy's checkpoint

1. Name istiod's four jobs, and explain why a dead control plane doesn't stop traffic already flowing. 2. Contrast sidecar mode and ambient mode — what does each cost, and what do ztunnel and a waypoint each handle? 3. Which resource defines subsets, which one references them by weight, and what happens if a VirtualService names a subset that isn't defined? 4. What's the difference between PeerAuthentication and RequestAuthentication — and which one actually decides who may call what? 5. State the two AuthorizationPolicy evaluation rules, and the mistake that silently flips an AND into an OR. 6. Name two things mtls.mode: STRICT commonly breaks, and the safe rollout order. 7. Give the three-step troubleshooting ladder and the istioctl command at each step.

Check your answers
  1. istiod watches the Kubernetes API, translates state into Envoy config and pushes it over xDS, acts as the mesh's certificate authority issuing SPIFFE identities, and runs the validating/injecting admission webhooks. It never sits on the traffic path — proxies keep enforcing whatever config they last received, so only new config delivery stalls if istiod dies, not existing connections.
  2. Sidecar mode injects an Envoy into every pod — a cost and a hop everywhere, paid whether or not that pod needs it, and it requires a pod restart to enrol. Ambient mode runs one per-node ztunnel handling L4 mTLS/identity for everyone on that node with no restart required, and only adds an optional per-namespace waypoint Envoy where L7 features (HTTP routing, method/path authorization) are genuinely needed.
  3. DestinationRule defines subsets as named, label-selected groups of pods; VirtualService references them in http[].route[].destination.subset with a weight, and weights in one route must sum to 100. Naming a subset no DestinationRule defines produces a 503istioctl analyze catches it before it ships.
  4. PeerAuthentication is workload authentication — does the caller present a valid mesh certificate (STRICT/PERMISSIVE/DISABLE/UNSET)? RequestAuthentication is end-user authentication — is this JWT valid and from a configured issuer — and on its own it rejects only invalid tokens, never missing ones. Deciding who may call what is AuthorizationPolicy's job, matching on identity, method and path.
  5. DENY policies are evaluated before ALLOW policies, so a matching DENY beats any ALLOW; and if any ALLOW policy selects a workload, that workload becomes default-deny for everything not explicitly matched — which is why an empty spec: {} denies a whole namespace. Splitting principals and requestPrincipals into two separate - source: entries — instead of two keys inside one — silently turns "identity AND valid token" into "identity OR valid token," a hole rather than a policy.
  6. Any two of: unmeshed clients (an unlabelled namespace, a legacy VM), a scraper or health check living outside the mesh, headless Services/databases where protocol detection guesses wrong, and anything reached by pod IP rather than Service name. Safe order: install PERMISSIVE → enrol and restart everything → verify with istioctl x describe pod → flip to STRICT one namespace at a time; the same AUDIT-before-DENY discipline applies to authorization.
  7. Configurationistioctl analyze. Control planeistioctl proxy-status (every column should read SYNCED) plus checking the istiod pod itself. Data planeistioctl proxy-config route|cluster|endpoint|secret on one proxy, istioctl x describe pod for the plain-English summary, and the istio-proxy container's own logs.