Module 02 — Cloud Identity & IAM¶
Type 3 · Blast-Radius Trace (+ Type 4 · Audit→Build→Verify) — predict one leaked key's reach, then prove it with simulate-principal-policy. (Secondary: Audit→Build→Verify — author the least-privilege policy that closes the path and re-simulate.) Go to the hands-on lab →
Last reviewed: 2026-06
Cloud & Container Security — in the cloud the perimeter is identity; one leaked key is a question of how much of the business it can touch.
In 60 seconds
In the cloud the perimeter is identity, so one leaked key is a question of how much of the business
it can touch — and the answer is the transitive closure of its permissions, including the
permissions it can grant itself. Code Spaces died in twelve hours because a single credential could
reach the whole account and the backups inside it. The reach is almost always wider than the
policy's label: iam:PassRole + ec2:RunInstances composes two legitimate grants into admin. Every
IAM wall obeys one rulebook — explicit deny > allow > implicit deny — and a fix is only "proven" when
simulate-principal-policy denies the dangerous action while still allowing the legitimate one.
The case¶
In June 2014, Code Spaces — a code-hosting and project-management company that had run for seven years — received an extortion email. The attacker already had access to the company's Amazon EC2 control panel. When Code Spaces tried to retake control by changing passwords, the attacker, still holding valid credentials, began deleting everything: EC2 instances, S3 buckets, AMIs, EBS volumes, and — fatally — the backups, which lived in the same account. Within roughly twelve hours most of the company's data and infrastructure was gone. Code Spaces did not recover. Its own closing notice told customers the damage was "unrecoverable" and that continuing would "cause irreparable harm" — the company effectively ceased to exist.
There was no zero-day, no exotic exploit. The whole breach was one set of credentials that could reach the entire account, including the one thing that was supposed to survive a bad day: the backups. So before you read on, this module turns on a single question:
One leaked key. How much of the business can it actually touch — and how much of that is reversible?
Your job¶
By the end of this module you'll predict a principal's blast radius, then prove it — enumerate a
seeded over-broad identity, demonstrate its real reach with iam simulate-principal-policy (AWS's own
evaluation logic, not a guess), and then do the half that auditing alone skips: author the
least-privilege policy that closes the path and re-simulate to prove the reach is gone. That
audit→build loop — find the over-grant, cut it to the minimum, verify the cut holds — is the exact
motion of a cloud IAM assessment, and it is what makes the verdict yours instead of a finding you copied.
Call it before you read on¶
Don't scroll. Write down your gut answers — being wrong here is the teaching event, and you'll grade yourself in the lab.
Q1. A developer's leaked access key is scoped "just for dev work." Realistically, how far past dev can it reach — its own bucket, the whole account, or somewhere in between?
Q2. The attacker deleted the backups too. Why didn't a backup save Code Spaces — what made the blast irreversible?
Q3. An IAM policy says
Allow s3:*. Another, attached to the same role, saysDeny s3:Del*. Can the principal delete an object?
The blast radius, revealed¶
Hold your answers against these.
Q1 — the reach is almost always wider than the label. "Dev" is a name on a policy, not a boundary.
The grant that matters is Action × Resource, and the moment either is * the label stops meaning
anything. The over-broad developer policy you'll enumerate grants s3:* on * and iam:PassRole on
* — so "dev-alice" can read and delete every bucket in the account, and, far worse, pass any role
to a service she controls. iam:PassRole + ec2:RunInstances is the canonical escalation: launch an
EC2 instance attached to the admin role, and the instance — and through it, the attacker — is admin.
That isn't a bug in IAM; it's two legitimate permissions that compose into root.
flowchart LR
A["dev-alice<br/>(iam:PassRole + ec2:RunInstances on *)"]
E["EC2 instance<br/>(launched by alice)"]
R["AdminRole<br/>(iam:* s3:*)"]
T(["Account admin"])
A -- "RunInstances, attach role" --> E
A -. "PassRole the admin role" .-> E
E -- "assumes its instance-profile role" --> R
R --> T
The blast radius of a key is never what its name suggests — it's the transitive closure of everything its permissions can reach, including the permissions it can grant itself. People reliably under-guess this, and the under-guess is how a "dev" key ends a company.
The mental model
A key's blast radius is not the label on its policy — it's the transitive closure of everything its
permissions can reach, including the permissions it can grant itself. iam:PassRole + a launch
action is two legitimate grants that compose into root.
The gotcha
People read Action and Resource and stop. The escalation hides in composition: no single line
says "admin," but iam:PassRole on * plus ec2:RunInstances does. And explicit Deny beats every
Allow everywhere in the chain — so a broad allow isn't dangerous if a deny walls it, and isn't safe
just because the name says "dev."
Q2 — the perimeter is identity, and there was only one of it. Backups defend against deletion only if the thing that can delete the primary cannot also delete the backup. Code Spaces' backups lived in the same account, reachable by the same control-plane credentials — so a single identity with account-wide power was a single point of failure for the entire business, recovery included. This is the cloud's hard lesson: when access is identity, the blast radius of one principal is the intersection of its reach and your inability to recover from it. Encryption, redundancy, and backups are all silent against a principal you authorized to destroy them. The fix isn't "more backups" — it's that no single principal should be able to reach both the system and its recovery path.
Q3 — explicit deny wins, always. This is the rulebook every wall in IAM obeys, and it's worth
memorizing because every guardrail you write depends on it. AWS evaluates a request as default-deny:
with no matching Allow, the answer is no (implicit deny). A matching Allow flips it to yes. But a
matching explicit Deny anywhere in the chain overrides every Allow — identity policy, resource
policy, SCP, boundary, session. So the order is explicit deny > allow > implicit (default) deny.
The principal in Q3 cannot delete: the Deny s3:Del* beats the s3:* allow. That ordering is what
makes least-privilege enforceable — you scope allows down to the minimum, and where you must keep a
broad allow, an explicit deny is the wall that holds regardless. In the lab, "the reach is gone" means
exactly this evaluation returns implicitDeny/explicitDeny for the dangerous action and allowed
for the legitimate one.
The federation footnote: the same evaluation governs who can assume a role via its trust policy.
A trust policy with "Principal": {"AWS": "...:root"} trusts every identity in the account, not one;
an OIDC trust with no sub condition trusts every workflow from the provider. When that trust is
forged or over-broad, the wall never even gets consulted — which is exactly how Golden SAML worked in
SolarWinds (a stolen token-signing key let attackers mint SAML assertions for any user, federating
straight past authentication). Same model — who can act, evaluated against policy — one layer up.
Go deeper: irreversibility is its own dimension of blast radius
Reach is only half the story. Code Spaces could survive having data read; it could not survive having data deleted with its backups in the same blast radius. When access is identity, blast radius is the intersection of a principal's reach and your inability to recover from what it does — which is why the fix is not "more backups" but ensuring no single principal can touch both the system and its recovery path.
AI caveat
A model is a strong first-pass escalation detector — it'll flag iam:PassRole and s3:* on *. But
it sees one document; it can't know whether an SCP, boundary, or Deny you didn't paste already caps
the grant. Treat every hit as a hypothesis and confirm it with simulate-principal-policy, which runs
AWS's real logic. The minimum cut is yours.
Learn (~3 hrs)¶
Richer than a foundations module: IAM evaluation is the load-bearing mechanism for the next three modules, so it's worth the time. Read the case above first, then go deep on the mechanism.
The evaluation rulebook (~1 hr)
- AWS — IAM policy evaluation logic (~30 min) — the primary source for explicit-deny > allow > implicit-deny. Read the flowchart and the "Determining whether a request is allowed or denied within an account" section; everything in this module is an application of that one diagram.
- AWS — simulate-principal-policy (CLI reference) (~15 min) — the command that runs that logic for you and returns allowed/explicitDeny/implicitDeny. This is how the lab proves a wall holds without LocalStack enforcing it.
- AWS — Grant least privilege (~15 min) — the official best-practice section; treat it as the gap analysis checklist against the findings.
The escalation that the reach hides (~1 hr)
- Rhino Security Labs — AWS IAM Privilege Escalation Methods (~40 min) — primary research cataloguing 21 real escalation paths. Read iam:PassRole+RunInstances and CreateAccessKey in detail; the rest is reference for module 03.
- BishopFox — cloudfox README (AWS section) (~20 min) — the enumeration accelerator; skim permissions, role-trusts, iam-simulator so the lab's commands are familiar.
The federation footnote (~30 min) - CISA — Emergency Directive 21-01 (SolarWinds / SUNBURST) (~15 min, skim) — the federal response; orient on the trust-compromise angle. - CISA — guidance on detecting forged SAML tokens (Golden SAML) (~15 min) — why a stolen signing key defeats the trust wall entirely.
Key concepts¶
- A key's blast radius is the transitive closure of its permissions — including the permissions it can grant itself (
iam:PassRole,CreateAccessKey) — not the label on its policy - IAM evaluation order is the rulebook every wall obeys: explicit deny > allow > implicit (default) deny
iam:PassRole+ a launch action (ec2:RunInstances,lambda:CreateFunction) composes two legitimate grants into admin- Trust policies decide who can assume a role;
roottrusts the whole account, an unscoped OIDC trust trusts every workflow — Golden SAML forges past the wall entirely - Single-principal, account-wide reach over both a system and its backups is what makes a blast irreversible (Code Spaces)
- Least privilege is verifiable: a fix is "proven" only when
simulate-principal-policydenies the dangerous action and still allows the legitimate one
AI acceleration¶
Hand a model the over-broad developer policy and ask it to enumerate the blast radius and escalation
paths before you do. It's a strong first-pass escalation detector — it will reliably flag
iam:PassRole and s3:* on *. But it sees one document, and IAM is a multi-layer evaluation: it
cannot tell you whether an SCP or permission boundary above caps the grant, or whether a Deny you
didn't paste already closes the path. Treat its output as a hypothesis and validate every hit with
simulate-principal-policy, which runs AWS's real logic. The skill the model can't do for you is the
minimum cut — author the smallest policy change that denies the dangerous action without breaking the
principal's real job, then prove it. You direct it; you own the verdict.
Check yourself
- A leaked key's policy is named "dev" — why is its real blast radius almost never what the label suggests, and what two things actually bound it?
- Two legitimate permissions composed into admin in this module — name the pair and explain why neither is a bug in IAM.
- A role has
Allow s3:*andDeny s3:Del*attached. Can it delete an object, and what evaluation rule decides it?
Comments
Sign in with GitHub to comment. Choose the type: Feedback (errors or suggestions on this page) · Hints (help for fellow learners — no spoilers) · General (anything else).