What is IAM?#

IAM controls who (authentication) can do what (authorization) on which AWS resources. It’s the security foundation of every AWS account — misconfigure IAM and nothing else matters.

flowchart LR
    Who["WHO?<br>(Principal)"] --> Auth["Authenticated?"]
    Auth --> What["WHAT?<br>(Action)"]
    What --> Which["ON WHICH?<br>(Resource)"]
    Which --> Conditions["UNDER WHAT CONDITIONS?<br>(Conditions)"]
    Conditions --> Decision{"ALLOW or DENY?"}

IAM Entities#

Users#

An IAM user represents a person or application that interacts with AWS:

{
  "UserName": "alice",
  "Arn": "arn:aws:iam::123456789012:user/alice",
  "CreateDate": "2024-01-15T10:00:00Z"
}

Best practice: Minimize IAM users. Use IAM Identity Center (SSO) for humans and IAM roles for applications.

Groups#

Groups are collections of users — attach policies to groups, not individual users:

flowchart TD
    DevGroup["Group: Developers"] --> Alice["User: alice"]
    DevGroup --> Bob["User: bob"]
    
    AdminGroup["Group: Admins"] --> Carol["User: carol"]
    
    DevPolicy["Policy: EC2ReadOnly"] --> DevGroup
    AdminPolicy["Policy: AdministratorAccess"] --> AdminGroup

Roles#

Roles are identities that can be assumed temporarily. No permanent credentials — they use temporary security tokens.

flowchart LR
    EC2["EC2 Instance"] -->|"Assumes"| Role["IAM Role:<br>app-server-role"]
    Lambda["Lambda Function"] -->|"Assumes"| LRole["IAM Role:<br>lambda-exec-role"]
    CI["CI/CD Pipeline"] -->|"Assumes via OIDC"| CIRole["IAM Role:<br>deploy-role"]
    
    Role --> S3["Access S3"]
    LRole --> DDB["Access DynamoDB"]
    CIRole --> TF["Run Terraform"]

Use roles for:

  • EC2 instances (instance profile)
  • Lambda functions (execution role)
  • ECS tasks (task role)
  • Cross-account access
  • CI/CD pipelines (OIDC federation)

Policies#

Policies are JSON documents that define permissions:

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

Policy Elements#

ElementPurposeExample
EffectAllow or Deny"Allow"
ActionAPI operations"s3:GetObject"
ResourceWhich resources"arn:aws:s3:::bucket/*"
ConditionWhen to applyIP range, time, MFA
PrincipalWho (resource policies only)"arn:aws:iam::123:role/x"

Policy Types#

TypeAttached ToUse Case
AWS ManagedUsers, Groups, RolesCommon permissions (ReadOnlyAccess)
Customer ManagedUsers, Groups, RolesCustom permissions for your org
InlineSingle entityOne-off, tightly coupled permissions
Resource-basedResources (S3, SQS, etc.)Cross-account access
Permission BoundaryUsers, RolesMaximum permission ceiling
SCPAWS Organizations OUsAccount-level guardrails

Policy Evaluation Logic#

flowchart TD
    Request["API Request"] --> Explicit{"Explicit DENY<br>in any policy?"}
    Explicit -->|"Yes"| Denied["❌ DENIED"]
    Explicit -->|"No"| SCP{"SCP allows?"}
    SCP -->|"No"| Denied
    SCP -->|"Yes"| Boundary{"Permission Boundary<br>allows?"}
    Boundary -->|"No"| Denied
    Boundary -->|"Yes"| Allow{"Explicit ALLOW<br>in any policy?"}
    Allow -->|"Yes"| Allowed["✅ ALLOWED"]
    Allow -->|"No"| Denied2["❌ DENIED<br>(implicit deny)"]

Key rule: Deny always wins. If no policy explicitly allows, the request is denied (implicit deny).


Least Privilege#

Grant only the permissions needed to perform a task — nothing more.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::prod-data-bucket/reports/*",
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": "10.0.0.0/16"
        }
      }
    }
  ]
}

This policy allows reading only from a specific path, only from the corporate network.

Common Mistakes#

// ❌ Too broad — allows everything on everything
{
  "Effect": "Allow",
  "Action": "*",
  "Resource": "*"
}

// ❌ Too broad — allows all S3 actions on all buckets
{
  "Effect": "Allow",
  "Action": "s3:*",
  "Resource": "*"
}

// ✅ Specific actions on specific resources
{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:PutObject"],
  "Resource": "arn:aws:s3:::my-app-uploads/*"
}

Conditions#

Add context-based restrictions:

{
  "Effect": "Allow",
  "Action": "ec2:RunInstances",
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "ec2:InstanceType": ["t3.micro", "t3.small"],
      "aws:RequestedRegion": "eu-central-1"
    },
    "Bool": {
      "aws:MultiFactorAuthPresent": "true"
    }
  }
}
Condition KeyUse Case
aws:SourceIpRestrict by IP range
aws:RequestedRegionRestrict to specific regions
aws:MultiFactorAuthPresentRequire MFA
aws:PrincipalOrgIDRestrict to your organization
ec2:InstanceTypeLimit instance sizes
s3:prefixRestrict to S3 key prefixes

Cross-Account Access#

sequenceDiagram
    participant App as Account A (App)
    participant STS as AWS STS
    participant Data as Account B (Data)
    
    App->>STS: AssumeRole (role in Account B)
    STS-->>App: Temporary credentials (15min-12hr)
    App->>Data: Access resources with temp credentials

Trust Policy (on the role in Account B)#

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111111111111:role/app-role"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "unique-external-id"
        }
      }
    }
  ]
}

Best Practices#

PracticeWhy
Use roles, not long-lived keysTemporary credentials auto-expire
Enable MFA for console usersPrevents credential theft
Use IAM Identity Center for humansCentralized, federated access
Apply least privilegeMinimize blast radius
Use permission boundariesCap maximum permissions
Audit with CloudTrailTrack who did what
Rotate credentials regularlyLimit exposure window
Use conditionsAdd context-based restrictions
Never use root accountCreate IAM users/roles instead

Key Takeaways#

  1. IAM is global — users, roles, and policies apply across all regions
  2. Deny always wins — explicit deny overrides any allow
  3. Use roles over users — temporary credentials are safer than permanent keys
  4. Least privilege — start with zero permissions, add only what’s needed
  5. Conditions add context — restrict by IP, region, MFA, time
  6. Resource policies enable cross-account — S3, SQS, KMS support them
  7. Audit everything — CloudTrail logs every API call