Skip to content

IAM & Security

Every mechanism on this page answers the same underlying question: who is trusted, for how long, and how is that trust narrowed to exactly what's needed? Policy evaluation narrows what an already-trusted identity can do; a trust policy decides who gets to become that identity in the first place; IRSA and STS answer both at once by handing out credentials that expire on their own instead of ones that sit around waiting to leak; and KMS and Secrets Manager apply that same narrowing instinct to keys and secrets rather than principals.

Scope: the AWS module page introduced the explicit-deny-wins evaluation order, the "roles beat users" rule, the IRSA diagram, and named permission boundaries/SCPs, Secrets Manager/Parameter Store, and KMS. This page goes one layer deeper on each: how evaluation actually combines multiple policy types (not just allow vs deny within one), the anatomy of a trust policy, the exact hop-by-hop IRSA mechanism, and what envelope encryption buys you underneath KMS and Secrets Manager.

TL;DR — in 30 seconds:

  • A request is evaluated against every applicable policy type at once (SCP → resource policy → permission boundary → identity policy), and an explicit deny at any layer ends evaluation immediately — SCPs and permission boundaries can only narrow, never grant.
  • IRSA's core property: a Pod gets short-lived, auto-refreshing AWS credentials via sts:AssumeRoleWithWebIdentity + OIDC federation — no static AWS key ever exists in the Pod, image, or a Secret.
  • KMS never encrypts your data directly — it hands out a data key (plaintext + KMS-encrypted copy), your app encrypts locally with the plaintext copy, and KMS is touched once per object, not once per byte.

1. Policy evaluation — one layer deeper than "deny wins"

The module page's rule — explicit deny wins, else explicit allow, else implicit deny — described the outcome for a single policy. In practice a request is evaluated against every policy type that applies to it at once, and the same explicit-deny-wins order governs how they combine:

flowchart TD
    Req["Request"] --> SCP{"SCP allows it?<br/>(org-level ceiling)"}
    SCP -->|"no"| Deny["Denied"]
    SCP -->|"yes"| RP{"Resource policy<br/>explicitly denies?"}
    RP -->|"yes"| Deny
    RP -->|"no"| PB{"Permission boundary<br/>allows it?"}
    PB -->|"no"| Deny
    PB -->|"yes"| ID{"Identity-based policy<br/>explicitly allows it?"}
    ID -->|"no"| Deny
    ID -->|"yes"| Allow["Allowed"]

Two ways to read this diagram:

  • SCPs and permission boundaries are ceilings, not grants. Each one can only narrow what's possible — neither can hand out a permission that no identity-based policy already grants. An SCP that allows everything still denies a request if the identity-based policy is silent on it.
  • An explicit deny at any layer ends the evaluation immediately — a resource policy's explicit deny overrides an identity-based policy's explicit allow, and no other layer gets consulted afterward.

2. Roles and trust policies — what "assume a role" actually means

The module page's "roles beat users" rule rests on a mechanism: a role has no long-term credentials of its own. What it has is a trust policy — a resource-based policy attached to the role itself, separate from the permissions policy that says what the role can do:

Policy Attached to Answers
Trust policy the role who is allowed to assume this role
Permissions policy the role what the role can do once assumed

Assuming a role is a call to STS (sts:AssumeRole for a same-account principal, or sts:AssumeRoleWithWebIdentity/AssumeRoleWithSAML for a federated identity) that checks the trust policy, then hands back short-lived, temporary credentials — an access key, secret key, and session token that expire on their own, so there is nothing long-lived to leak in the first place.

3. IRSA — the exact hop-by-hop path

The module page's IRSA diagram named the hops; here's the same path as a sequence, then what each one actually is:

sequenceDiagram
    participant Pod
    participant SA as ServiceAccount
    participant STS as AWS STS
    participant IAM as IAM role (trust policy)
    Pod->>SA: reads projected token (mounted by webhook)
    Pod->>STS: sts:AssumeRoleWithWebIdentity(token)
    STS->>IAM: validate token signature + trust policy condition
    IAM-->>STS: match on ServiceAccount namespace/name
    STS-->>Pod: short-lived, auto-refreshing credentials
  1. OIDC provider registration — the EKS cluster's control plane exposes an OIDC issuer URL. That issuer is registered once as an IAM OIDC identity provider, which is what lets IAM trust tokens the cluster itself issues.
  2. ServiceAccount annotation — a Kubernetes ServiceAccount is annotated with the target IAM role's ARN. Any Pod that uses this ServiceAccount inherits the annotation.
  3. Token projection — the EKS Pod Identity webhook mutates the Pod spec to mount a projected service account token (a short-lived, audience-scoped JWT) and sets environment variables pointing the AWS SDK at that token file and the role ARN.
  4. sts:AssumeRoleWithWebIdentity — the AWS SDK inside the Pod reads the projected token and calls STS with it. STS validates the token's signature against the registered OIDC provider, then checks the role's trust policy for a matching condition on the ServiceAccount's namespace/name.
  5. Temporary credentials land in the Pod — STS returns short-lived credentials scoped to exactly what the role's permissions policy allows; the SDK caches and auto-refreshes them as they near expiry.

