DEV Community

Cover image for AWS IAM Roles Explained - A Beginner's Guide (With Real Examples)
Parag Agrawal
Parag Agrawal

Posted on • Originally published at turbodeploy.dev

AWS IAM Roles Explained - A Beginner's Guide (With Real Examples)

When we first started building on AWS, we gave everything AdministratorAccess.

ECS task? AdministratorAccess. Lambda function? AdministratorAccess. CI/CD pipeline? Same thing.

It worked. Everything could talk to everything. We shipped fast. Then we read about a startup whose leaked CI/CD credentials let an attacker spin up $50,000 worth of crypto-mining EC2 instances over a single weekend. The leaked role had AdministratorAccess.

That's when we actually sat down and learned IAM.

This guide is what we wish someone had handed us on day one. No certification jargon, no 47-page docs. Just the mental models, real examples, and copy-paste policies you need to lock down your AWS setup as a web developer.


Why IAM Matters (Even If You're "Just a Developer")

IAM (Identity and Access Management) controls who can do what on which AWS resources. It's not a DevOps-only thing. It's the backbone of your entire AWS security setup.

Every AWS API call goes through IAM. When your ECS container reads from S3, when your Lambda writes to DynamoDB, when your GitHub Actions pipeline pushes new code, IAM is the gatekeeper deciding if the call goes through or gets blocked.

Get it wrong, and you're either:

  • Too permissive → one leaked credential = full account compromise
  • Too restrictive → your app crashes with AccessDenied errors in production at 2 AM

The goal is least privilege: give exactly the permissions needed, nothing more.


The 4 Core Concepts

AWS IAM: The 4 Core Concepts - Users, Groups, Roles and Policies

1. IAM Users - An Identity for a Person

An IAM user is a person (or application) that interacts with AWS. Each user has permanent, long-term credentials.

IAM User "alice"
├── Console password (for AWS web console)
├── Access Key ID + Secret Key (for CLI/SDK)
└── Attached policies (what Alice can do)
Enter fullscreen mode Exit fullscreen mode

When to use: Rarely. IAM users are considered legacy for human access. Use IAM Identity Center (SSO) instead for your team.

When they're still needed: Break-glass emergency access, or legacy applications that can't use roles.

2. IAM Groups

A group is a collection of users with shared permissions. Instead of attaching policies to each user one by one, attach them to a group.

Group "Developers"
├── Policy: AmazonECS_FullAccess
├── Policy: AmazonS3ReadOnlyAccess
├── User: alice
├── User: bob
└── User: charlie
Enter fullscreen mode Exit fullscreen mode

All three users inherit the group's permissions. When a new developer joins, you add them to the group, no need to configure individual policies.

3. IAM Roles (This Is the Important One) ⭐

A role is like a user, but with three critical differences:

Feature IAM User IAM Role
Credentials Long-term (access key + secret) Short-term (temporary, auto-rotated)
Who uses it A specific person or app Anyone/anything trusted to "assume" it
Credential rotation Manual (you rotate keys yourself) Automatic (AWS rotates every 1–12 hours)
Security risk if leaked High (valid until manually revoked) Low (expires automatically)

Roles are the recommended way to grant permissions to:

  • AWS services (ECS tasks, Lambda functions, EC2 instances)
  • CI/CD pipelines (GitHub Actions, GitLab CI)
  • Cross-account access (accessing resources in another AWS account)
  • Federated users (SSO via Identity Center)

4. IAM Policies

A policy is a JSON document that defines permissions. It answers three questions:

  1. Effect : Allow or Deny?
  2. Action : What operation? (e.g., s3:GetObject, ecs:RunTask)
  3. Resource : On which AWS resource? (specified by ARN)

Policies are attached to users, groups or roles. They do nothing on their own, they must be attached to an identity.


Reading an IAM Policy (It's JSON, That's It)

Anatomy of an IAM Policy

Let's read a real policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowReadFromSpecificBucket",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-app-uploads",
        "arn:aws:s3:::my-app-uploads/*"
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

In plain English: "Allow getting objects and listing the contents of the my-app-uploads S3 bucket."

Breaking It Down

Field Value Meaning
Version "2012-10-17" Always use this version (it's the current IAM policy language version)
Sid "AllowReadFromSpecificBucket" Optional human-readable name
Effect "Allow" This statement grants permission (vs. "Deny")
Action ["s3:GetObject", "s3:ListBucket"] The specific S3 operations allowed
Resource ["arn:aws:s3:::my-app-uploads", "arn:aws:s3:::my-app-uploads/*"] The specific bucket and its objects

The ARN Format

Every AWS resource has an Amazon Resource Name:

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

Examples:
arn:aws:s3:::my-bucket                          (S3 bucket)
arn:aws:s3:::my-bucket/*                        (All objects in bucket)
arn:aws:dynamodb:us-east-1:123456789:table/Users (DynamoDB table)
arn:aws:ecs:us-east-1:123456789:cluster/my-app   (ECS cluster)
Enter fullscreen mode Exit fullscreen mode

Common Mistake: Wildcards

//  DANGEROUS: Allows ALL actions on ALL resources
{
  "Effect": "Allow",
  "Action": "*",
  "Resource": "*"
}

//  SAFE: Allows only specific actions on specific resources
{
  "Effect": "Allow",
  "Action": ["s3:GetObject"],
  "Resource": ["arn:aws:s3:::my-app-uploads/*"]
}
Enter fullscreen mode Exit fullscreen mode

Rule of thumb: If you see "Action": "*" or "Resource": "*" in a production policy, something is wrong.


How Roles Actually Work (The Mental Model)

How IAM Role Assumption Works

Every IAM role has two policies:

1. Trust Policy - WHO Can Assume This Role

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Translation: "ECS tasks are allowed to assume this role." No other service, user, or account can use it.

2. Permission Policy - WHAT the Role Can Do

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::my-app-uploads/*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Translation: "Whoever assumes this role can read and write objects in the my-app-uploads bucket."

The Assumption Flow

  1. Your ECS task starts and calls AWS Security Token Service (STS): sts:AssumeRole
  2. STS checks the trust policy - is ecs-tasks.amazonaws.com allowed? ✅
  3. STS returns temporary credentials (access key + secret + session token) valid for 1–12 hours
  4. Your container uses these credentials to access S3
  5. When the credentials expire, ECS automatically requests new ones

You never manage, rotate, or store these credentials. AWS handles everything.


5 Real-World Examples (Copy-Paste Ready)

Example 1: ECS Task Role (Your App Accesses S3 + DynamoDB)

Probably the most common role you'll create. Your containerized app needs to read/write to S3 and query DynamoDB.

Trust Policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Permission Policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "S3Access",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::my-app-uploads/*"
    },
    {
      "Sid": "S3ListBucket",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::my-app-uploads"
    },
    {
      "Sid": "DynamoDBAccess",
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem",
        "dynamodb:Query"
      ],
      "Resource": "arn:aws:dynamodb:us-east-1:*:table/my-app-*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Create it with CLI:

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

# Create and attach the permission policy
aws iam put-role-policy \
  --role-name MyAppTaskRole \
  --policy-name MyAppPermissions \
  --policy-document file://permission-policy.json
Enter fullscreen mode Exit fullscreen mode

Use it in your ECS task definition:

{
  "family": "my-web-app",
  "taskRoleArn": "arn:aws:iam::123456789:role/MyAppTaskRole",
  "executionRoleArn": "arn:aws:iam::123456789:role/ecsTaskExecutionRole",
  "containerDefinitions": [...]
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Task Role vs. Execution Role: Don't confuse these. The Task Role is used by your app code (S3, DynamoDB). The Execution Role is used by the ECS agent (pulling images from ECR, writing to CloudWatch). They are separate roles with separate permissions.


Example 2: ECS Task Execution Role (Pulling Images + Logging)

This is the role the ECS agent uses, not your application code.

Trust Policy: Same as above (ECS tasks service principal).

Permission Policy: Use the AWS managed policy:

aws iam attach-role-policy \
  --role-name ecsTaskExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
Enter fullscreen mode Exit fullscreen mode

This managed policy includes:

  • ecr:GetAuthorizationToken : authenticate with ECR
  • ecr:BatchGetImage : pull container images
  • logs:CreateLogStream : create CloudWatch log streams
  • logs:PutLogEvents : write logs

If you also need to read secrets from Secrets Manager (for database passwords, API keys):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue"
      ],
      "Resource": [
        "arn:aws:secretsmanager:us-east-1:*:secret:my-app/*"
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Example 3: GitHub Actions CI/CD Role (OIDC, No Access Keys)

This is the right way to deploy from GitHub Actions to AWS in 2026. Instead of storing long-lived AWS access keys in GitHub secrets, you use OIDC federation.

Step 1: Create the OIDC Provider (one-time setup)

aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the Role with a Trust Policy scoped to your repo

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
        }
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Pay attention to the Condition block. It locks this role down to the main branch of your specific repo. A fork, a PR branch, or a different repo can't assume it.

Step 3: Permission Policy (deploy-only)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ECRPushAccess",
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:PutImage",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload"
      ],
      "Resource": "*"
    },
    {
      "Sid": "ECSDeployAccess",
      "Effect": "Allow",
      "Action": [
        "ecs:UpdateService",
        "ecs:DescribeServices",
        "ecs:DescribeTaskDefinition",
        "ecs:RegisterTaskDefinition"
      ],
      "Resource": "*"
    },
    {
      "Sid": "PassRoleToECS",
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": [
        "arn:aws:iam::123456789:role/MyAppTaskRole",
        "arn:aws:iam::123456789:role/ecsTaskExecutionRole"
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Use it in GitHub Actions:

- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789:role/GitHubActionsDeployRole
    aws-region: us-east-1
Enter fullscreen mode Exit fullscreen mode

No access keys. No secrets. Temporary credentials that expire automatically. ✅


Example 4: Lambda Function Role

Your Lambda function needs to process images from S3 and send notifications via SNS.

Trust Policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Permission Policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::image-uploads/*"
    },
    {
      "Effect": "Allow",
      "Action": ["sns:Publish"],
      "Resource": "arn:aws:sns:us-east-1:*:image-processed"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:us-east-1:*:*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Example 5: Cross-Account Access Role

Your staging account needs to pull Docker images from your production ECR repository.

Trust Policy (on the production account role):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::STAGING_ACCOUNT_ID:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "staging-access-2026"
        }
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The ExternalId prevents the "confused deputy" problem, ensuring only your staging account (not a random third party) can assume this role.


Task Role vs. Execution Role (ECS Cheat Sheet)

This trips up almost every web developer who starts using ECS:

Task Role Execution Role
Who uses it Your application code The ECS agent
When it's used While your container is running During task launch (pulling image, starting container)
Typical permissions S3, DynamoDB, SQS, SNS, SES ECR (pull images), CloudWatch Logs, Secrets Manager
If missing AccessDenied when your app calls AWS services CannotPullContainerError at task startup
Task definition field taskRoleArn executionRoleArn

Think of it this way: the Execution Role is the building manager who lets the tenant move in (pulls image, sets up logs). The Task Role is the tenant's key card that opens specific doors (S3, DynamoDB) inside the building.


The 10 IAM Commandments (2026 Best Practices)

1. ❌ Never Use "Action": "*" or "Resource": "*" in Production

//  The "I'll fix it later" policy (you won't)
{ "Effect": "Allow", "Action": "*", "Resource": "*" }

//  Specific actions, specific resources
{ "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::my-bucket/*"] }
Enter fullscreen mode Exit fullscreen mode

2. ✅ Use Roles Instead of Users for Everything

Use Case ❌ Old Way ✅ Modern Way
ECS container accesses S3 Access keys in env vars Task Role
Lambda accesses DynamoDB Access keys in code Lambda execution role
GitHub Actions deploys to ECS Access keys in secrets OIDC federation
Developer accesses AWS console IAM user + password Identity Center (SSO)

3. ✅ Enable MFA on the Root Account (Day 1)

# Check if root has MFA
aws iam get-account-summary --query "SummaryMap.AccountMFAEnabled"
# Should return: 1
Enter fullscreen mode Exit fullscreen mode

Your root account can delete your entire AWS infrastructure. Protect it with MFA and never use it for daily work.

4. ✅ Use AWS Managed Policies When They Fit

AWS maintains pre-built policies for common scenarios:

Policy Name What It Grants
AmazonS3ReadOnlyAccess Read-only access to all S3 buckets
AmazonDynamoDBFullAccess Full access to DynamoDB
AmazonECSTaskExecutionRolePolicy ECR pull + CloudWatch Logs
CloudWatchLogsFullAccess Full CloudWatch Logs access
# List all AWS managed policies
aws iam list-policies --scope AWS --query "Policies[].PolicyName" | head -50
Enter fullscreen mode Exit fullscreen mode

But for production, prefer custom policies scoped to specific resources.

5. ✅ Use IAM Access Analyzer

Access Analyzer finds unused permissions, external access, and overly broad policies:

aws accessanalyzer create-analyzer \
  --analyzer-name my-account-analyzer \
  --type ACCOUNT

# Check findings
aws accessanalyzer list-findings \
  --analyzer-arn arn:aws:access-analyzer:us-east-1:123456789:analyzer/my-account-analyzer
Enter fullscreen mode Exit fullscreen mode

6. ✅ Tag Your Roles

aws iam tag-role \
  --role-name MyAppTaskRole \
  --tags Key=Environment,Value=production Key=Service,Value=my-web-app Key=ManagedBy,Value=terraform
Enter fullscreen mode Exit fullscreen mode

Tags make auditing and cost tracking way easier down the line.

7. ✅ Use Conditions for Extra Security

{
  "Effect": "Allow",
  "Action": "s3:*",
  "Resource": "arn:aws:s3:::sensitive-data/*",
  "Condition": {
    "IpAddress": {
      "aws:SourceIp": "203.0.113.0/24"
    },
    "Bool": {
      "aws:MultiFactorAuthPresent": "true"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This allows S3 access only from a specific IP range AND only when MFA is active.

8. ✅ Audit with CloudTrail

Every IAM action is logged in CloudTrail. Use it to investigate:

# Who assumed a role in the last 24 hours?
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
  --start-time $(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ) \
  --max-items 10
Enter fullscreen mode Exit fullscreen mode

9. ✅ Rotate Credentials Regularly

If you must use IAM users with access keys (avoid if possible):

# List access keys and their age
aws iam list-access-keys --user-name alice

# Create new key, update apps, then delete old key
aws iam create-access-key --user-name alice
aws iam delete-access-key --user-name alice --access-key-id AKIA_OLD_KEY
Enter fullscreen mode Exit fullscreen mode

10. ✅ Use Separate Roles for Each Service

Don't share one role between your web app, worker, and cron job. Each service should have its own role with only the permissions it needs.

my-app-web-task-role      → S3 read, DynamoDB read/write
my-app-worker-task-role   → SQS consume, S3 read/write  
my-app-cron-task-role     → DynamoDB read, SES send
Enter fullscreen mode Exit fullscreen mode

If the worker is compromised, the attacker can't access DynamoDB or send emails. Blast radius minimized.


Debugging IAM Issues

When your app throws AccessDenied, here's how to figure out what went wrong:

Step 1: Read the Error Message

An error occurred (AccessDeniedException) when calling the GetItem operation:
User: arn:aws:sts::123456789:assumed-role/MyAppTaskRole/abc123
is not authorized to perform: dynamodb:GetItem 
on resource: arn:aws:dynamodb:us-east-1:123456789:table/Users
Enter fullscreen mode Exit fullscreen mode

This tells you exactly:

  • Who tried: MyAppTaskRole
  • What they tried: dynamodb:GetItem
  • On what: table/Users

Step 2: Check the Policy

# List policies attached to the role
aws iam list-attached-role-policies --role-name MyAppTaskRole
aws iam list-role-policies --role-name MyAppTaskRole

# View a specific inline policy
aws iam get-role-policy --role-name MyAppTaskRole --policy-name MyAppPermissions
Enter fullscreen mode Exit fullscreen mode

Step 3: Simulate the Policy

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789:role/MyAppTaskRole \
  --action-names dynamodb:GetItem \
  --resource-arns arn:aws:dynamodb:us-east-1:123456789:table/Users
Enter fullscreen mode Exit fullscreen mode

This tells you if the permission would be allowed or denied without actually making the call.

Step 4: Check for Deny Policies

Remember: an explicit Deny always overrides an Allow. Check for:

  • Service Control Policies (SCPs) at the organization level
  • Permission boundaries on the role
  • Resource-based policies on the target service

How TurboDeploy Handles IAM

When you deploy through TurboDeploy, we create least-privilege IAM roles in your AWS account:

Role Purpose Permissions
TurboDeployServiceRole TurboDeploy manages your infrastructure ECS, ECR, ALB, Route 53 (scoped to your resources)
{app}-TaskRole Your app's runtime permissions Only what your app needs (S3, DB, etc.)
{app}-ExecutionRole ECS agent role ECR pull + CloudWatch Logs
{app}-CICDRole GitHub Actions deploy ECR push + ECS update (OIDC, no keys)

Every role is scoped to specific resources (no wildcards), tagged with your app name and environment, and visible in your AWS console. You can audit everything through CloudTrail.

We never ask for AdministratorAccess. We never store your credentials. Everything runs in your account.


TL;DR

Concept What to Remember
Users Legacy for humans. Use Identity Center (SSO) instead.
Groups Organize users. Attach policies to groups, not users.
Roles Use for everything. Services, CI/CD, cross-account. Temporary creds.
Policies JSON rules. Effect + Action + Resource. No wildcards in prod.
Trust Policy WHO can assume the role (the Principal).
Permission Policy WHAT the role can do (the Actions + Resources).
Task Role Your app code uses this (S3, DynamoDB, SQS).
Execution Role ECS agent uses this (ECR pull, CloudWatch Logs).
OIDC Modern CI/CD auth. No access keys. GitHub Actions → STS → temp creds.
Least privilege Always. If "Action": "*", you've already failed.

Don't want to deal with IAM policies? TurboDeploy creates least-privilege roles for every deployment, scoped to your resources and visible in your console. No IAM expertise needed.

Join the waitlist

Top comments (0)