Filter concepts by levelShowing all levels.

AWS · Section 4

IAM — Master This

Level
intermediate
Read
32 min
Concepts
6

IAM decides every allow-or-deny call AWS makes: who is asking, what they want to do, on which resource, and under what condition. This section covers the policy vocabulary and the five policy types, why AWS recommends short-lived role credentials over long-lived user keys, how evaluation logic combines multiple policies (and why an explicit deny always wins), and how to troubleshoot and review access with Access Analyzer rather than guessing.

This section

What is true here

  1. Every IAM policy statement answers who, what action, on which resource (by ARN), under what condition.
  2. AWS recommends roles over long-lived IAM user credentials — federation and IAM Identity Center for humans, IAM roles for workloads.
  3. IAM defaults to deny; an explicit Deny in any applicable policy always overrides every Allow, regardless of source.
  4. Access Analyzer covers three distinct jobs — external access, unused-access analysis, and policy validation — reviewed on a schedule, not once.

What you will be able to do

  • Read any IAM policy statement as identity, action, resource, and condition
  • Tell an identity-based policy, a resource-based policy, a trust policy, a permissions boundary, and a session policy apart
  • Explain why a role with temporary credentials is preferred over an IAM user with long-lived keys
  • Trace how IAM combines multiple applicable policies, and predict when an explicit Deny overrides an Allow
  • Use IAM Access Analyzer to find unused permissions and validate a policy before saving it
How IAM decides allow or deny
is checkedagainstcombined andevaluatedgrantedaccess

Identity requests an action

user, role, or federated principal

Every applicable policy

identity-based, resource-based, boundary, SCP

Evaluated: deny wins, else allow needed

Reviewed on a schedule

Access Analyzer flags unused or excess access

  • Identity requests an action — user, role, or federated principal
    • leads to Every applicable policy (is checked against)
  • Every applicable policy — identity-based, resource-based, boundary, SCP
    • leads to Evaluated: deny wins, else allow needed (combined and evaluated)
  • Evaluated: deny wins, else allow needed
    • leads to Reviewed on a schedule (granted access)
  • Reviewed on a schedule — Access Analyzer flags unused or excess access

IAM

The vocabulary, the five policy types, roles vs users, evaluation logic, and how to troubleshoot and review access.

IAM vocabulary: identities, policies, resources, actions

coreintermediate

An IAM policy is a JSON document that answers one question: can this identity take this action on this resource, under these conditions? Every other IAM concept is a variation on how that document is written or attached.

Think of it as

A policy is a sentence with four blanks: WHO (the identity), CAN DO WHAT (the action), TO WHICH THING (the resource), UNDER WHAT CIRCUMSTANCES (the condition). Every IAM feature — roles, boundaries, SCPs — is a different way of filling in or constraining those same four blanks.

text
Effect + Action + Resource + Condition  →  one statement
Statement[]                              →  one policy (JSON document)

What we're doing: Read a real policy statement as the four elements it is built from.