The property this buys: no static AWS key ever exists inside the Pod, the image, or a Secret — only a Kubernetes-issued token that's worthless outside the narrow trust relationship set up in step 1–2. (Go deeper: the official IAM roles for service accounts guide — optional, not required to defend this page.)

4. KMS — envelope encryption, one layer under "encrypts your data key"

The module page named the shape: a data key encrypts your data, and KMS encrypts that data key. The reason this two-key structure exists is performance and blast radius, not ceremony:

  • A KMS key (the module page's CMK) never leaves KMS — every operation that uses it is an API call to the KMS service, which is deliberately slow-ish and heavily audited.
  • Encrypting a large object directly against a KMS key would mean every read/write is an API round-trip. Instead, KMS generates a data key, hands back both its plaintext and a KMS-encrypted copy; the caller encrypts the actual data locally with the plaintext data key, discards the plaintext immediately, and stores only the encrypted data key alongside the ciphertext.
  • Decrypting later means one KMS call — decrypt the small data key — then a fast local decrypt of the large payload. The KMS key itself is touched once per object, not once per byte.

A key policy (a resource-based policy on the KMS key, evaluated the same way as §1's resource-policy layer) governs who can use or manage the key. A grant is a narrower, temporary permission on a key — handed to a specific AWS service or principal for a specific operation, without editing the key policy — which is how services like EBS or RDS get just enough access to encrypt/decrypt on your behalf. KMS keys also support scheduled automatic rotation of the underlying cryptographic material, so a compromised key version ages out without you having to re-encrypt anything that depends on it.

5. Secrets Manager vs Parameter Store — the rotation mechanism

The module page's rule was cost-shaped: Secrets Manager buys rotation and audit at a price; Parameter Store is the cheaper, config-shaped sibling. The rotation Secrets Manager buys is a specific mechanism, not just a checkbox:

  • Rotation is driven by a Lambda function (AWS provides templates for common databases) that Secrets Manager invokes on a schedule.
  • That function follows a four-step contract — create a new secret version, set it on the target resource, test that it works, then finish by marking the new version current — so a failed test doesn't leave the secret and the resource out of sync.
  • Every version is retained with a staging label (current vs previous), which is what lets an in-flight rotation roll back cleanly if the test step fails.

Both services can be read by a Pod via IRSA-scoped credentials exactly as in §3 — the credential path doesn't change based on which secret store sits at the far end.

6. How this connects

  • This extends the AWS module page's Security theme (explicit-deny-wins, roles-beat-users, the IRSA diagram, permission boundaries/SCPs, Secrets Manager/Parameter Store/KMS) with how multiple policy types combine, the trust-policy mechanism underneath role assumption, the exact IRSA hop sequence, and the envelope-encryption and rotation mechanics underneath KMS and Secrets Manager.
  • The Prove gate's credential-path question draws directly on §3's hop list; the provenance question about verifying a tutor-supplied IRSA claim is exactly what §3 spells out end to end.

Common gotchas

  • Assuming an SCP that "allows everything" grants a permission by itself. Fix: SCPs and permission boundaries are ceilings, not grants — they can only narrow what's possible; an identity-based policy still has to explicitly allow the action.
  • Confusing a trust policy with a permissions policy on a role. Fix: the trust policy decides who can assume the role; the permissions policy decides what the role can do once assumed — editing the wrong one doesn't get you the change you wanted.
  • Baking a static AWS key into a Pod or image "just to get it working." Fix: IRSA exists specifically so a Pod never needs one — an annotated ServiceAccount + OIDC federation hands it short-lived, auto-refreshing credentials instead.
  • Encrypting a large object directly against a KMS key. Fix: that means an API round-trip to KMS per read/write — use the envelope-encryption pattern instead (KMS issues a data key once, your app encrypts locally with it, KMS is touched once per object).
  • Assuming a failed Secrets Manager rotation leaves the secret unusable. Fix: rotation's four-step contract (create → set on target → test → mark current) keeps the previous version's staging label intact until the test step passes — a failed test doesn't finish the rotation, so the working secret is still there.
Check yourself — a resource policy explicitly denies a request, but the identity-based policy explicitly allows it. What happens, and why?

The request is denied — an explicit deny at any policy layer (SCP, resource policy, permission boundary, identity policy) ends the evaluation immediately, and no other layer gets consulted afterward, no matter what it would have allowed.

Check yourself — name every hop between a Pod and its AWS credentials under IRSA, without skipping one.

The Pod reads a projected ServiceAccount token (mounted by the EKS Pod Identity webhook) → calls STS with sts:AssumeRoleWithWebIdentity → STS validates the token's signature against the registered OIDC provider → checks the IAM role's trust policy for a matching condition on the ServiceAccount's namespace/name → returns short-lived, auto-refreshing credentials scoped to the role's permissions policy.

You can defend this when you can explain why an SCP or permission boundary can only narrow, never grant, a permission; name every hop between a Pod and its temporary AWS credentials under IRSA without skipping one; explain why KMS encrypts a data key instead of your data directly; and say what actually happens, step by step, when a Secrets Manager rotation fails partway through.