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?
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
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?
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?
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
- 1empty
- 2empty
- 3empty
- 4empty
- 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?
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?
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?
Go deeper
Pro tip
- 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.
- AWS IAM, Policy evaluation logic (named in the row): https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic.html ·
aws-iam-eval-logic.txt. "If an action is allowed by an identity-based policy, a resource-based policy, or both, then AWS allows the action. An explicit deny in either of these policies overrides the allow." Supports steps 1, 3. - AWS IAM, How AWS enforcement code logic evaluates requests to allow or deny access (linked from that page): https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic_policy-eval-denyallow.html ·
aws-iam-eval-denyallow.txt. "By default, all requests are implicitly denied"; "An explicit deny overrides an explicit allow."; "If the enforcement code finds even one explicit deny that applies, the enforcement code returns a final decision of Deny."; "Depending on the type of principal, an Allow in a resource-based policy can result in a final decision of Allow, even if an implicit deny in an identity-based policy, permissions boundary, or session policy is present."; "Resource-based policies that grant permissions to an IAM role ARN are limited by an implicit deny in a permissions boundary or session policy." Supports steps 1–3; the model has no permissions boundaries or session policies. - AWS IAM, Security best practices in IAM (named in the row): https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html ·
aws-iam-best-practices.txt. "Require workloads to use temporary credentials with IAM roles to access AWS"; "When you set permissions with IAM policies, grant only the permissions required to perform a task." Supports steps 6, 7. - GitHub, Configuring OpenID Connect in Amazon Web Services (named in the row): https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-aws ·
gh-oidc-aws.txt. "Evaluating this condition key in the role trust policy limits which GitHub actions are able to assume the role."; "If you use a workflow with an environment, the sub field must reference the environment name"; "The job or workflow run requires a permissions setting with id-token: write"; "StringLike is used with a wildcard operator (*) to allow any branch, pull request merge branch, or environment". Supports steps 10–12. - GitHub, OpenID Connect reference: https://docs.github.com/en/actions/reference/security/oidc ·
gh-oidc-reference.txt. "The subject claim includes the environment name when the job references an environment."; "The subject claim includes the pull_request string when the workflow is triggered by a pull request event, but only if the job doesn't reference an environment."; "repositories created after July 15, 2026 now use an immutable default subject format that includes both the owner ID and repository ID." Supports step 11; the stems' repositories were created in 2024, so the name-only format applies. - AWS IAM, IAM JSON policy elements: Resource and Action: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_resource.html ·
aws-iam-resource.txt; https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_action.html ·aws-iam-action.txt. "The asterisk (*) character can expand to replace everything within a segment, including characters like a forward slash (/)"; "The prefix and the action name are case insensitive." Supports steps 4, 5. - AWS IAM, Condition operators, Condition and multiple context keys or values: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html ·
aws-iam-condition-ops.txt; https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html ·aws-iam-condition.txt; https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-logic-multiple-context-keys-or-values.html ·aws-iam-condition-multi.txt. "Case-sensitive matching. The values can include multi-character match wildcards (*) and single-character match wildcards (?) anywhere in the string."; "the condition operators are evaluated using a logical AND." Supports steps 10–12. - AWS IAM, Temporary security credentials in IAM and Manage access keys for IAM users: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html ·
aws-iam-temp-creds.txt; https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html ·aws-iam-access-keys.txt. "After the credentials expire, AWS no longer recognizes them or allows any kind of access from API requests made with them."; "Temporary security credentials work almost identically to long-term access key credentials"; "Access keys are long-term credentials for an IAM user or the AWS account root user."; "By doing this, you might give someone permanent access to your account." Supports steps 7–9. - AWS STS API Reference, AssumeRoleWithWebIdentity: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html ·
aws-sts-arwwi.txt. "By default, the temporary security credentials created by AssumeRoleWithWebIdentity last for one hour." Supports step 7.
Checked against the tools and docs as of 2026-10-13.