ICA — Istio Certified Associate
Identity Is the Service Account, Not the IP
In one line
In Istio, a workload's identity is neither its IP nor its Pod name but its Kubernetes service account. From this one fact derive mTLS, authorization policy, and most policy debugging.
Why this was needed
The inside of a cluster is commonly treated as a trusted network, but in reality it is a flat network where if even a single Pod is hijacked, everything else can be reached in plaintext. Zero trust flips this assumption. The problem is that implementing this in application code means putting certificate issuance and renewal, peer verification, and permission-check logic into every service. In an organization with several language stacks, keeping this consistent is practically impossible.
So Istio moved three things (encryption, workload authentication, and authorization) down to the infrastructure layer. The foundation of that is the identity system. An IP changes when a Pod restarts and can be spoofed, but an identity based on a service account is etched into a certificate and cryptographically verified in the handshake.
How it works
Identity follows the SPIFFE standard format.
spiffe://cluster.local/ns/payments/sa/payments-api
트러스트 도메인 네임스페이스 서비스어카운트
What to remember about the certificate issuance flow is that the private key never leaves the Pod. The istio-agent inside the Pod generates the key pair and sends only a CSR to istiod. istiod verifies the requester with the service account token, then signs and returns a certificate with the SPIFFE ID etched into the SAN. The certificate is valid for 24 hours by default, and from around the halfway point of its lifetime the agent reissues it on its own. Envoy receives certificates through SDS, so renewal does not require a Pod restart or connection draining.
PeerAuthentication decides "how to require mTLS for incoming traffic." There are three: PERMISSIVE (accepts both, the default), STRICT (mTLS only), and DISABLE, and the narrower scope always wins. The order is workload > namespace > mesh-wide. A mesh-wide policy is placed in the root namespace (usually istio-system) with the name default. If you want to make an exception for just one port that a health check or a Prometheus outside the mesh scrapes, use portLevelMtls.
The evaluation order of AuthorizationPolicy comes up on the exam almost without fail.
1. CUSTOM → 외부 인가기가 거부하면 즉시 거부
2. DENY → 하나라도 매칭되면 즉시 거부
3. ALLOW → 대상 워크로드에 ALLOW 정책이 하나도 없으면 허용(기본 개방)
ALLOW 정책이 하나라도 있으면 매칭되어야 허용(기본 거부로 전환)
The last line is the key property. The moment you add a single ALLOW policy, that workload flips into allowlist mode. Using this property, a single empty policy with spec: {} creates a namespace default deny. It means "an ALLOW policy exists but has no matching rules," so nothing passes.
Another trap that often catches people. principals come from the mTLS certificate, so a plaintext request has an empty principal and never matches. PERMISSIVE accepts both plaintext and mTLS. That means a plaintext request has no principal, not that every request in PERMISSIVE lacks an identity. An mTLS request can have a principal even in this mode, so in policy debugging, also check the actual connection method. STRICT is a separate decision not to accept plaintext.
RequestAuthentication verifies the end-user token (JWT). The most common misunderstanding here is that with this resource alone, a request without a token just passes. This is because it only enforces "if a JWT is present, it must be valid." To make a token mandatory you also need an AuthorizationPolicy, but you must not simply add one more ALLOW with a JWT condition. Multiple ALLOWs are a union. If an existing ALLOW for the orders identity is there, an orders request without a JWT passes through that policy, and an ALLOW that checks only the JWT can allow other identities and paths as well.
To require both conditions together, put principals and requestPrincipals in the same source of the same ALLOW rule, or keep the existing least-privilege ALLOW and block requests without a JWT principal with a DENY using notRequestPrincipals: ["*"]. Remember the difference that fields in the same source are AND, while different from items or ALLOW policies are OR. A DENY can also catch TCP requests that have no HTTP attributes, so you must state the ports it applies to. The next lab targets only HTTP port 8080.
Failure codes do not tell you the whole cause either. In an isolated Istio 1.31.0 experiment, a JWT with a different audience gave a 403 and an audience rejection message, and an identity or path authorization failure gave a 403 and an RBAC rejection message. Instead of a single 401/403 number, check the response body together with the applied policy and the verified principal.
When putting a policy into production, first attach the annotation istio.io/dry-run: "true". It only evaluates without actually blocking, and shows "what would have been blocked if it were on" through logs and metrics. Removing the annotation when the deny list matches your intent is the safe order.
What it looks like in the field
In the author's homelab, inter-node Pod traffic is transparently encrypted with WireGuard. cilium status shows Encryption: Wireguard [cilium_wg0 (Port: 51871, Peers: 2)], and Modules Health is OK 92 / Degraded 0.
An important distinction arises here that also comes up on the exam. WireGuard is node-to-node transport-segment encryption, while Istio's mTLS is application-layer encryption that comes with proof of workload identity. The former guarantees "eavesdropping on the wire cannot read it" but does not prove "who sent it." What AuthorizationPolicy's principals require is the latter. You can layer the two, but if the team does not settle on a single answer to which one is responsible for encryption, double policies open a debugging hell.
Another sense gained from the same cluster is the log shape of an L7 verdict. A policy-violating request was recorded as DROPPED, and the 403 response to it as FORWARDED. Istio's RBAC denial is likewise of the form "the connection was established and a 403 came back." Never confuse it with a problem where the connection itself fails.
What you will do in the next lab
After storing a dedicated service account and workload specs in the kwok API, you write mesh-wide STRICT and a workload-level port exception, and build up policies in the order default deny → least-privilege ALLOW → admin-path DENY (dry-run) → mandatory JWT.
In this lab, no real containers or Envoy run. Distinguish manifest design from real traffic verification.