Engineering Notes
Home/DevOps & Cloud/AWS/IAM Deep Dive
DevOps & Cloud · AWS

IAM Deep Dive

Go beyond basic policies: principals and STS, the full evaluation logic across policy types, conditions, cross-account access, federation, permissions boundaries, ABAC, and the privilege-escalation paths attackers look for.

☁️ DevOps & Cloud● Advanced⏱ 8 min read▶ 2 interactive🛠️ 3 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • Principals, trust policies and STS temporary credentials
  • All policy types and the complete evaluation logic
  • Least privilege as a process, cross-account and federation
  • Boundaries, ABAC, escalation paths (iam:PassRole) and monitoring
Part 1Principals, Policies and Evaluation

01-fundamentals_and_iam/ introduced users, groups, roles and policies. This deep dive covers how authorization decisions are actually made, how roles and temporary credentials work, and the policy types that matter in real organizations.

1Principals and Credentials

PrincipalCredentialsUse
IAM userlong-lived access keys / passwordavoid for humans and workloads; legacy or break-glass only
IAM roletemporary credentials from STS (15 min – 12 h)workloads (EC2/ECS/Lambda/EKS), cross-account access, humans via SSO, CI via OIDC
federated identityIAM Identity Center (SSO), SAML, OIDC → role sessionhumans
AWS service principallambda.amazonaws.com, ecs-tasks.amazonaws.comin trust policies
root useraccount ownerlock away with MFA; almost never used

2A Role Has Two Policies

json
// TRUST policy — WHO may assume the role (resource policy on the role)
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "ecs-tasks.amazonaws.com" },
    "Action": "sts:AssumeRole",
    "Condition": { "ArnLike": { "aws:SourceArn": "arn:aws:ecs:eu-west-1:111111111111:*" } }
  }]
}

// PERMISSIONS policy — WHAT the role may do once assumed
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["s3:GetObject", "s3:PutObject"],
    "Resource": "arn:aws:s3:::petpal-uploads/*"
  }]
}
💡 Example: trust policy vs permissions policy
json
// trust policy: WHO may assume the role (GitHub OIDC, one repo, main branch)
{ "Effect": "Allow",
  "Principal": { "Federated": "arn:aws:iam::111111111111:oidc-provider/token.actions.githubusercontent.com" },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": { "StringEquals": {
    "token.actions.githubusercontent.com:sub": "repo:petpal/api:ref:refs/heads/main" } } }

// permissions policy: WHAT the role may do once assumed
{ "Effect": "Allow", "Action": ["ecr:PutImage", "ecs:UpdateService"], "Resource": "…" }

3Policy Types

TypeAttached toEffect
identity-based (managed or inline)users, groups, rolesgrants permissions
resource-basedS3 buckets, KMS keys, SQS queues, SNS topics, Lambda functions, Secrets, role trust policiesgrants access to principals (incl. other accounts)
permissions boundaryusers/rolescaps what identity policies can grant
SCP (Service Control Policy)AWS Organizations OU/accountcaps everything in the account (even root) — 23-multi_account_and_organizations/
RCP (Resource Control Policy)Organizationscaps what resource policies can allow (e.g. no access from outside the org)
session policypassed when assuming a rolefurther restricts that session
ACLslegacy S3/VPCavoid

4The Evaluation Logic

For a request in a single account:

text
1. explicit DENY in any applicable policy?            → DENY  (deny always wins)
2. SCPs / RCPs allow it?                              → if not, DENY
3. resource-based policy allows the principal?         → (can grant on its own within the same account)
4. identity policy allows it?                          → needed unless 3 granted it
5. permissions boundary allows it?                     → must also allow (for identity grants)
6. session policy allows it?                           → must also allow
default                                                → implicit DENY

Cross-account: the request must be allowed by both the caller's identity policy (in account A) and the resource policy in account B (or the role trust policy, if assuming a role in B).

5Statement Anatomy and Conditions

json
{
  "Sid": "ReadOwnTenantData",
  "Effect": "Allow",
  "Action": ["dynamodb:GetItem", "dynamodb:Query"],
  "Resource": "arn:aws:dynamodb:eu-west-1:111111111111:table/helpdesk",
  "Condition": {
    "ForAllValues:StringLike": { "dynamodb:LeadingKeys": ["TENANT#${aws:PrincipalTag/tenant}*"] },
    "Bool": { "aws:SecureTransport": "true" }
  }
}