read-only-reports.jsonjson
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": "arn:aws:s3:::reports-bucket/*",
      "Condition": {
        "IpAddress": { "aws:SourceIp": "203.0.113.0/24" }
      }
    }
  ]
}
5
Effect is Allow — IAM defaults to deny, so this statement exists to open a specific door.
6
Action lists exactly two API operations — not a wildcard, not "all S3 actions."
7
Resource scopes to one bucket by ARN, not every bucket in the account.
9
Condition narrows further: even a matching identity/action/resource is denied outside this IP range.

Why this works: Reading any IAM policy is the same four-question exercise: who, what, on which resource, under what condition. A statement missing Condition simply has no extra restriction — it is not a different kind of policy.

Treating a wildcard Resource as "just for now"

Wrong

json
{ "Effect": "Allow", "Action": "s3:GetObject", "Resource": "*" }

Better

json
{ "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::reports-bucket/*" }

What you see: The policy works immediately during testing, then quietly grants read access to every bucket in the account, including ones created months later.

Why: "*" in Resource is not a placeholder — it matches every object in every bucket the identity could ever reach, forever, not just the one bucket being tested against right now.

What a policy statement evaluates

Identity

who is asking

Action

what they want to do

Resource

which thing, by ARN

Condition

under what circumstances

  1. Identity — who is asking
  2. Action — what they want to do
  3. Resource — which thing, by ARN
  4. Condition — under what circumstances

The four parts of every IAM policy statement

The four parts of every IAM policy statement
ElementAnswersExample value
Principal / identityWho is asking?An IAM user, role, or federated identity
ActionWhat are they trying to do?s3:GetObject, ec2:StartInstances
ResourceOn which specific thing?arn:aws:s3:::reports-bucket/2026/*
ConditionUnder what circumstances?aws:SourceIp, aws:MultiFactorAuthPresent
EffectAllow or deny?Allow (default posture is deny)

Together

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::reports-bucket/2026/*",
      "Condition": {
        "Bool": { "aws:MultiFactorAuthPresent": "true" }
      }
    }
  ]
}

Remember: Every IAM policy answers who, what action, on which resource, under what condition — every other IAM feature is a variation on those four parts.

See also: policy types and trust · least privilege and evaluation

Identity-based, resource-based, trust, boundary, and session policies

coreintermediate

IAM has five policy types: identity-based says what an identity can do, resource-based says who can reach a resource, trust says who may assume a role, boundaries cap what a policy can grant, session policies narrow one session.

Think of it as

Identity-based and resource-based policies are the two ways to say "yes" — attached to the person or attached to the thing. Trust policies are a resource-based policy on a role itself, answering "who may put this role on." Boundaries and session policies never grant anything by themselves; they only cap what an allow elsewhere is permitted to reach.

text
Identity-based  → attached to the WHO
Resource-based  → attached to the WHAT
Trust policy    → resource-based policy on a ROLE itself
Boundary/session → never grant; only cap what an allow elsewhere can reach

What we're doing: See a role built from two different policy types working together: a trust policy controlling who can assume it, and an identity-based policy controlling what it can do once assumed.

deploy-role.jsonjson
// 1. Trust policy — who may assume this role
{
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::111122223333:root" },
    "Action": "sts:AssumeRole",
    "Condition": { "StringEquals": { "sts:ExternalId": "deploy-2026" } }
  }]
}

// 2. Identity-based policy — what it can do once assumed
{
  "Statement": [{
    "Effect": "Allow",
    "Action": ["s3:PutObject"],
    "Resource": "arn:aws:s3:::deploy-artifacts/*"
  }]
}
3
The trust policy is attached to the role, controlling who may call AssumeRole on it — nothing about S3 yet.
12
The identity-based policy is attached to the same role, controlling what it can do after someone has assumed it.

Why this works: A role always has exactly one trust policy (who may assume it) and any number of identity-based policies (what it can do). Confusing the two is the single most common source of "AccessDenied when assuming a role that clearly has S3 permissions."

Adding S3 permissions to the trust policy instead of the identity-based policy

Wrong

json
{ "Effect": "Allow", "Principal": {...}, "Action": ["sts:AssumeRole", "s3:PutObject"] }

Better

json
// Trust policy: only sts:AssumeRole.
// Separate identity-based policy: s3:PutObject on the role.

What you see: The role can be assumed successfully, but every S3 call from within that session fails with AccessDenied.

Why: A trust policy only evaluates sts:AssumeRole (and related STS actions) — any other action listed there is simply never checked, because nothing ever asks the trust policy about S3.

Two different attachment points, two different questions

Identity-based

  • +Attached to a user, group, or role
  • +Answers "what can THIS IDENTITY do"
  • +The policy type most engineers write first

Resource-based

  • Attached to the resource itself (bucket, queue, key)
  • Answers "who may reach THIS RESOURCE"
  • A trust policy is this type, attached to a role
  • Identity-based
    • Attached to a user, group, or role
    • Answers "what can THIS IDENTITY do"
    • The policy type most engineers write first
  • Resource-based
    • Attached to the resource itself (bucket, queue, key)
    • Answers "who may reach THIS RESOURCE"
    • A trust policy is this type, attached to a role

The five IAM policy types

The five IAM policy types
TypeAttached toQuestion it answers
Identity-basedA user, group, or roleWhat can this identity do?
Resource-basedA resource (S3 bucket, SQS queue, KMS key…)Who may reach this resource?
Trust policyA role (a resource-based policy on the role itself)Who is allowed to assume this role?
Permissions boundaryA user or role, alongside its identity-based policyWhat is the maximum this identity could ever be granted?
Session policyPassed when assuming a role (AssumeRole call)What should this one temporary session be limited to?

Together

json
// Trust policy on a role: WHO may assume it
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "lambda.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}

Remember: Identity-based and resource-based policies grant; boundaries and session policies only cap. A trust policy answers "who may assume it," not "what can it do."

See also: iam vocabulary · users roles and federation

Users vs roles, role assumption, and federation

coreintermediate

An IAM user has long-lived credentials; a role has none of its own and is assumed instead, handing out short-lived credentials that expire. AWS now recommends roles for almost everything — federation for humans, IAM roles for workloads.

Think of it as

A user is like a key you cut once and keep forever — it works until someone revokes it. A role is like a hotel keycard dispenser: you request one, it hands you a card that stops working at checkout time, and no permanent key ever left the front desk.

text
aws sts assume-role --role-arn arn:aws:iam::111122223333:role/deploy --role-session-name ci-run

What we're doing: Compare a long-lived IAM user credential to the temporary credentials a role assumption returns.

terminaltext
# IAM user: a long-lived access key, valid until manually rotated or deleted
$ aws configure list
access_key     AKIAIOSFODNN7EXAMPLE     shared-credentials-file

# Role assumption: temporary credentials with a built-in expiry
$ aws sts assume-role --role-arn arn:aws:iam::111122223333:role/ci-deploy \
    --role-session-name build-4821 --query Credentials
{
  "AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
  "Expiration": "2026-08-21T15:42:00Z"
}
2
The IAM user access key has no expiry field — it stays valid until someone acts to rotate or delete it.
6
The assumed-role credential starts with ASIA (not AKIA) and always carries an Expiration timestamp.

Why this works: The access-key prefix is a real, visible signal: AKIA marks a long-lived IAM user key, ASIA marks temporary credentials from a role assumption — useful for spotting a long-lived key where a role should have been used instead.

Creating an IAM user with access keys for a CI/CD pipeline

Wrong

text
$ aws iam create-access-key --user-name ci-pipeline
# stores AKIA... key as a long-lived secret in CI config

Better

text
# Use OpenID Connect federation from the CI provider directly to an IAM role.
# No long-lived secret is ever created or stored.

What you see: The access key works fine for months, then shows up in a security review as a credential with no expiry and no owner who remembers creating it.

Why: Most CI/CD platforms (GitHub Actions, GitLab) support OIDC federation directly to an IAM role — the pipeline gets temporary credentials per run and there is no long-lived secret to rotate, leak, or forget about.

How a temporary credential gets issued
authenticatesviais authorizedtoissues

Human or workload

Identity provider / IAM Identity Center

sts:AssumeRole

Temporary credentials

expires in minutes-to-hours

  • Human or workload
    • leads to Identity provider / IAM Identity Center (authenticates via)
  • Identity provider / IAM Identity Center
    • leads to sts:AssumeRole (is authorized to)
  • sts:AssumeRole
    • leads to Temporary credentials (issues)
  • Temporary credentials — expires in minutes-to-hours

User vs role, side by side

User vs role, side by side
PropertyIAM userIAM role
CredentialsLong-lived (password / access keys)None of its own — issued temporarily on assumption
Lifetime of a sessionUntil explicitly revokedMinutes to a few hours, then expires automatically
Who/what uses itA specific named person or appAnyone/anything the trust policy allows to assume it
Typical use todayNarrow legacy cases only (see mistake below)Human sign-in via federation, and all workloads

Together

text
# Federated human sign-in (via IAM Identity Center) → assumes a role → temporary credentials
# EC2 instance with an instance profile → assumes a role automatically → temporary credentials
# Both paths end the same way: short-lived credentials, no long-term secret to leak

Remember: A user has long-lived credentials; a role has none and must be assumed for short-lived ones. AWS recommends roles over long-lived keys almost everywhere.

See also: policy types and trust · access analyzer and review

Least privilege, explicit deny, and policy evaluation logic

coreintermediate

IAM defaults to deny unless a policy explicitly allows a request. An explicit Deny anywhere applicable always wins, no matter how many policies say Allow. Least privilege means granting only what a task needs, not starting broad.

Think of it as

Think of evaluation as a two-round vote. Round one: does any applicable policy say Allow? If not, denied by default — no vote needed. Round two: does any applicable policy say explicit Deny? If so, that overrides every Allow from round one. An explicit Deny is not one vote among many; it is a veto.

text
No applicable Allow           → implicit deny (the default)
Applicable Allow, no Deny     → allowed
Applicable Allow AND Deny     → denied (explicit Deny always wins)

What we're doing: Trace a request through both the default-deny rule and an explicit deny that overrides an otherwise-valid allow.

two-policies.jsonjson
// Identity-based policy attached to the role: broad read access
{
  "Statement": [{ "Effect": "Allow", "Action": "s3:*", "Resource": "*" }]
}

// SCP attached at the OU level: blocks deletion everywhere, no exceptions
{
  "Statement": [{
    "Effect": "Deny",
    "Action": "s3:DeleteObject",
    "Resource": "*"
  }]
}
3
This Allow is broad — s3:* on every resource — and would normally permit deletes too.
12
This Deny is narrower in scope (just DeleteObject) but wins anyway: an explicit Deny always overrides any Allow, from any policy source.

Why this works: The role can still read and write S3 objects freely — only DeleteObject is blocked, and only because of the explicit Deny, not because the identity-based policy changed at all.

Assuming a broad identity-based Allow guarantees access

Wrong

text
# "The policy has s3:* on Resource: *, so this call must be allowed."

Better

text
# Check every applicable policy for an explicit Deny too — SCPs, RCPs,
# resource-based policies, and permissions boundaries all get a veto.

What you see: A call fails with AccessDenied despite the identity-based policy clearly allowing it.

Why: An identity-based Allow is necessary but not sufficient — any one explicit Deny elsewhere in the evaluation, often an organization-wide SCP the individual engineer cannot see, silently overrides it.

The evaluation order for one request
evaluatedfirstyesnoyesno (implicitdeny)

Request arrives

Any explicit Deny apply?

in SCP, RCP, resource, identity, or boundary

Any explicit Allow apply?

Denied

Allowed

  • Request arrives
    • leads to Any explicit Deny apply? (evaluated first)
  • Any explicit Deny apply? — in SCP, RCP, resource, identity, or boundary
    • leads to Denied (yes)
    • leads to Any explicit Allow apply? (no)
  • Any explicit Allow apply?
    • leads to Allowed (yes)
    • leads to Denied (no (implicit deny))
  • Denied
  • Allowed

How policy evaluation combines across policy types

How policy evaluation combines across policy types
CombinationEffective permission
Identity-based policy aloneWhatever it allows
Identity-based + resource-based (same account)Union — allowed by either grants access
Identity-based + permissions boundaryIntersection — must be allowed by both
Identity-based + SCP/RCP (organization member)Intersection — must be allowed by all
Any explicit Deny, anywhere applicableAlways wins, regardless of any Allow

Together

text
# Identity-based policy: allows s3:GetObject on *
# Permissions boundary:   allows only s3:GetObject on "reports-*" buckets
# Effective permission:   s3:GetObject on "reports-*" buckets only (the intersection)

# Identity-based policy: allows s3:GetObject
# SCP at the OU level:    explicit Deny on s3:DeleteObject for all principals
# Effective permission:   s3:GetObject still works; s3:DeleteObject is blocked everywhere in the OU

Remember: Deny by default; an explicit Deny anywhere applicable wins over any Allow. Grant the narrow set a task needs from the start, not "broad now, narrow later."

See also: iam vocabulary · arns conditions and troubleshooting

ARNs, conditions, wildcards, and troubleshooting access

standardintermediate

An ARN is the address of one AWS resource, and it is what a policy's Resource field matches against. A wildcard broadens that match; a Condition narrows it. When access breaks, use the policy simulator instead of guessing.

Think of it as

An ARN is a full postal address, down to the resource: service, region, account, and resource path, in one fixed format. A wildcard says "any street in this city"; a Condition says "but only if it's a weekday." Troubleshooting access is reading that address and those conditions back, in order, rather than guessing which policy is wrong.

text
arn:partition:service:region:account-id:resource
s3:Get*        matches s3:GetObject, s3:GetBucketPolicy, ...
s3:GetObject   matches exactly one action, nothing else

What we're doing: Compare a wildcard Resource ARN against an exact one, and see what each actually matches.

wildcard-scope.jsonjson
// Wildcard: matches every object in every bucket the account owns
{ "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::*/*" }

