AWS Basics · Lesson 326 min read

IAM, in Depth

How AWS actually decides who can do what, the request model, the policy types, the evaluation logic, roles and AssumeRole, and real least privilege.

From your mentor

IAM is the topic interviewers hammer hardest, and the one that explains every 'Access Denied' you'll ever hit. We'll build the decision model from first principles, then climb from AdministratorAccess to provable least privilege.

Every AWS permission question is one decision, allow or deny, run on every request.

In 12 minutes you’ll trace exactly why a call was allowed or denied. You do NOT need to memorize every action name.

Pick your way in, same idea, 5 doors

Every time anything touches AWS, a bouncer checks a rulebook and says yes or no. IAM is just that bouncer and its rules. Learn how the bouncer reads the rules and nothing about permissions stays mysterious.

Next: run a real request through the evaluation engine and watch an explicit deny win.
01

The one question IAM answers

Billions of times a day, IAM answers exactly one thing: can this principal perform this action on this resource, under these conditions? Everything else is detail.

Every request to AWS carries four things. Internalise these and the rest of IAM falls into place.

ElementQuestion it answersExample
PrincipalWho is asking?The role DeployRole
ActionWhat do they want to do?s3:GetObject
ResourceOn what?arn:aws:s3:::my-bucket/*
ConditionUnder what constraints?MFA present, aws:SourceIp, time of day

Deny by default

Nothing is permitted until a policy explicitly allows it, and an explicit deny anywhere overrides any allow. That single rule is the backbone of everything below. (The principals themselves, root, IAM user, role, Identity Center, we covered in Lesson 1.)
02

The policy types, and which grant vs cap

Permissions come from JSON policies attached to identities or resources. Five types decide real requests, and the crucial split is: some grant access, some only set a ceiling.

Policy typeAttached toGrants or caps?Use it for
Identity-basedUser, group, or roleGrantsThe everyday “what can this identity do”
Resource-basedA resource (S3 bucket, SQS, role trust)Grants (+ cross-account)Who outside the identity may touch this resource
Permissions boundaryA user or roleCaps (maximum)Limit what an identity-based policy can ever grant
SCP (Organizations)An account or OUCaps (maximum)Org-wide guardrails across many accounts
Session policyA temporary sessionCapsShrink permissions for one assumed session

Grant vs cap, the mental shortcut

Identity- and resource-based policies hand out permissions. Boundaries, SCPs, and session policies never grant anything, they only set a ceiling. So: effective permissions = what’s granted, intersected with every ceiling, minus any explicit deny.
03

How a request is actually decided

When a request arrives, AWS runs a fixed procedure. Memorise its shape, it's the #1 IAM interview topic and the thing that explains every Access Denied.

  1. 1

    Authenticate

    Who is the principal? (some S3 calls allow anonymous)

  2. 2

    Build request context

    Principal, action, resource, conditions

  3. 3

    Evaluate all policies

    Every applicable policy type, by the rules below

  4. 4

    Allow or deny

    Explicit deny wins; else allow if granted; else implicit deny

Inside that third step, the policy types combine by clear, fixed rules:

CheckRule
Explicit denyA Deny in ANY policy type → request denied. Always wins.
Identity + resource (same account)Union, an Allow in either is enough
Permissions boundaryIntersection, must be allowed by the identity policy AND the boundary
Organizations SCP / RCPIntersection, must also be allowed by the SCP (SCPs only cap)
Session policyIntersection, further narrows an assumed session
Nothing matchedImplicit deny, if no policy allows it, it’s denied

The two rules that explain every Access Denied

1. Explicit deny always wins, over any allow, anywhere. 2. SCPs and boundaries only take away, they never grant. So when you have an Allow but still get denied, hunt for an explicit Deny, a permissions boundary, or an SCP capping you.
04

Users & groups, and why roles usually win

Groups manage human permissions cleanly. But AWS's own guidance is to prefer roles and Identity Center, and create IAM users only when nothing else works.

Attach policies to a group, add users to the group, never paste policies onto individuals one by one. That said, a long-lived IAM user is now the exception, not the default:

Create an IAM user only when…Why
A workload truly can’t assume a roleSome legacy or off-AWS tools can’t use temporary credentials
A third-party client doesn’t support Identity CenterNo federation path available
Break-glass / emergency accessWhen your identity provider is unreachable, lock it down with MFA

Everything else is a role

For applications, CI, cross-account access, and normal human sign-in, you use roles (humans via Identity Center). That’s the next section, and the heart of IAM.
05

Roles & AssumeRole, the heart of IAM

A role is an identity with permissions but no long-term credentials. You assume it via STS and get temporary credentials for a session. Apps, CI, and cross-account access all run on roles.

Every role has two policies, and confusing them is the classic AssumeRole failure:

Policy on a roleAnswersType
Trust policyWHO may assume this roleResource-based (required)
Permissions policyWHAT the role can do once assumedIdentity-based
trust-policy.json (who may assume)
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "ec2.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}
AWS docs: IAM roles terms & concepts
permissions-policy.json (what it can do)
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["s3:GetObject", "s3:ListBucket"],
    "Resource": [
      "arn:aws:s3:::app-assets",
      "arn:aws:s3:::app-assets/*"
    ]
  }]
}

Assuming a role returns temporary credentials (access key, secret, and a session token):

aws cli
# returns short-lived AccessKeyId / SecretAccessKey / SessionToken
$ aws sts assume-role --role-arn arn:aws:iam::123456789012:role/DeployRole --role-session-name demo
AWS docs: sts assume-role
ScenarioHow the role is used
App on EC2 / LambdaAttach a role (instance profile / execution role), no keys on the box
Cross-account accessA role in the target account trusts the other account; require an ExternalId for third parties
CI/CD (GitHub Actions)OIDC federation assumes a role, no AWS keys stored in the repo
HumansIdentity Center assigns a permission set, which is a role, per account

Role chaining caps at 1 hour

Assume a role from another role and the session maxes out at 60 minutes, regardless of the role’s configured maximum. A direct AssumeRole can run up to 12 hours.

Cross-account? Use an ExternalId

When a third party assumes a role in your account, require an ExternalId in the trust policy. It defeats the “confused deputy” problem, they can’t be tricked into acting against your account.
06

Least privilege in practice

Least privilege = grant only the actions and resources actually needed. You don't nail it on the first try, you start tight and refine with real data.

Permissionsthe climb
  1. Rung 0 · Naive

    AdministratorAccess on everything

    Attach the admin managed policy to the identity and move on.

    One leaked credential = the whole account. No blast-radius limit at all.
  2. Better

    Scoped AWS managed policies

    Attach job-specific managed policies (AmazonS3ReadOnlyAccess, etc.) instead of admin.

    Far smaller blast radius, still convenient, a sane default while you learn what's actually needed.
  3. Best practice today

    Least-privilege custom policy + boundary

    Generate a policy from real CloudTrail activity, scope it to specific resources and conditions, and cap the identity with a permissions boundary.

    The identity can do exactly its job and nothing more, and you can prove it.

AWS gives you the tooling to climb that last rung without guessing:

ToolWhat it does
Access Analyzer, policy generationBuilds a least-privilege policy from your CloudTrail activity (free)
Last-accessed informationShows services/actions an identity hasn’t used, prune them
Access Analyzer, policy validationLints policies for errors and overly-broad grants
Conditions / ABACTighten with conditions (aws:SourceIp, resource tags) instead of more policies
aws cli
# generate a least-privilege policy from observed CloudTrail activity
$ aws accessanalyzer start-policy-generation --policy-generation-details '{...}'
AWS docs: accessanalyzer start-policy-generation
07

How this shows up in interviews

Interview angle

Walk me through how AWS decides whether a request is allowed.

How to answer it

This is THE IAM question. Give the flow, then the two golden rules, then name the policy types, that ordering shows real understanding.

  1. 1AWS authenticates the principal and builds the request context: principal, action, resource, conditions.
  2. 2Default is implicit deny, nothing is allowed unless a policy allows it.
  3. 3It gathers all applicable policy types and applies them: an explicit Deny in any of them wins immediately.
  4. 4Identity-based and resource-based policies are a union (allow in either is enough, same account).
  5. 5Then it intersects with the permissions boundary, the Organizations SCP/RCP, and any session policy, each can only narrow.
  6. 6If a remaining Allow applies and nothing denies it, the request is allowed; otherwise implicit deny.

Green flags

  • Leads with deny-by-default and “explicit deny wins”
  • Knows SCPs/boundaries cap but never grant
  • Distinguishes union (identity+resource) from intersection (boundary/SCP)

Red flags

  • Thinks an Allow always means access
  • Confuses trust policy with permissions policy
  • Reaches for AdministratorAccess as the answer to everything

Q.Trust policy vs permissions policy on a role?

A.Trust = who may assume the role; permissions = what it can do once assumed.

Trust policy
Permissions policy
Resource-based, lives on the role
Identity-based, attached to the role
Answers: WHO can assume it
Answers: WHAT it can do
Checked at AssumeRole
Checked on every API call after

They’re checking: That you don’t conflate the two, the classic AssumeRole failure.

Q.When would you use an IAM user instead of a role?

A.Almost never, only when roles/federation genuinely can’t be used.

  • Legacy or off-AWS tools that can’t assume a role.
  • Third-party clients without Identity Center support.
  • Tightly-controlled break-glass access.

They’re checking: That “a user” is your last resort, not your default.

Q.An identity has Allow for s3:* but still gets AccessDenied, why?

A.Something higher is capping or denying it, an Allow is necessary but not sufficient.

  • An explicit Deny in any policy (always wins).
  • A permissions boundary that doesn’t include the action.
  • An Organizations SCP ceiling above the account.

They’re checking: That you check the deny / boundary / SCP ceilings, not just the identity policy.

Q.How do you build a least-privilege policy in practice?

A.Start scoped, run it, then prune from real activity, don’t hand-write from scratch.

  • Begin with a scoped managed policy, not AdministratorAccess.
  • Use Access Analyzer to generate a policy from CloudTrail + last-accessed data.
  • Scope to specific resources + conditions; optionally cap with a permissions boundary.

They’re checking: That you use last-accessed / Access Analyzer data, not guesswork.

Q.How does GitHub Actions get AWS access without stored keys?

A.OIDC federation: a role trusts GitHub’s OIDC provider and the workflow assumes it for short-lived creds.

  • The trust policy conditions on the repo (sub) and audience (aud).
  • Nothing long-lived lives in the repo, the creds expire with the job.

They’re checking: That you reach for OIDC roles over storing AWS keys in CI secrets.

08

Your turn, build real IAM

As your IAM admin user, build a group, a scoped policy, and a role you actually assume. All free, about 20 minutes.

Now do it in your own account

~20 min $0, IAM is free

Sign in as your IAM admin user from Lesson 1, then build a group, a scoped policy, and a role you actually assume. IAM is free; this takes ~20 minutes.

Before you start

3 to have ready

Your IAM admin user from Lesson 1 (not root).

AWS CLI v2 signed in.

aws sts get-caller-identity

Your 12-digit account ID handy, you’ll paste it into the role ARNs.

  1. 1

    Create a “developers” group with a scoped managed policy (not AdministratorAccess).

    Free tier

    Your terminal

    aws cli
    $ aws iam attach-group-policy --group-name developers --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
    AWS docs: iam attach-group-policy

    You should see: the group lists AmazonS3ReadOnlyAccess, not full admin.

  2. 2

    Create a role with a trust policy (who can assume) and a permissions policy (what it can do).

    Free tier

    Console → IAM → Roles → Create role

    You should see: the role exists with both a trust policy and a permissions policy.

  3. 3

    Assume the role from the CLI.

    Free tier

    Your terminal

    aws cli
    $ aws sts assume-role --role-arn arn:aws:iam::<acct>:role/DemoRole --role-session-name demo
    AWS docs: sts assume-role

    You should see: a Credentials block (AccessKeyId, SecretAccessKey, SessionToken) that expires.

  4. 4

    With those temporary credentials, confirm the session identity.

    Free tier

    Your terminal

    aws cli
    $ aws sts get-caller-identity
    AWS docs: sts get-caller-identity

    You should see: the ARN is assumed-role/DemoRole/demo, not your user.

  5. 5

    Review last-accessed data to find permissions never used.

    Free tier

    Your terminal

    aws cli
    $ aws iam generate-service-last-accessed-details --arn arn:aws:iam::<acct>:role/DemoRole
    AWS docs: iam generate-service-last-accessed-details

    You should see: a JobId; fetch it to see which services the role has never touched, then prune.

  6. 6

    Optional: have Access Analyzer generate a least-privilege policy from your activity.

    Free tier

    Console → IAM → Access Analyzer

    You should see: a generated policy scoped to what you actually used.

Last step: tear it down

Once you’ve seen it work, remove everything so nothing keeps billing.

Delete the test role when done.

aws iam delete-role --role-name DemoRole

IAM groups, roles, and policies are free, but don’t leave an over-permissioned role lying around, it’s a standing risk.

Confirm no access keys exist for the role, roles use temporary creds, not long-term keys.

Next up

Next, Compute: EC2, instance families & Lambda

Launch and right-size EC2, read the instance-family naming, and learn when a serverless function beats an always-on box.