Useful condition keys: aws:PrincipalTag/* and aws:ResourceTag/* (ABAC — attribute-based access control), aws:SourceIp, aws:SourceVpce, aws:PrincipalOrgID, aws:SecureTransport, aws:MultiFactorAuthPresent, aws:RequestedRegion, service-specific keys (s3:prefix, kms:ViaService, ec2:InstanceType). Policy variables (${aws:username}, ${aws:PrincipalTag/team}) let one policy serve many principals.

NotAction / NotResource invert matches — powerful and easy to get wrong (an Allow with NotAction grants almost everything).

Under the HoodSTS and Temporary Credentials▾

sts:AssumeRole (or AssumeRoleWithWebIdentity for OIDC tokens, AssumeRoleWithSAML for SAML) returns three values: AccessKeyId (starting with ASIA… for temporary keys), SecretAccessKey, and a SessionToken that encodes the role, session name, tags, session policy and expiry. Every API call is signed with SigV4 using the secret key; the receiving service validates the signature and token, then evaluates policies for the role session principal (arn:aws:sts::111111111111:assumed-role/RoleName/SessionName). Because credentials expire automatically, a leaked session key is useful only until expiry (and can be revoked by denying sessions issued before a time via aws:TokenIssueTime). SDKs refresh credentials transparently from the instance/task metadata endpoints or the OIDC token file — which is why workloads should never be given long-lived keys.

A request is evaluated by checking explicit denies, then service control and resource control policies, then resource-based and identity-based policies, then permissions boundaries and session policies, defaulting to deny; roles combine a trust policy with permissions and hand out temporary STS credentials
A request is evaluated by checking explicit denies, then service control and resource control policies, then resource-based and identity-based policies, then permissions boundaries and session policies, defaulting to deny; roles combine a trust policy with permissions and hand out temporary STS credentials

📋 Quick Reference (Part 1)

text
humans: SSO → roles · workloads: roles (instance profile, task role, Lambda role, Pod Identity) · CI: OIDC → role
role = trust policy (who) + permissions (what) · STS temporary creds (ASIA…)
evaluation: explicit deny > SCP/RCP > resource ∪ identity > boundary > session > implicit deny
cross-account: caller identity policy AND target resource/trust policy
conditions: PrincipalTag/ResourceTag (ABAC), PrincipalOrgID, SourceVpce, SecureTransport, MFA, ViaService
Part 2Least Privilege in Practice

1Least Privilege Is a Process

Nobody writes perfect policies up front. The practical loop:

  1. Start from AWS managed policies only for prototypes; for production write customer managed policies scoped to specific actions and resource ARNs.
  2. Use IAM Access Analyzer: policy validation (syntax, security warnings), policy generation from CloudTrail activity ("generate a policy from what this role actually did in the last 90 days"), unused access findings (roles, keys, permissions not used), and external access findings (resources shared outside the account/org).
  3. Use last accessed data (service and action level) to remove unused permissions.
  4. Enforce guardrails centrally with SCPs, permissions boundaries and RCPs.

2Cross-Account Access Patterns

text
account A (CI/tools)                       account B (prod)
role ci-deployer ── sts:AssumeRole ──►  role deploy-prod
                                         trust: { "Principal": { "AWS": "arn:aws:iam::A:role/ci-deployer" },
                                                  "Condition": { "StringEquals": { "sts:ExternalId": "…" } } }   ← ExternalId for third parties
  • Prefer role assumption over sharing resources with resource policies, except for things like S3/KMS where resource policies are natural (with aws:PrincipalOrgID conditions).
  • ExternalId protects against the confused deputy problem when a third party (a SaaS vendor) assumes roles in many customers' accounts.
  • aws:SourceArn / aws:SourceAccount conditions protect service principals from confused-deputy issues (e.g. SNS publishing to your SQS queue).

3Federation for Humans and CI

  • IAM Identity Center (SSO): users from your IdP (Okta, Entra ID, Google) get permission sets in specific accounts; short-lived role sessions via the CLI (aws sso login) — no IAM users (security/security/09-identity_providers).
  • GitHub Actions / GitLab OIDC: the CI system's OIDC token is exchanged for role credentials with conditions on the repository, branch or environment (devops-cloud/terraform/07-terraform_in_ci_cd).

4Permissions Boundaries: Safe Delegation

Let developers create roles for their Lambda functions without being able to escalate privileges:

json
// developers may create roles only if they attach this boundary
{
  "Effect": "Allow",
  "Action": ["iam:CreateRole", "iam:PutRolePolicy", "iam:AttachRolePolicy"],
  "Resource": "arn:aws:iam::111111111111:role/app/*",
  "Condition": { "StringEquals": { "iam:PermissionsBoundary": "arn:aws:iam::111111111111:policy/AppBoundary" } }
}

The boundary caps effective permissions of any role they create, no matter what policy they attach.

5ABAC: Scaling With Tags

Instead of one policy per team per resource, tag principals and resources and write one policy:

json
{
  "Effect": "Allow",
  "Action": ["secretsmanager:GetSecretValue"],
  "Resource": "*",
  "Condition": { "StringEquals": { "aws:ResourceTag/team": "${aws:PrincipalTag/team}" } }
}

New team members and new secrets need only tags. Protect the tags themselves (deny iam:TagRole/*:TagResource for the controlling tag keys except to admins).

6Privilege Escalation Paths to Block

Permissions that let a principal grant itself more: iam:CreatePolicyVersion, iam:SetDefaultPolicyVersion, iam:AttachUserPolicy/AttachRolePolicy, iam:PutUserPolicy, iam:UpdateAssumeRolePolicy, iam:PassRole (to a more privileged role) combined with lambda:CreateFunction/ec2:RunInstances/ecs:RunTask, iam:CreateAccessKey for other users. Scope iam:PassRole to specific role ARNs and iam:PassedToService.

7Monitoring

  • CloudTrail (organization trail, all regions, log file validation) — who did what.
  • Alerts on root usage, IAM policy changes, access key creation, AssumeRole from unexpected sources, AccessDenied spikes.
  • GuardDuty for anomalous credential use (keys used from unusual locations, instance credentials used outside AWS).
Under the HoodHow iam:PassRole Works▾

Many services act as a role you choose: Lambda runs with its execution role, ECS tasks with task roles, EC2 with an instance profile, CloudFormation with a service role. When you create such a resource, the service doesn't check what the role can do — it checks whether you are allowed to iam:PassRole that role to that service. PassRole isn't an API call; it's a permission evaluated implicitly during CreateFunction, RunInstances, RegisterTaskDefinition, etc. If it's granted on *, any user who can create a Lambda function can attach the account's most powerful role to it and run arbitrary code with those permissions — one of the most common escalation paths. Scope it: "Resource": "arn:aws:iam::111111111111:role/app/*" with "Condition": { "StringEquals": { "iam:PassedToService": "lambda.amazonaws.com" } }.

Least privilege as a loop: write scoped policies, validate and generate them with Access Analyzer from CloudTrail activity, remove unused access, and enforce guardrails with SCPs, permissions boundaries and ABAC tags; cross-account access uses role trust with ExternalId and organization conditions
Least privilege as a loop: write scoped policies, validate and generate them with Access Analyzer from CloudTrail activity, remove unused access, and enforce guardrails with SCPs, permissions boundaries and ABAC tags; cross-account access uses role trust with ExternalId and organization conditions

⚠️ Common Pitfalls (Part 2)

  1. Action: "*" / Resource: "*" in workload roles.
  2. IAM users with access keys for humans or apps.
  3. iam:PassRole on *.
  4. Trust policies with "Principal": {"AWS": "*"} (anyone!) or missing ExternalId for vendors.
  5. Ignoring Access Analyzer external-access findings (buckets/keys shared publicly).
  6. Unprotected ABAC tags — users change their own tags to gain access.

📋 Quick Reference (Part 2)

text
Access Analyzer: validate · generate from CloudTrail · unused access · external access
cross-account: role assumption, ExternalId for third parties, PrincipalOrgID, SourceArn/SourceAccount
humans: Identity Center permission sets · CI: OIDC with repo/branch conditions
delegation: permissions boundaries · scale: ABAC (PrincipalTag = ResourceTag) · block escalation: scope PassRole

🧠 Key takeaways

  • A role has a trust policy (who can assume it) and permissions policies (what it can do); STS issues temporary credentials.
  • Evaluation: explicit deny > SCP/RCP limits > resource policy > boundary > session policy > identity policy; default deny.
  • Least privilege is a loop: start narrow, observe (Access Analyzer, last-accessed), refine; federate humans and CI via SSO/OIDC.
  • Use permissions boundaries for delegation, ABAC tags to scale, and block escalation paths like broad iam:PassRole.

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1Identity-based vs resource-based policies?core▾

Identity-based policies attach to users/roles and say what they can do. Resource-based policies attach to resources (S3 buckets, KMS keys, SQS queues) and say who can access them; they enable cross-account access without assuming a role.

Follow-ups interviewers ask
  • Cross-account access needs? Permission on both sides unless the resource policy grants the external principal directly.
Q2How does assuming a role work?core▾

A principal calls sts:AssumeRole; the role's trust policy must allow that principal; STS returns temporary credentials with the role's permissions (optionally narrowed by a session policy).

Follow-ups interviewers ask
  • What's an external ID? A condition preventing the confused-deputy problem for third-party access.
Q3How do you write least-privilege policies?senior▾

Grant specific actions on specific resource ARNs, use conditions (tags, source VPC, MFA), start from access analyzer-generated policies based on CloudTrail activity, and review with last-accessed data.

Follow-ups interviewers ask
  • What's ABAC in AWS? Policies matching principal tags to resource tags to scale permissions.
Q4What are SCPs?core▾

Service Control Policies in AWS Organizations set the maximum permissions for accounts in an OU; they don't grant permissions, only restrict. Used for guardrails like denying disabling CloudTrail or using unapproved regions.

Follow-ups interviewers ask
  • Do SCPs apply to the management account? No.
Q5How do you debug an AccessDenied error?core▾

Read the error message (newer messages state which policy type denied), check identity policies, resource policies, SCPs, permission boundaries and session policies, KMS key policies, and use the IAM policy simulator and CloudTrail.

Follow-ups interviewers ask
  • Often overlooked? KMS key policies for encrypted resources.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.

✅ Check yourself

Answer the questions — feedback is instant.

1. Which policy says WHO can assume a role?
The role's resource policy for STS.
2. A permissions boundary allows s3:* and the identity policy allows ec2:*. Effective permissions?
Effective = intersection.
3. Why is iam:PassRole on Resource '*' dangerous?
Scope to specific roles and services.
4. Cross-account access to a role needs…
Both sides must agree.