TT Lab
Get started
Learn Learning paths Courses

Istio Service Mesh

Narrowing From Deny-All to Least Privilege

Continue in TT Lab

Goal

You build least-privilege authorization that starts from default deny and opens permissions only along the call graph, and you become able to explain the result of each call as a matrix.

Why it matters

The most common mistake in authorization design is approaching it in the direction of blocking what you need to block one by one. Then a path you left out stays open forever, and nobody knows. Conversely, if you start from a full deny, a path you left out is revealed immediately as a 403, so the design becomes complete. Istio expresses this reversal in a single line, spec: {} — an ALLOW policy exists but has no matching rule, so nobody gets through.

You must also internalize the evaluation order. It is CUSTOM → DENY → ALLOW, and ALLOW means "if there is even one policy, you must match to get through." That is, the moment you first add an ALLOW policy, that workload switches to allowlist mode. Accidents where someone adds a single policy without knowing this and all existing traffic is blocked are common. The property that DENY is evaluated before ALLOW conversely becomes a safety net — for things like management paths that "must be blocked no matter what," you lay an extra layer of DENY.

Finally, the principals of an authorization policy come from the identity in the mTLS certificate. A request that arrives in plaintext has an empty principal and never matches, so moving to STRICT is a precondition for authorization.

Steps