// Exact: matches only objects under this one prefix, in this one bucket
{
  "Effect": "Allow",
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::reports-bucket/2026/*"
}
2
Two wildcards — bucket name and object key are both "*" — so this matches literally every object in the account.
7
One wildcard, scoped to a single bucket and a single year-prefix — the ARN itself does the narrowing, no extra Condition needed.

Why this works: A wildcard's position matters as much as its presence — "*/*" and "reports-bucket/2026/*" are both technically wildcards, but they authorize wildly different amounts of access.

Debugging AccessDenied by adding permissions until it works

Wrong

text
# AccessDenied → attach AdministratorAccess → works → move on

Better

text
# Use IAM Access Analyzer policy validation, or the IAM policy simulator,
# to see exactly which statement is missing or which Deny is firing.

What you see: The immediate task unblocks, but the identity now holds far more access than the task ever needed, and nobody narrows it back down afterward.

Why: AccessDenied errors and CloudTrail's event history identify the specific action and resource that were denied — the policy simulator and Access Analyzer validation exist specifically so the fix can be as narrow as the original request, not a blanket grant.

Reading an ARN, field by field

Reading an ARN, field by field
FieldExample valueMeaning
partitionaws"aws" (standard), "aws-cn", or "aws-us-gov"
services3Which AWS service owns this resource
region(often empty for S3)Region the resource lives in — global services omit it
account-id111122223333The AWS account that owns the resource
resourcereports-bucket/2026/q1.csvThe specific resource, service-defined format

Together

text
arn:aws:s3:::reports-bucket/2026/q1.csv
arn:aws:ec2:eu-west-1:111122223333:instance/i-0a1b2c3d
arn:aws:iam::111122223333:role/deploy

# S3 and IAM omit region (S3 buckets are named globally; IAM is account-wide)
# EC2 always includes region — the same instance ID format exists in every region

Remember: An ARN is the address a Resource field matches — a wildcard broadens it, a Condition narrows it. Debug AccessDenied with the simulator, not more grants.

See also: least privilege and evaluation · access analyzer and review

IAM Access Analyzer and ongoing permission review

standardintermediate

IAM Access Analyzer flags resources reachable from outside the account, checks a policy's JSON for security issues before saving, and identifies permissions granted but never used — so unused access is found and removed on a schedule.

Think of it as

A building's security review, done on a schedule instead of never: which doors can outsiders reach (external access findings), are the locks installed correctly (policy validation), and which badges have not been used in ninety days (unused-access analysis). None of these three questions has an obvious owner unless something is run on purpose to ask them.

text
aws accessanalyzer create-analyzer --analyzer-name <name> --type ACCOUNT|ORGANIZATION
aws accessanalyzer list-findings --analyzer-arn <arn>

What we're doing: Create an analyzer and read back a finding — the shape every Access Analyzer capability shares: create once, review findings on a schedule.

terminaltext
# 1. Create the analyzer once (account- or organization-scoped)
$ aws accessanalyzer create-analyzer --analyzer-name prod-external --type ACCOUNT

# 2. Review findings — repeated, on a schedule, not a one-time check
$ aws accessanalyzer list-findings --analyzer-arn arn:aws:access-analyzer:eu-west-1:111122223333:analyzer/prod-external
{
  "findings": [
    { "resource": "arn:aws:s3:::reports-bucket", "isPublic": true, "action": ["s3:GetObject"] }
  ]
}
2
The analyzer is created once and then runs continuously — it is not a one-off scan.
6
Each finding names the exact resource, whether it is public, and which actions are exposed — specific enough to act on directly.

Why this works: Access Analyzer's value is in the schedule, not the single check — a bucket policy edited next month to add public access generates a fresh finding automatically, without anyone remembering to re-run a scan.

Treating "no findings today" as a permanent state

Wrong

text
# Ran Access Analyzer once during setup, zero findings, considered it done.

Better

text
# Route findings to a notification channel (EventBridge → chat/ticket)
# so a new finding is reviewed when it appears, not discovered months later.

What you see: A bucket policy changed six months ago now grants public access, and nobody has looked at Access Analyzer since the initial setup.

Why: An analyzer evaluates continuously and generates new findings as configurations change — but only a human or an automated pipeline reading those findings turns that into an actual security benefit.

What IAM Access Analyzer actually checks

What IAM Access Analyzer actually checks
CapabilityWhat it flags
External access analysisResources (buckets, roles, keys…) reachable from outside the account or org
Unused access analysisRoles, permissions, and credentials granted but not used within a set window
Policy validationJSON syntax errors, overly permissive patterns, and 100+ known-bad constructs
Policy generationA draft least-privilege policy, built from a role's real CloudTrail activity

Together

text
$ aws accessanalyzer create-analyzer --analyzer-name org-wide --type ORGANIZATION
$ aws accessanalyzer list-findings --analyzer-arn arn:aws:access-analyzer:...
# → a bucket policy grants s3:GetObject to a principal outside the account

Remember: Temporary credentials over long-lived keys, MFA for humans, least privilege from the start, scheduled review — Access Analyzer is the tool for that last one.

See also: arns conditions and troubleshooting · users roles and federation

Advertisement