STRICT mTLS Migration Versus Plaintext Batch Jobs and a JWT Policy
Namespace retail runs 40 Deployments, all with sidecars. Namespace legacy-batch runs 6 pods with injection disabled; those pods call retail's pricing service over plaintext HTTP on port 8080 and will not be retired for 90 days. Mesh-wide PeerAuthentication in istio-system is PERMISSIVE. The security team requires STRICT mTLS in retail within 30 days and has written an AuthorizationPolicy in retail: allow requests to GET /api/cart when the principal is cluster.local/ns/retail/sa/frontend and request.auth.claims[groups] contains shopper; deny everything else. A RequestAuthentication for issuer https://auth.example.com was applied in retail. Immediately after rollout, frontend gets sporadic 403 responses with RBAC: access denied, roughly 4% of calls, and every legacy-batch job fails with 403. Frontend always sends its JWT. No sidecar restarts are reported.
1. Design the sequence to reach STRICT mTLS in retail without breaking legacy-batch, and describe the evidence you would collect at each step.
Read the worked answer
STRICT is a destination-side statement: the retail pricing sidecar will reject plaintext. So the migration is driven by who still speaks plaintext into retail, not by who has a sidecar.
- Enumerate plaintext sources before changing anything. Query
istio_requests_total{reporter="destination",destination_service_name="pricing",connection_security_policy="none"}and break it down by source workload. Also runistioctl x describe pod <pricing-pod> -n retailto see the effective peer authentication. If legacy-batch is the only plaintext source, you have one exception to design, not forty. - Apply namespace-level STRICT in retail, with a scoped exception. Create a PeerAuthentication in retail with
mtls.mode: STRICTandportLevelMtls: {8080: mode: PERMISSIVE}; alternatively use aselectorto exempt only the pricing workloads legacy-batch calls. Namespace-level policy applies to workloads in that namespace; mesh-wide policy must live in the root namespace (istio-system) with no selector - do not confuse the two. - Prove enforcement is real. After the change, new connections from sidecar-equipped callers should report
connection_security_policy="mutual_tls"; only legacy-batch should still shownone, and only on 8080. Confirm withistioctl x describe podand destination-side metrics. - Retire the exception. Either give legacy-batch sidecars, front it with an egress proxy, or move it to HTTPS/mTLS. Then remove the port-level exception and finally set mesh-wide STRICT.
Order matters: setting STRICT first and adding exceptions afterwards creates an outage window you cannot undo for in-flight connections. Keep the exception as the last thing removed, and put a calendar check on the 90-day retirement date.
2. Frontend receives intermittent 403 RBAC: access denied even though it always sends a valid JWT, and legacy-batch fails 100%. Are these the same cause? Explain how you would separate them.
Read the worked answer
They are almost certainly different failures that happen to share an error string.
Legacy-batch, 100% failure. There are two candidates, and both are expected under this configuration:
- PeerAuthentication is still PERMISSIVE mesh-wide, so plaintext arrives without a peer identity. If the policy also matches on
source.principals, plaintext requests have no principal and are denied. - No sidecar means no JWT validation happens at the workload;
request.auth.claimsis never populated, so thewhencondition cannot be satisfied and the deny-all tail of the policy applies.
A 100% failure with a stable input is a policy-shape problem, not a flake.
Frontend, ~4% failure. A valid JWT plus a sporadic 403 suggests something varies per request or per pod: token expiry/clock skew at the edge of the token lifetime, a subset of frontend pods running an older image that sends a different audience or omits the groups claim, or some frontend replicas talking to a differently configured revision. Check the 403 rate broken down by source pod (istio_requests_total{response_code="403"} grouped by source workload) - if it is concentrated on a few pods, it is the callers, not the policy.
Separate them with istioctl x authz check <pod> -n retail, which lists every policy that applies to the workload and shows which one denies a given request shape, plus proxy logs filtered for RBAC. Fix legacy-batch by giving it an identity or an explicit IP-scoped exception; fix frontend by confirming the token contents (iss, aud, groups) actually match the RequestAuthentication and policy conditions for every replica.
3. Security asks you to "just add an allow rule for legacy-batch" so the jobs stop failing today. What rule would you write, what are its limits, and what must be true in PeerAuthentication for it to work at all?
Read the worked answer
An allow rule for an unauthenticated caller can only match on network attributes, because there is no cryptographic identity to match on.
Shape of the rule, in the retail namespace, ordered before the deny:
from: source: ipBlocks: [<legacy-batch pod CIDR>]- use the actual pod CIDR or the specific addresses, not the whole cluster range.to: operation: methods: ["GET"], paths: ["/api/cart", "/api/pricing/*"]andports: ["8080"], so the exception cannot be reused for other endpoints.- Keep it as a separate, clearly named policy with a comment and an owner, so it is deletable in one object when the jobs retire.
Prerequisite: STRICT mTLS at that port would reject the plaintext connection before authorization is consulted. The allow rule only matters if port 8080 is PERMISSIVE - via portLevelMtls in the retail PeerAuthentication or by leaving the namespace policy PERMISSIVE for that port. This is why "add an allow rule" alone does not fix a 403-turned-into-TLS-failure situation; you must change both layers.
Limits to state explicitly to security:
- IP-based rules are spoofable from inside the cluster network and break if the jobs are rescheduled onto nodes with a different CIDR.
- The rule is invisible to audit: you cannot tell which human or job made the call.
- It provides no protection against a compromised workload in the allowed range.
Therefore treat it as a dated exception with the 90-day retirement as the removal trigger, and add an alert on the rule's request volume so the exception cannot quietly become permanent.