AWS IAM Fundamentals

IAM (Identity and Access Management) is the gate every AWS request passes through. It’s the most important security service in AWS, and the most common source of misconfiguration. This note covers the mental model; vendor-specific CLI is elsewhere.

What IAM does

For every single AWS API call, IAM answers: is this principal allowed to do this action on this resource under these conditions?

  • A principal (user, role, service) makes a request
  • The request names an action (s3:GetObject, ec2:RunInstances, …)
  • The request targets a resource (a specific bucket, instance, …)
  • The request has context (source IP, MFA status, time, …)

IAM evaluates policies and returns allow or deny. No gray area.

The four principal types

PrincipalWhat it representsCredentials
Root userThe account ownerEmail + password (+ MFA, hopefully)
IAM UserA human or long-lived scriptPassword (console), access keys (programmatic)
IAM RoleAn identity that’s assumed by something elseTemporary credentials (STS tokens)
Federated userExternal identity (SSO / SAML / OIDC / social)Temporary credentials from a trust relationship

The direction modern AWS is pushing: away from IAM users with long-lived access keys, toward roles + federated identity + temporary credentials. Keys on disk are the #1 source of breaches.

Users, Groups, Roles — what’s what

IAM User

A long-lived identity with durable credentials. One per human or service that needs its own audit trail.

  • Best for: break-glass admins, IaC tooling where OIDC isn’t possible, a small number of human users if you’re not using SSO

IAM Group

A collection of users to which you attach policies. Not a principal itself — you don’t assume a group, you add users to it. Useful for “all developers get these permissions.”

IAM Role

An identity without permanent credentials. Something (a user, a service, another account) assumes the role via STS and receives temporary credentials (1-12 hour lifetime).

  • Best for: EC2 instances, Lambda functions, ECS tasks (via instance profiles / execution roles), cross-account access, CI/CD pipelines (via OIDC federation)
  • The idiomatic AWS pattern — if you can use a role, use a role.

Policies — the rules

A policy is a JSON document describing what’s allowed or denied. Four kinds:

Policy typeAttached toCommon use
Identity-based policyUser, Group, or Role”What can this principal do?”
Resource-based policyA resource (S3 bucket, SNS topic, KMS key, …)”Who can touch this resource?”
Permission boundaryUser or RoleMax permissions ceiling, regardless of other policies
Service Control Policy (SCP)Organization unit / accountOrg-wide guardrails

Anatomy of a policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadOurLogBucket",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::my-logs",
        "arn:aws:s3:::my-logs/*"
      ],
      "Condition": {
        "IpAddress": { "aws:SourceIp": "203.0.113.0/24" }
      }
    }
  ]
}

Every statement has the four parts:

  • Effect — Allow or Deny
  • Action — one or more IAM actions (wildcards allowed)
  • Resource — one or more ARNs (wildcards allowed)
  • Condition (optional) — context-based guards (source IP, MFA, tag match, time)

Permission evaluation — how a decision is reached

Simplified algorithm:

  1. Start with implicit deny. Default for everything.
  2. Collect all applicable policies — identity-based, resource-based, permission boundaries, SCPs, session policies.
  3. If any explicit Deny matches → final answer is Deny. No policy can override a Deny.
  4. If any Allow matches → final answer is Allow.
  5. Otherwise → implicit Deny.

The two rules that trip everyone up:

  • Explicit Deny wins over everything. Including "Action": "*" / "Resource": "*" allows.
  • Default is deny. You must grant permissions explicitly; nothing is allowed by accident (except misconfigured resource policies).

AssumeRole — the cornerstone pattern

A principal assumes a role to get temporary credentials for a different identity. This is how:

  • EC2 instances access S3 (the instance profile)
  • Lambda functions call DynamoDB
  • A user in account A accesses account B
  • A CI/CD pipeline deploys to AWS
  • A human uses aws sso login and picks a role

The role has two policies that gatekeep assumption:

Trust policy (attached to the role)

“Who is allowed to assume me?”

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::111122223333:user/alice" },
    "Action": "sts:AssumeRole"
  }]
}

Permission policy (attached to the role)

“What can the role do once assumed?”

Both must allow — the trust policy says Alice may assume, the permission policy says what Alice can do once she has the role.

This two-sided structure is the most confusing part of IAM for newcomers. Memorise it:

  • Trust = who may become me
  • Permissions = what I can do

Instance profiles — roles for EC2

EC2 instances don’t directly use IAM roles — they use an instance profile, which is a wrapper around a role. You attach an instance profile to the instance; applications on the instance ask the Instance Metadata Service (IMDS) at http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME for temporary credentials.

  • SDKs (boto3, AWS CLI) do this automatically — no configuration needed
  • Always use IMDSv2 (token-based, SSRF-resistant). IMDSv1 was the root cause of the 2019 Capital One breach.
  • Lambda, ECS, EKS, etc. have analogous mechanisms (execution roles, task roles, service accounts with IRSA)

Federation — bringing external identities

Instead of creating IAM users, bring identities from an external IdP via SAML 2.0 or OIDC:

  • AWS IAM Identity Center (formerly SSO) — modern choice; wire up your IdP once, users log in via an SSO portal, pick an account+role, get temporary credentials
  • SAML federation with Active Directory — common in enterprises
  • OIDC federation with GitHub Actions / GitLab CI — the modern way to avoid storing long-lived keys in CI

A federated user is just like a role assumer — they receive temporary credentials from STS based on a trust policy.

Best practices — the short list

  1. Don’t use the root user. Enable MFA on it. Delete root access keys if any exist. Lock it away.
  2. Enable MFA on every IAM user. No exceptions.
  3. Prefer roles over users. Especially for code — never put access keys in source or instance disk.
  4. Least privilege. Grant only what’s needed; narrow with Resource and Condition.
  5. Use groups for humans. Policies on groups, not directly on users.
  6. Use Permission Boundaries when delegating IAM to teams — caps what they can grant themselves.
  7. Use SCPs at the organization level for broad guardrails (e.g. “no one can disable CloudTrail”, “no region outside EU”).
  8. Rotate access keys. If you must use them, rotate regularly.
  9. Monitor with CloudTrail. Every API call is logged; surface suspicious activity.
  10. Test with IAM Policy Simulator before deploying complex policies.

Common pitfalls

  • Two Allow policies are not more permissive than one. Union of Allows is the grant.
  • An explicit Deny in an SCP can block an account-level admin. Surprising the first time you hit it.
  • Resource-based policy can allow cross-account without the other side setting up anything — but the calling identity also needs an identity-based Allow. Both sides must permit.
  • Wildcards in Resource are often too broad — "Resource": "*" grants across the whole account.
  • Forgetting to grant the iam:PassRole permission — you can create a role but can’t attach it to EC2/Lambda without PassRole on the role’s ARN.

See also