Stop reading whenever you have enough

AuthorizationPolicy, in seventeen questions.

Each one is the question the previous answer makes you ask. Ten come with the manifest or the command, because that is the part you meet at work.

Not technical? The first three questions are written for you, and they are enough to follow any conversation about this.

Rungs
17
Hands on
10
Read
~10 min
01

What it is

0103

Proving who is calling is one job. Deciding whether they are allowed is this one.

three minutes, no cluster needed · for anyone

  1. 01

    What problem is this solving?

    Not knowing this costs

    A mesh with perfect mTLS and no policies is still a flat network.

    • mTLS proves who is calling, then stops
    • Proven is not the same as permitted
    • This is where permitted gets decided
  2. 02

    What can a rule actually say?

    Not knowing this costs

    Three dimensions. Every policy you will ever read is these three.

    • Who: an identity, a namespace, or anyone
    • What: paths, methods, ports
    • To which workload the rule is attached
  3. 03

    What happens with no policy at all?

    Not knowing this costs

    Most meshes run for years in the default, wide open on purpose by accident.

    • Everything is allowed, for everyone
    • The default is open, not closed
    • One deny all per namespace flips that

Before you scroll on

0/3

You should now be able to

02

The YAML you will see

0407

A policy names who may do what, to which workload. Everything else is these three parts rearranged.

