Skip to content
C Codeloom
AWS

AWS IAM Policies vs Roles Cheatsheet

A quick-reference guide to the difference between IAM users, roles, policies, and trust relationships, with examples you can paste into your AWS account today.

·4 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • The difference between identity and permissions
  • When to use a role vs a user
  • Trust policies vs permission policies
  • How AssumeRole and instance profiles work
  • Patterns for cross-account access

Prerequisites

  • Basic familiarity with AWS console or CLI

What and Why

IAM has four core concepts: users, roles, policies, and trust relationships. They are simple individually but easy to confuse. The most common mistake — long-lived access keys handed to applications — is also the most expensive when it leaks. Roles fix that by issuing short-lived credentials on demand.

This is the cheatsheet I wish I had on day one.

Mental Model

  • A policy is a JSON document that grants or denies actions on resources. Policies are attached to identities.
  • A user is a long-lived identity with credentials (password or access keys).
  • A role is an identity that has no permanent credentials. Something assumes the role and gets temporary credentials for it.
  • A trust policy says who is allowed to assume a role.
  • A permission policy says what the assuming principal can do once they have assumed it.
  [EC2 instance / Lambda / User]
            |
 sts:AssumeRole (allowed by trust policy)
            |
            v
      [IAM Role]
            |
 uses permission policies attached to role
            |
            v
      [API calls]
            |
            v
     [AWS resources]
Assume role flow

Hands-on Example

A permission policy that allows read on one S3 bucket:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["s3:GetObject", "s3:ListBucket"],
    "Resource": [
      "arn:aws:s3:::reports-prod",
      "arn:aws:s3:::reports-prod/*"
    ]
  }]
}

A trust policy that lets an EC2 instance assume the role:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "ec2.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}

Create the role and attach the policy:

aws iam create-role --role-name reports-reader \
  --assume-role-policy-document file://trust.json

aws iam put-role-policy --role-name reports-reader \
  --policy-name s3-read --policy-document file://perm.json

Attach via an instance profile so the EC2 instance picks it up:

aws iam create-instance-profile --instance-profile-name reports-reader
aws iam add-role-to-instance-profile \
  --instance-profile-name reports-reader \
  --role-name reports-reader

The instance now gets temporary credentials from the metadata service. No access keys to rotate, no secrets to leak.

For a cross-account role, the trust policy lists another account ID:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::111122223333:root" },
    "Action": "sts:AssumeRole",
    "Condition": {
      "StringEquals": { "sts:ExternalId": "shared-secret-value" }
    }
  }]
}

The external ID prevents the confused-deputy problem when third parties assume roles into your account.

Common Pitfalls

Long-lived access keys in code. Never. Use roles via instance profile, IRSA for EKS, or OIDC federation from GitHub Actions.

Wildcard everything. "Action": "*", "Resource": "*" is admin. Audit for wildcards in production policies regularly.

Confusing identity-based and resource-based policies. S3 buckets, KMS keys, SNS topics, and a few others have their own resource policies. Both must allow the action; either denying wins.

Forgetting explicit denies. A Deny always overrides an Allow. Useful for guardrails (no one, not even admins, may delete production CloudTrail), but devastating when a typo hits a wide pattern.

Inline vs managed policies. Inline policies are tied to one identity and hard to audit. Use customer-managed policies you can attach to many roles.

Practical Tips

For workloads on EKS, use IRSA (IAM Roles for Service Accounts). Pods assume roles via OIDC federation, with per-pod scoping. No more node-wide instance profile that every pod inherits.

For CI/CD, configure OIDC federation from GitHub Actions, GitLab, or your CI of choice:

permissions:
  id-token: write
  contents: read
steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/github-deploy
      aws-region: us-east-1

No long-lived secrets in the runner.

Use IAM Access Analyzer to find unused permissions and overbroad trust policies. It generates least-privilege policy suggestions from real CloudTrail usage.

Tag roles by team and purpose. When a permission seems suspicious, tags shorten the “whose is this?” investigation from hours to seconds.

Memorize the policy evaluation order: explicit deny > organization SCP > resource policy > identity policy > permissions boundary > session policy. When access is unexpectedly blocked, walk it from the top.

Wrap-up

Users are identities, roles are assumable identities, policies are what each one can do, and trust policies decide who can assume what. Prefer roles over users for everything programmatic, federate CI through OIDC, and avoid wildcards in production. Master sts:AssumeRole and the policy evaluation order, and IAM stops feeling like a maze and starts feeling like the security backbone it is designed to be.