Preparation before starting. Lab Pods come up fresh for each lab, so the mesh configuration from the earlier lab does not remain. If kubectl get crd authorizationpolicies.security.istio.io is empty, run istioctl manifest generate --set profile=minimal > /root/istio/manifest.yaml and then kubectl apply -f /root/istio/manifest.yaml twice, and prepare the namespace with kubectl create ns mesh-lab && kubectl label ns mesh-lab istio-injection=enabled. You must also create anew, within this lab, the payments and ratings workloads that the selectors in steps 2 and 4 point to.

  1. Create an AuthorizationPolicy deny-all in mesh-lab — put only spec: {} and apply it with neither a selector nor rules (leave out action so the default ALLOW applies). And in /root/istio/authz/out/denyall-note.txt, write why nobody gets through when there are no rules.
  2. Create a ServiceAccount frontend in mesh-lab (and also create the payments workload from the earlier lab if it does not exist), and create an AuthorizationPolicy allow-frontend: selector.matchLabels.app is payments, action is ALLOW, and in rules[0].from[0].source.principals write cluster.local/ns/mesh-lab/sa/frontend. You must not attach the spiffe:// prefix.
  3. Add rules[0].to[0].operation to the same allow-frontend policy. methods is only ["GET"], and paths includes /api/*. And in /root/istio/authz/out/path-note.txt, write that path matching supports only prefix/suffix wildcards, not regular expressions.
  4. Create the ratings workload (Service ratings, Deployment ratings-v1, Pod label app: ratings) in mesh-lab, and create an AuthorizationPolicy allow-mesh-lab: selector.matchLabels.app is ratings, and rules[0].from[0].source.namespaces is ["mesh-lab"]. And in /root/istio/authz/out/ns-note.txt, write that namespace level is coarser control than service account (principal) level.
  5. Create an AuthorizationPolicy deny-admin in mesh-lab: action is DENY, and rules[0].to[0].operation.paths includes /admin/*. And in /root/istio/authz/order.txt, write the evaluation order one per line, in three lines — the first line must contain CUSTOM, the second DENY, and the third ALLOW (you must not crowd them onto one line).
  6. Create an AuthorizationPolicy allow-v2-clients in mesh-lab. Put two conditions in rules[0].when: one is key: request.headers[x-api-version] with values: ["v2"], and the other is key: source.namespace with notValues: ["legacy"]. The key of a condition that uses notValues must not contain request.headers.
  7. Create an AuthorizationPolicy audit-sensitive in mesh-lab: action is AUDIT, and in rules[0].to[0].operation.paths specify the path to audit (for example /internal/*). And in /root/istio/authz/out/audit-note.txt, write together that AUDIT only records and does not block traffic, and that it is for seeing the impact in advance before actually turning the policy on.
  8. Create /root/istio/authz/out/authz-matrix.json. Put 6 or more entries in the cases array, where each entry must have expected (either "allow" or "deny") and policy (the name of the policy that produced that result — it must actually exist in mesh-lab and be a single word with no spaces). You need at least 2 expected allows and at least 2 expected denies. mesh-lab must have at least 5 AuthorizationPolicies, and save the result of istioctl analyze -n mesh-lab to /root/istio/authz/out/analyze.txt, with no Error [.

Notes

Lay down a full deny with empty rules

Create an AuthorizationPolicy deny-all in mesh-lab — put only spec: {} and apply it with neither a selector nor rules (leave out action so the default ALLOW applies). And in /root/istio/authz/out/denyall-note.txt, write why nobody gets through when there are no rules.

An allow policy with no rules at all is a full deny. The answer to why is in the last line of the evaluation order. It must apply to the whole namespace, so do not put a selector.

Open just one path by caller identity

Create a ServiceAccount frontend in mesh-lab (and also create the payments workload from the earlier lab if it does not exist), and create an AuthorizationPolicy allow-frontend: selector.matchLabels.app is payments, action is ALLOW, and in rules[0].from[0].source.principals write cluster.local/ns/mesh-lab/sa/frontend. You must not attach the spiffe:// prefix.

A policy attaches to the receiving workload. The identity is a service account, not an IP, and note that this field takes no prefix.

Narrow down to method and path

Add rules[0].to[0].operation to the same allow-frontend policy. methods is only ["GET"], and paths includes /api/*. And in /root/istio/authz/out/path-note.txt, write that path matching supports only prefix/suffix wildcards, not regular expressions.

This is the step that allows only reading. Confirm that path matching is not a regular expression and write it in the note.

Allow by namespace

Create the ratings workload (Service ratings, Deployment ratings-v1, Pod label app: ratings) in mesh-lab, and create an AuthorizationPolicy allow-mesh-lab: selector.matchLabels.app is ratings, and rules[0].from[0].source.namespaces is ["mesh-lab"]. And in /root/istio/authz/out/ns-note.txt, write that namespace level is coarser control than service account (principal) level.

It is coarser control than the identity level. Leave in the note when this is enough and when it is not.

Lay a deny safety net and write the evaluation order

Create an AuthorizationPolicy deny-admin in mesh-lab: action is DENY, and rules[0].to[0].operation.paths includes /admin/*. And in /root/istio/authz/order.txt, write the evaluation order one per line, in three lines — the first line must contain CUSTOM, the second DENY, and the third ALLOW (you must not crowd them onto one line).

A deny is evaluated before an allow, so it defends against mistakes in allow policies. The order summary must be written one per line.

Narrow down more finely with conditions

Create an AuthorizationPolicy allow-v2-clients in mesh-lab. Put two conditions in rules[0].when: one is key: request.headers[x-api-version] with values: ["v2"], and the other is key: source.namespace with notValues: ["legacy"]. The key of a condition that uses notValues must not contain request.headers.

You can attach several conditions, and you can also express exclusion conditions. Split the header condition and the exclusion condition into separate items.

Create a policy that only records without blocking

Create an AuthorizationPolicy audit-sensitive in mesh-lab: action is AUDIT, and in rules[0].to[0].operation.paths specify the path to audit (for example /internal/*). And in /root/istio/authz/out/audit-note.txt, write together that AUDIT only records and does not block traffic, and that it is for seeing the impact in advance before actually turning the policy on.

It is a device for seeing the impact first before actually turning the policy on. Write exactly what effect it has on traffic.

Build a per-call allow/deny matrix

Create /root/istio/authz/out/authz-matrix.json. Put 6 or more entries in the cases array, where each entry must have expected (either "allow" or "deny") and policy (the name of the policy that produced that result — it must actually exist in mesh-lab and be a single word with no spaces). You need at least 2 expected allows and at least 2 expected denies. mesh-lab must have at least 5 AuthorizationPolicies, and save the result of istioctl analyze -n mesh-lab to /root/istio/authz/out/analyze.txt, with no Error [.

You must write down, for each call, which policy makes it decided that way. The policy name must be one that actually exists in the cluster.