assumeRole and trust policy: why IAM users aren't enough

2025/11/05
awsiam

sts:AssumeRole is an API action. A service or user calls it to get temporary credentials for a role. Trust policy is the policy attached to that role that controls who is allowed to call sts:AssumeRole on it. Not a separate concept. Just a policy governing one specific action.

The examples in this article build these resources:

NameTypePurpose
s3-reader-roleIAM Rolethe role ECS/Lambda assumes
trust policyPolicy on the roleallows ecs.amazonaws.com and lambda.amazonaws.com to call sts:AssumeRole
s3-reader-policy.jsonPermission policy attached to the rolegrants s3:GetObject and s3:ListBucket on my-app-assets

IAM users attach to policies. Policies define what actions are allowed on which resources. Simple.

Then assumeRole and trust policy show up. Two terms sitting on top of everything, and the docs don't explain why they exist before explaining how they work.

why not just use IAM users

An IAM user has permanent credentials. Access key, secret key. They work forever until someone manually revokes them.

This breaks in three ways:

  • A key leaks in a git commit. It stays valid until someone notices.
  • A partner team in another AWS account needs access. Now you're managing someone else's permanent identity.
  • An AWS service (ECS, Lambda) needs permissions. Services aren't users. They can't hold permanent keys.

All three cases have the same shape: something needs temporary, scoped access instead of a permanent key.

assumeRole returns temporary credentials that expire on their own. No cleanup.

the simple path: AWS services

ECS needs to read from S3. Two steps.

step 1: create the role and set who can assume it

Go to IAM > Roles > Create role.

First thing AWS asks: "Who will use this role?" Select AWS service, pick ECS.

This creates the trust policy. It's not a separate thing. It's baked into the role at creation. You can see it later under the Trust relationships tab:

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

One role, one trust policy. The Principal field accepts multiple services in an array. No need to create separate trust policies.

Name the role s3-reader-role.

step 2: attach permissions to the role

Next screen. Attach a permission policy that defines what this role can do. Save the following as s3-reader-policy.json:

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

Attach this to s3-reader-role:

CLI:

aws iam create-policy \
  --policy-name s3-reader-policy \
  --policy-document file://s3-reader-policy.json
 
aws iam attach-role-policy \
  --role-name s3-reader-role \
  --policy-arn arn:aws:iam::ACCOUNT-ID:policy/s3-reader-policy

Console: IAM > Roles > s3-reader-role > Permissions > Add permissions > Attach policies > select s3-reader-policy.

Done. Go to the ECS task definition, set the Task Role to s3-reader-role.

At runtime: ECS task starts → ECS service calls sts:AssumeRole → STS checks the trust policy, finds ecs.amazonaws.com listed → grants temporary credentials to the task → task uses those credentials to access S3.

No third step. AWS services handle the assume call internally.

what if there's no assumeRole

Without roles, the permissions have to live on the S3 side. The bucket policy lists every service, every account, every task that needs access:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs.amazonaws.com"
      },
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::my-app-assets",
        "arn:aws:s3:::my-app-assets/*"
      ]
    },
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::my-app-assets",
        "arn:aws:s3:::my-app-assets/*"
      ]
    }
  ]
}

Every new consumer means another Statement block on the bucket. Ten services reading S3, ten blocks. And this is just one bucket. Multiply by every resource they touch.

With assumeRole, the bucket doesn't need to know who's coming. The role carries the permissions. Add a new service to the trust policy, it inherits the same S3 access. The bucket policy stays untouched.

the full path: IAM users and cross-account

A developer in another AWS account needs to read your S3 bucket. Three steps.

step 1 and 2: same as above

Create the role, set the trust policy to trust the other account:

{
  "Principal": {
    "AWS": "arn:aws:iam::OTHER-ACCOUNT-ID:root"
  }
}

Attach S3 read permissions. Same as before.

step 3: the other side also needs permission

In the other account, go to IAM > Users > [the user] > Permissions, add a policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "arn:aws:iam::YOUR-ACCOUNT-ID:role/s3-reader-role"
    }
  ]
}

This is the extra step that AWS services don't need. A human (or their scripts) has to prove they're allowed to ask.

Both sides must agree. The role says "I trust this account" (trust policy). The user says "I'm allowed to assume this role" (user policy). Either side says no, access denied.

Same runtime flow as above: the user calls sts:AssumeRole, STS checks both sides, and returns temporary credentials (default: one hour).

the mental model

An IAM user with permanent keys is a house key. Works forever until the locks change.

assumeRole is a hotel key card. Opens one room, for one night, stops working at checkout.

Trust policy = the hotel's guest list (who can check in) User policy = the reservation (proof the guest is allowed to ask for a room) Temporary credentials = the key card (works for a limited time, then dies)