the four shapes that appear in real repos · for whoever writes the manifests

  1. 04

    Where do I start?

    hands on

    Not knowing this costs

    An allow list only means something when the default answer is no.

    • Deny everything in the namespace first
    • An empty spec is the deny all
    • Every allow after this is a deliberate decision
    manifestthe default becomes no
    apiVersion: security.istio.io/v1
    kind: AuthorizationPolicy
    metadata:
      name: deny-all
      namespace: payments
    spec: {}               # empty spec denies every request
    
    Empty spec matches nothing, and a workload covered by any policy refuses whatever no ALLOW matches. Apply this first, in a quiet moment, not during the incident.
  2. 05

    How do I let one service through?

    hands on

    Not knowing this costs

    One character of prefix turns an allow list into an outage.

    • An ALLOW policy on the receiving workload
    • principals is the SPIFFE identity, scheme dropped
    • The identity is the service account, nothing else
    manifestonly checkout may call payments
    apiVersion: security.istio.io/v1
    kind: AuthorizationPolicy
    metadata:
      name: payment-svc-callers
      namespace: payments
    spec:
      selector:
        matchLabels:
          app: payment-svc
      action: ALLOW
      rules:
        - from:
            - source:
                principals:
                  - cluster.local/ns/checkout/sa/checkout
    
    principals drops the spiffe:// prefix. Leaving it in matches nothing, and an ALLOW rule that matches nothing denies everyone the deny-all was already refusing.
  3. 06

    Can I allow the caller but not everything it asks?

    hands on

    Not knowing this costs

    Allowing a caller wholesale grants every endpoint you add later, forever.

    • to blocks narrow paths, methods and ports
    • from and to combine as AND within a rule
    • Separate rules combine as OR
    manifestread only, for one caller
      rules:
        - from:
            - source:
                principals:
                  - cluster.local/ns/reporting/sa/dashboard
          to:
            - operation:
                methods: ["GET"]
                paths: ["/api/invoices/*"]
    
    The dashboard can read invoices and nothing else. Note the wildcard is a path prefix segment, not a regex: /api/invoices/* does not match /api/invoices.
  4. 07

    When do I use DENY instead?

    hands on

    Not knowing this costs

    An absolute written as ALLOW exceptions erodes one exception at a time.

    • DENY is evaluated first and cannot be overridden
    • Use it for absolutes: never from outside, never DELETE
    • Everything conditional belongs in ALLOW
    manifestthe absolute, stated as one
    apiVersion: security.istio.io/v1
    kind: AuthorizationPolicy
    metadata:
      name: no-deletes-ever
      namespace: payments
    spec:
      selector:
        matchLabels:
          app: payment-svc
      action: DENY
      rules:
        - to:
            - operation:
                methods: ["DELETE"]
    
    Order of evaluation: DENY matches refuse immediately, then ALLOW is consulted. No ALLOW can punch a hole through a DENY, which is exactly why absolutes go there.

Before you scroll on

0/3

You should now be able to

03

Proving it works

0810

Trust nothing you cannot test. Two calls prove the door is open for one caller and shut for another.

commands that answer yes or no · for whoever has to sign it off

  1. 08

    Does the allowed caller get through?

    hands on

    Not knowing this costs

    A rule tested from the wrong pod proves nothing about the right one.

    • Call from the allowed workload, expect 200
    • The identity comes from the pod service account
    shellpositive test, from the allowed pod
    kubectl exec deploy/checkout -n checkout -- \
      curl -s -o /dev/null -w '%{http_code}\n' \
      payment-svc.payments:8080/api/charge
    
    # 200      <- allowed, as written
    
    Run it from the checkout deployment specifically. Running it from any handy pod tests that pod identity, not the rule you wrote.
  2. 09

    Is everyone else actually refused?

    hands on

    Not knowing this costs

    Without the negative test you proved the door opens, not that it locks.

    • Call from any other workload, expect 403
    • The body says RBAC: access denied
    • That exact string separates authz from every other 403
    shellnegative test, from anywhere else
    kubectl exec deploy/sleep -n default -- \
      curl -s -w '\n%{http_code}\n' payment-svc.payments:8080/api/charge
    
    # RBAC: access denied
    # 403      <- the policy working, verbatim
    
    A 403 from your application looks identical at the status line. The RBAC body text is the proof it was Envoy refusing, not your code.
  3. 10

    Which policies apply to this workload right now?

    hands on

    Not knowing this costs

    A policy in the namespace is not a policy on the workload.

    • Ask the pod, not the namespace listing
    • Selectors and namespaces make the listing misleading
    shellthe effective set
    istioctl x describe pod payment-svc-7d9c-x2k4 -n payments
    
    # Authorization policies:
    #   ns[payments]-policy[deny-all]-rule[none]
    #   ns[payments]-policy[payment-svc-callers]-rule[0]
    
    deny-all missing from this list means it never matched this workload, and your allow rules are decorating a door that was never locked.

Before you scroll on

0/3

You should now be able to

04

When it breaks

1113

It fails closed or fails open. Both have one line signatures once you know them.

the failures you will actually hit · for whoever gets paged

  1. 11

    A service started getting 403s after a deploy. What is it?

    hands on

    Not knowing this costs

    Renaming a service account is an authz change nobody reviews as one.

    • RBAC: access denied in the body confirms authz
    • The caller identity changed, usually its service account
    • principals pin the old name
    shellfind the identity that actually arrived
    kubectl logs deploy/payment-svc -c istio-proxy -n payments --tail=20 \
      | grep rbac
    
    # ...rbac_access_denied_matched_policy[none]
    # principal: cluster.local/ns/checkout/sa/checkout-v2   <- renamed
    
    A deployment that switched service accounts switched identity. The policy is doing exactly what it says; it is the caller that changed name.
  2. 12

    My identity rule works for some callers and not others. Why?

    hands on

    Not knowing this costs

    An identity allow list over plaintext is a lock on an open door.

    • principals only exist when the connection is mTLS
    • A plaintext caller has no identity to match
    • PERMISSIVE mode lets exactly those callers in
    shellthe two rungs meet
    istioctl x describe pod payment-svc-7d9c-x2k4 -n payments
    #    Workload mTLS mode: PERMISSIVE   <- plaintext callers possible
    
    # a plaintext caller matches no principals rule:
    # with deny-all present it gets 403,
    # without deny-all it walks straight past your allow list
    
    This is why the mTLS ladder ends by pointing here and this one starts by pointing back. Identity rules stand on STRICT; on PERMISSIVE they have a hole shaped like every proxyless pod.
  3. 13

    I added an ALLOW policy and everything got refused. What is it?

    hands on

    Not knowing this costs

    The most common authz outage is a well meant policy for a scraper.

    • The first ALLOW flips the workload to default deny
    • Everything not matching it is now refused
    • Your policy worked; the surprise is the flip
    shellthe flip, demonstrated
    kubectl get authorizationpolicy -n payments
    # NAME               ACTION   AGE
    # metrics-scraper    ALLOW    2m    <- the only policy
    
    # every caller except the scraper now gets:
    # RBAC: access denied
    
    One narrow ALLOW for Prometheus quietly denies every real caller. Ship the deny-all and the full allow set together, or ship nothing yet.

Before you scroll on

0/3

You should now be able to

05

Where it ends

1417

What these rules cannot see, and the one dependency they never forgive.

the limits, where people get caught · for whoever reviews the design

  1. 14

    What do these rules stand on?

    Not knowing this costs

    Authorization inherits every weakness of the authentication below it.

    • Identity comes from the mTLS handshake
    • No STRICT, no trustworthy principals
    • The two ladders are one system
  2. 15

    What is the smallest thing a rule can see?

    Not knowing this costs

    Ten deployments on default share one identity, and one compromise.

    • A service account, not a pod or deployment
    • Workloads sharing one account are indistinguishable
    • Give each workload its own account, early
  3. 16

    What can the rules not inspect?

    Not knowing this costs

    Path rules cannot know invoice 4132 belongs to a different tenant.

    • Anything inside an encrypted body
    • Business logic: which invoice, whose account
    • That authorization still belongs in your code
  4. 17

    Does a DENY here protect against a stolen identity?

    Not knowing this costs

    Rules bound what a stolen identity can do. They cannot make it zero.

    • Only within what that identity was never allowed
    • A compromised pod keeps its legitimate permissions
    • Least privilege is the whole defence

Before you scroll on

0/3

You should now be able to

Go deeper

5 links, each earning its place.

Where this leaves you

Rungs 4 and 5 are the pair to memorise: deny all, then allow by name. Rungs 8 and 9 are the two curls that prove both halves.

If you keep one thing: the first ALLOW policy on a workload flips it to default deny. Ship the deny-all and the allow set together, or not yet.