Skip to content

Portal | Level: L1: Foundations | Topics: AWS IAM, Cloud Deep Dive | Domain: Cloud

AWS IAM - Primer

Why This Matters

IAM is the gateway to everything in AWS. Every API call, every resource access, every cross-account trust relationship goes through IAM. A misconfigured policy can lock your team out of production or — worse — give an attacker full admin access. IAM is the single most important security control in your AWS account, and most AWS security incidents trace back to an IAM misconfiguration.

Understanding IAM deeply is not optional. It is the difference between "we got breached because someone had *:*" and "the blast radius was contained because we scoped permissions correctly."

Core Concepts

1. The IAM Identity Model: Users, Roles, and Groups

IAM has three principal types, and they serve fundamentally different purposes.

IAM Users are long-lived identities with permanent credentials (password + optional access keys). They map to humans or service accounts. Every user has an ARN:

arn:aws:iam::123456789012:user/alice

IAM Groups are collections of users. Groups cannot be used as principals in resource policies — they exist solely to attach policies to multiple users at once.

# Create a group and attach a policy
aws iam create-group --group-name developers
aws iam attach-group-policy \
  --group-name developers \
  --policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess

# Add users to the group
aws iam add-user-to-group --group-name developers --user-name alice
aws iam add-user-to-group --group-name developers --user-name bob

IAM Roles are assumable identities with temporary credentials. Roles are the modern default — prefer them over users for almost everything. A role has a trust policy (who can assume it) and permission policies (what they can do once assumed).

# List all roles matching a pattern
aws iam list-roles --query "Roles[?contains(RoleName, 'deploy')].[RoleName,Arn]" --output table

When to use what: - Users: human console access (with MFA), legacy integrations that require long-lived keys - Groups: organizing users for policy attachment - Roles: EC2 instances, Lambda functions, EKS pods, cross-account access, CI/CD pipelines, anything automated

2. Policies: The Permission Language

IAM policies are JSON documents that define what actions are allowed or denied on which resources under what conditions.

Policy structure:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowS3ReadOnly",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-app-config",
        "arn:aws:s3:::my-app-config/*"
      ],
      "Condition": {
        "StringEquals": {
          "aws:RequestedRegion": "us-east-1"
        }
      }
    }
  ]
}

Three types of policies:

Type Where it lives Use case
Managed policies (AWS or customer) Standalone, attachable to users/roles/groups Reusable, auditable, versioned (up to 5 versions)
Inline policies Embedded directly in a user/role/group One-off, tightly coupled — harder to audit
Permission boundaries Attached to user or role as a ceiling Delegated admin — limits what a role can ever do, even if other policies grant more
# List managed policies attached to a role
aws iam list-attached-role-policies --role-name deploy-role

# List inline policies on a role
aws iam list-role-policies --role-name deploy-role

# Get the actual inline policy document
aws iam get-role-policy --role-name deploy-role --policy-name my-inline

# Check permission boundary
aws iam get-role --role-name deploy-role \
  --query 'Role.PermissionsBoundary.PermissionsBoundaryArn'

3. Policy Evaluation Logic

AWS evaluates policies in a specific order. The critical rule: explicit Deny always wins.

Evaluation order:
1. SCPs (Organization level) — if denied, STOP → Deny
2. Resource-based policies — if allowed AND same account, ALLOW
3. Permission boundaries — if not allowed, STOP → Deny
4. Session policies — if not allowed, STOP → Deny
5. Identity-based policies — if allowed, ALLOW
6. If nothing explicitly allows → implicit Deny

The key mental model:

Final decision = Allow ONLY IF:
  - No explicit Deny anywhere in the chain
  - At least one explicit Allow exists
  - The Allow is not blocked by a boundary or SCP

Cross-account access is stricter: both the identity policy in the source account AND the resource policy in the target account must allow the action. One-sided is not enough.

4. ARN Format

Remember: The ARN mnemonic is S-R-A-RService, Region, Account, Resource. Global services (IAM, S3) leave region or account empty. The triple-colon in arn:aws:s3:::bucket is not a typo — it means "no region, no account."

Every AWS resource has an Amazon Resource Name. Learn the format — you will write hundreds of these:

arn:aws:<service>:<region>:<account-id>:<resource-type>/<resource-id>

