Skip to content

IAM and least privilege

Refused unless allowed, Deny beats Allow, and every identity gets the least it needs for the shortest time.

Predict first

Before you read: predict

ExamHall's grading service runs under the role examhall-grader, whose only policy is:

{ "Statement": [
  { "Effect": "Deny",  "Action": "s3:DeleteObject", "Resource": "arn:aws:s3:::examhall-media/proctoring-uploads/*" },
  { "Effect": "Allow", "Action": "s3:*",            "Resource": "arn:aws:s3:::examhall-media/proctoring-uploads/*" }
] }

The grader sends three requests to the bucket examhall-media: (1) GetObject on proctoring-uploads/a-1/c-7.webm, (2) DeleteObject on the same object, (3) GetObject on results/a-1.json.

Which of them does AWS allow?

Pick one

The 30-second version

AWS refuses every request unless a policy allows it, and one matching Deny anywhere beats every Allow. Grant each role only the actions and resources its job needs. Give workloads roles, whose credentials expire, not access keys, which work until switched off. CI reaches its role through OIDC; the trust policy's sub condition decides which runs get in.

Refused unless allowed, Deny beats Allow, and every identity gets the least it needs for the shortest time.

How it works

Step 1 of 12

Every policy that applies is read. The role's own policies and the resource's policy, such as a bucket policy; statement order decides nothing.

Analogy: A members' makerspace, its door fobs and its machine inductions

In the makerspace In IAM
a member's fob opens only the doors and machines it was set up for implicit deny (step 2)
a red "do not use" tag on a machine beats every induction an explicit Deny wins (step 3)
an induction names the laser cutter, not "the machines" least privilege (step 6)
a day fob printed at reception temporary credentials (step 7)
the day fob stops opening doors at closing time a session ends, and its credentials with it (step 8)
a metal key cut years ago has no closing time a long-term access key (step 9)
a visiting school's letter names one class and one day; reception prints fobs only for that class the trust policy's sub condition (steps 10–12)

In real terms: AWS evaluates every applicable policy on every request; scope, expiry and the trust condition are what you control.

Breaks down when: one fob opens a handful of doors, while one AWS resource pattern can cover millions of objects.

Contrasting case: a broad managed policy on a new role makes every request work on day one; least privilege means removing what the role never uses, which is harder later (step 6).

Examples, run for real

4.1 ExamHall EXH-3980 → MR !4280: an upload role that could delete

ExamHall's upload role was granted s3:* on the whole bucket. A recorder release ran its local-cache cleanup against storage, and AWS allowed the deletes: three proctoring chunks of attempt a-4 were lost (EXH-3980). MR !4280 grants only s3:PutObject, on the attempts' folders.

4.1 EXH-3980, s3:* on examhall-media/*: PutObject chunk c-12 Allow · cleanup DeleteObject on chunks c-9 to c-11: 3 of 3 allowed
4.1 MR !4280, s3:PutObject on examhall-media/proctoring-uploads/a-*: PutObject chunk c-12 Allow · the same 3 deletes: 0 of 3 allowed (ImplicitDeny)

The narrowed role still stores chunks; the deletes now meet no Allow and are refused, so the next cleanup bug loses nothing (steps 2, 6).

4.2 Proof domain, payments ledger: releases deploy from tags through OIDC

A payments ledger's deploy role trusts GitHub's provider for release tags only: "StringLike": { "token.actions.githubusercontent.com:sub": "repo:acme/ledger:ref:refs/tags/v*" }.

4.2 tag v2.4.0 (sub repo:acme/ledger:ref:refs/tags/v2.4.0): session issued 10:00, ends 11:00 · push to main (sub repo:acme/ledger:ref:refs/heads/main): refused
4.2 the tag's session at 10:50: accepted · at 11:05: refused (expired)

A tag's job gets a one-hour session; a push to main names the branch, so the trust refuses it, and the session stops working once it expires (steps 7, 10, 11).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 6

ExamHall's upload role examhall-uploader has one policy statement: { "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::examhall-media/proctoring-uploads*" }. A review runs two test requests as the role: PutObject on proctoring-uploads/a-1/c-7.webm, which must work, and PutObject on proctoring-uploads-2025/a-0/c-1.webm, last year's recordings, which the role must not touch. AWS allows both.

Which change keeps the first and refuses the second?

Pick one

Question 2 of 6

A payments ledger deploys from the GitHub repository acme/ledger (created in 2024) through an AWS role whose trust policy has these conditions:

{ "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" }
               , "StringLike":   { "token.actions.githubusercontent.com:sub": "repo:acme/ledger:*" } } }

Four workflow runs ask for credentials for that role, each with id-token: write and no environment: a push to main in acme/ledger; a push to the branch fx-rates in acme/ledger; a pull request from fx-rates in acme/ledger; and a push to main in acme/ledger-tools, another repository.

How many of the four get credentials?

Pick one

Question 3 of 6

ExamHall's deploy workflow signs in to AWS with an IAM user's access key stored as a repository secret. The team moves it to GitHub's OIDC provider and a role. After every step, a push to main deploys, and a pull-request run asks for the role and tries the same deploy with it. Every main deploy must work, no pull-request run may ever deploy with the role, and at the end no stored key may work.

In what order do they run these steps?

Your order

  1. 1empty
  2. 2empty
  3. 3empty
  4. 4empty
  5. 5empty

Tap the lines in the order you expect.

Question 4 of 6

At 09:00 a payments ledger's CI prints two jobs' AWS credentials into a build log the whole company can read. The reconcile job ran with an IAM user's access key, created 14 months ago; nobody has deactivated it. The export job ran with a role session it got at 08:55, valid for one hour. At 09:30 a security engineer checking the exposure copies both from the log and, from her own laptop, uses each to call s3:GetObject on an object both identities may read.

Which calls does AWS accept?

Pick one

Question 5 of 6

ExamHall's examhall-auditor role has one identity policy, which allows s3:GetObject on arn:aws:s3:::examhall-reports/term-1/*. The bucket examhall-reports has a bucket policy with one statement: { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::111122223333:role/examhall-auditor" }, "Action": "s3:PutObject", "Resource": "arn:aws:s3:::examhall-reports/audit-notes/*" }. No other policy applies, and the role and the bucket are in one account. The auditor sends two requests to examhall-reports: GetObject on term-1/r-12.json and PutObject on audit-notes/2026-10.txt.

Which of them does AWS allow?

Pick one

Question 6 of 6

A payments ledger's deploy role trusts GitHub's provider with "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", "token.actions.githubusercontent.com:sub": "repo:acme/ledger:ref:refs/heads/main" } (the repository was created in 2024). A push to main starts a workflow with three jobs, each with id-token: write: migrate, with no environment; deploy-staging, with environment: staging; and deploy-prod, with environment: production.

How many of the three jobs get credentials for the role?

Pick one

Go deeper

Pro tip
  1. When asked how a service should get AWS permissions, answer in three parts before anyone asks: a role, not a key; only the actions and prefixes it uses; and for CI, an OIDC trust pinned to the branch or environment that deploys. When: the interviewer asks how a service should get permission to write to storage. Status: SELF; not run.
Source

Valid as of 2026-10-13. Every rule of the three models is quoted from these pages in lib/iam.mjs, lib/oidc.mjs and lib/creds.mjs.

Checked against the tools and docs as of 2026-10-13.