Examples:
arn:aws:s3:::my-bucket                          # S3 bucket (global, no region/account)
arn:aws:s3:::my-bucket/*                        # All objects in bucket
arn:aws:iam::123456789012:role/deploy-role      # IAM role (global, no region)
arn:aws:ec2:us-east-1:123456789012:instance/i-abc123  # EC2 instance
arn:aws:lambda:us-west-2:123456789012:function:my-func  # Lambda function
arn:aws:ecs:us-east-1:123456789012:task/cluster-name/task-id  # ECS task

Common gotcha: S3 bucket ARNs have no region or account ID (arn:aws:s3:::bucket), while S3 object ARNs use arn:aws:s3:::bucket/*. You need both in policies — one for ListBucket (bucket-level) and one for GetObject (object-level).

5. Trust Policies and Cross-Account Access

A role's trust policy defines who can assume it. This is a resource-based policy on the role itself:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::999888777666:role/ci-pipeline"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "unique-id-from-partner"
        }
      }
    }
  ]
}

Cross-account access workflow:

Account A (source)  sts:AssumeRole  Account B (target)

Requirements:
1. Account B's role trust policy must allow Account A's principal
2. Account A's identity must have permission to call sts:AssumeRole on Account B's role
3. SCPs in both accounts must not block the action
# Assume a cross-account role
aws sts assume-role \
  --role-arn arn:aws:iam::999888777666:role/target-role \
  --role-session-name my-session \
  --external-id unique-id-from-partner

# Use the temporary credentials
export AWS_ACCESS_KEY_ID=<from-response>
export AWS_SECRET_ACCESS_KEY=<from-response>
export AWS_SESSION_TOKEN=<from-response>

6. Instance Profiles for EC2

Gotcha: Instance profiles are a legacy indirection layer from IAM's early design. The console hides this — when you attach a role to an EC2 instance in the console, it silently creates an instance profile with the same name. With CLI or Terraform, you must create them explicitly. Forgetting the instance profile is one of the most common "role not working on EC2" debugging dead ends.

EC2 instances cannot directly "be" a role. They use an instance profile, which is a container for a role. When you use the console, AWS auto-creates the instance profile. With CLI/Terraform, you create it explicitly:

# Create the role
aws iam create-role \
  --role-name ec2-app-role \
  --assume-role-policy-document file://ec2-trust.json

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

# Create the instance profile and add the role
aws iam create-instance-profile --instance-profile-name ec2-app-profile
aws iam add-role-to-instance-profile \
  --instance-profile-name ec2-app-profile \
  --role-name ec2-app-role

# Attach to a running instance
aws ec2 associate-iam-instance-profile \
  --instance-id i-abc123 \
  --iam-instance-profile Name=ec2-app-profile

The instance gets temporary credentials via the metadata service:

# From inside the EC2 instance — IMDSv2
TOKEN=$(curl -sX PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -sH "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-app-role

7. IRSA for EKS Pods

IAM Roles for Service Accounts (IRSA) lets Kubernetes pods in EKS assume IAM roles without node-level credentials. This is the correct way to give pods AWS access — never use node instance roles for application workloads.

How it works:

1. EKS cluster has an OIDC provider
2. IAM role trusts the OIDC provider for a specific service account
3. Pod's service account gets annotated with the role ARN
4. AWS SDK in the pod exchanges the K8s token for AWS credentials via STS
# Get the OIDC issuer for your cluster
aws eks describe-cluster --name my-cluster \
  --query 'cluster.identity.oidc.issuer' --output text

# Create the OIDC provider in IAM (one-time setup)
eksctl utils associate-iam-oidc-provider --cluster my-cluster --approve

# Trust policy for the role:
# {
#   "Version": "2012-10-17",
#   "Statement": [{
#     "Effect": "Allow",
#     "Principal": {
#       "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/ABCDEF"
#     },
#     "Action": "sts:AssumeRoleWithWebIdentity",
#     "Condition": {
#       "StringEquals": {
#         "oidc.eks.us-east-1.amazonaws.com/id/ABCDEF:sub": "system:serviceaccount:default:my-app-sa",
#         "oidc.eks.us-east-1.amazonaws.com/id/ABCDEF:aud": "sts.amazonaws.com"
#       }
#     }
#   }]
# }
# Kubernetes service account annotation
apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-app-sa
  namespace: default
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-app-role

8. STS and AssumeRole

AWS Security Token Service (STS) is the engine behind temporary credentials. Every role assumption goes through STS.

# AssumeRole — for cross-account or role switching
aws sts assume-role \
  --role-arn arn:aws:iam::123456789012:role/admin-role \
  --role-session-name debug-session \
  --duration-seconds 3600

# AssumeRoleWithWebIdentity — for OIDC federation (IRSA, GitHub Actions)
# AssumeRoleWithSAML — for SAML federation (corporate SSO)
# GetSessionToken — for MFA-protected API access

# Check current identity (always your first debug step)
aws sts get-caller-identity

Session duration limits: - Default: 1 hour - Maximum: 12 hours (configurable on the role, MaxSessionDuration) - Role chaining (role assumes role): capped at 1 hour, cannot be extended

9. MFA Enforcement

MFA should be required for all human users. The standard pattern uses a self-managed MFA policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowViewAccountInfo",
      "Effect": "Allow",
      "Action": [
        "iam:ListMFADevices",
        "iam:GetUser"
      ],
      "Resource": "arn:aws:iam::*:user/${aws:username}"
    },
    {
      "Sid": "AllowManageOwnMFA",
      "Effect": "Allow",
      "Action": [
        "iam:CreateVirtualMFADevice",
        "iam:EnableMFADevice",
        "iam:ResyncMFADevice",
        "iam:DeactivateMFADevice",
        "iam:DeleteVirtualMFADevice"
      ],
      "Resource": [
        "arn:aws:iam::*:mfa/${aws:username}",
        "arn:aws:iam::*:user/${aws:username}"
      ]
    },
    {
      "Sid": "DenyAllExceptMFASetupUnlessMFAd",
      "Effect": "Deny",
      "NotAction": [
        "iam:CreateVirtualMFADevice",
        "iam:EnableMFADevice",
        "iam:GetUser",
        "iam:ListMFADevices",
        "iam:ResyncMFADevice",
        "sts:GetSessionToken"
      ],
      "Resource": "*",
      "Condition": {
        "BoolIfExists": {
          "aws:MultiFactorAuthPresent": "false"
        }
      }
    }
  ]
}

10. Service Control Policies (SCPs)

SCPs are guardrails at the AWS Organization level. They restrict what member accounts can do — even if the account's IAM policies allow it. SCPs do not grant permissions; they only restrict.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyRegionsOutsideUS",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": [
            "us-east-1",
            "us-west-2"
          ]
        },
        "ArnNotLike": {
          "aws:PrincipalARN": "arn:aws:iam::*:role/OrganizationAdmin"
        }
      }
    }
  ]
}

SCPs apply to all principals in the account except the management (root) account. Common SCP patterns: - Region restriction (block all API calls outside approved regions) - Prevent disabling CloudTrail or GuardDuty - Prevent leaving the organization - Restrict root user actions - Enforce encryption on S3 and EBS

11. Condition Keys

Conditions add context-based restrictions to policies. Some of the most useful:

{
  "Condition": {
    "StringEquals": {
      "aws:RequestedRegion": "us-east-1",
      "ec2:ResourceTag/Environment": "production"
    },
    "IpAddress": {
      "aws:SourceIp": "203.0.113.0/24"
    },
    "Bool": {
      "aws:SecureTransport": "true"
    },
    "StringLike": {
      "s3:prefix": ["home/${aws:username}/*"]
    },
    "ArnLike": {
      "aws:PrincipalArn": "arn:aws:iam::*:role/Admin*"
    },
    "DateGreaterThan": {
      "aws:CurrentTime": "2024-01-01T00:00:00Z"
    }
  }
}

Key variable substitutions: - ${aws:username} — IAM user name - ${aws:PrincipalTag/team} — tag on the calling principal - ${aws:SourceIp} — requester's IP - ${aws:CurrentTime} — current UTC time - ${ec2:ResourceTag/Name} — tag on the target resource

12. Policy Simulator and IAM Access Analyzer

Policy Simulator tests policies without making real API calls:

# Simulate: can this role read this S3 object?
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/app-role \
  --action-names s3:GetObject \
  --resource-arns arn:aws:s3:::my-bucket/config.yaml \
  --query 'EvaluationResults[].{Action:EvalActionName,Decision:EvalDecision}'

IAM Access Analyzer finds resources shared with external principals:

# Create an analyzer
aws accessanalyzer create-analyzer \
  --analyzer-name my-analyzer \
  --type ACCOUNT

# List findings (resources accessible from outside the account)
aws accessanalyzer list-findings \
  --analyzer-arn arn:aws:access-analyzer:us-east-1:123456789012:analyzer/my-analyzer \
  --query 'findings[].{Resource:resource,Principal:principal,Action:action}'

Access Analyzer also validates policies against best practices:

# Validate a policy document
aws accessanalyzer validate-policy \
  --policy-document file://policy.json \
  --policy-type IDENTITY_POLICY

13. Resource-Based vs Identity-Based Policies

Identity-based policies attach to users, groups, or roles — "what can this principal do?"

Resource-based policies attach to resources (S3 buckets, KMS keys, SQS queues, Lambda functions) — "who can access this resource?"

Critical difference for cross-account: resource-based policies can grant access to external principals without the external account needing an identity policy. This is how you share an S3 bucket with another account — put a bucket policy that allows the external account's role.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "AllowCrossAccountRead",
    "Effect": "Allow",
    "Principal": {
      "AWS": "arn:aws:iam::999888777666:role/data-reader"
    },
    "Action": ["s3:GetObject"],
    "Resource": "arn:aws:s3:::shared-data/*"
  }]
}

Services that support resource-based policies: S3, KMS, SQS, SNS, Lambda, ECR, Secrets Manager, API Gateway, and many more.

14. Session Policies

Session policies are passed during AssumeRole or federation. They act as an additional filter — the effective permissions are the intersection of the role's policies and the session policy. You cannot escalate permissions with a session policy.

# Assume role with restricted session
aws sts assume-role \
  --role-arn arn:aws:iam::123456789012:role/broad-role \
  --role-session-name limited-session \
  --policy '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::one-bucket/*"}]}'

Use case: a CI/CD system assumes a broad deployment role but scopes each job to only the resources that job needs.

15. Least Privilege in Practice

Least privilege is easy to say, hard to do. Practical approaches:

  1. Start with AWS managed policies for broad categories, then tighten
  2. Use CloudTrail + IAM Access Analyzer to see what actions are actually used
  3. Generate policies from access activity (Access Analyzer can do this)
  4. Use permission boundaries to cap what developers can create
  5. Tag-based access control (ABAC) — grant access based on tags instead of explicit ARNs
# Generate a policy from CloudTrail activity
aws accessanalyzer generate-policy \
  --policy-generation-details '{"principalArn":"arn:aws:iam::123456789012:role/app-role"}' \
  --cloud-trail-details '{
    "trails": [{"cloudTrailArn":"arn:aws:cloudtrail:us-east-1:123456789012:trail/main","allRegions":true}],
    "accessRole": "arn:aws:iam::123456789012:role/AccessAnalyzerRole",
    "startTime": "2024-01-01T00:00:00Z",
    "endTime": "2024-02-01T00:00:00Z"
  }'

Key Takeaways

  • Roles with temporary credentials are the default; long-lived access keys are the exception
  • Explicit Deny always wins in policy evaluation — you cannot override it
  • Cross-account access requires both sides to allow the action
  • Permission boundaries set ceilings that identity policies cannot exceed
  • SCPs are organization-level guardrails — they restrict, never grant
  • Always start IAM debugging with aws sts get-caller-identity
  • The policy simulator is your best friend for pre-flight permission checks
  • IRSA is the correct way to give EKS pods AWS access — not node roles

Wiki Navigation

Prerequisites

Next Steps

  • AWS CloudWatch (Topic Pack, L2) — Cloud Deep Dive
  • AWS Devops Flashcards (CLI) (flashcard_deck, L1) — Cloud Deep Dive
  • AWS EC2 (Topic Pack, L1) — Cloud Deep Dive
  • AWS ECS (Topic Pack, L2) — Cloud Deep Dive
  • AWS General Flashcards (CLI) (flashcard_deck, L1) — Cloud Deep Dive
  • AWS Lambda (Topic Pack, L2) — Cloud Deep Dive
  • AWS Networking (Topic Pack, L1) — Cloud Deep Dive
  • AWS Route 53 (Topic Pack, L2) — Cloud Deep Dive
  • AWS S3 Deep Dive (Topic Pack, L1) — Cloud Deep Dive
  • AWS Security Flashcards (CLI) (flashcard_deck, L1) — AWS IAM