AWS IAM Roles and Policies

By Vishvesh Patel · DevOps EngineerUpdated September 12, 2026

Trust policies versus permission policies, assuming roles with STS, managed and inline policies, permissions boundaries, and how to debug an assume-role failure.

A role is an identity with no permanent credentials. Something assumes it, receives temporary credentials, and acts with its permissions until they expire.

That is why roles are preferred almost everywhere: there is no lasting secret to leak, rotate, or find committed to a repository.

Every role has two policies

This trips up nearly everyone at first, and keeping them straight makes IAM errors far easier to read.

The trust policy: who may assume this role:

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

The permissions policy: what the role can do once assumed:

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

The error message tells you which one failed:

Denied onProblem
sts:AssumeRoleTrust policy (you are not permitted to assume it)
Any other actionPermissions policy: you assumed it but it cannot do that

Principals in a trust policy

// An AWS service
"Principal": { "Service": "lambda.amazonaws.com" }

// A specific role in another account
"Principal": { "AWS": "arn:aws:iam::444455556666:role/CIDeployer" }

// A whole account. Broader, and means anyone in it with the right IAM
"Principal": { "AWS": "arn:aws:iam::444455556666:root" }

// A federated identity provider, e.g. GitHub Actions via OIDC
"Principal": { "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com" }

Naming :root means "any principal in that account whose own IAM policy allows it", not the root user. It is broader than most people intend; naming a specific role is better.

Conditions narrow it considerably, and for cross-account access an external ID protects against the confused deputy problem:

{
  "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::444455556666:root" },
  "Action": "sts:AssumeRole",
  "Condition": {
    "StringEquals": { "sts:ExternalId": "acme-7f3c1e2a" }
  }
}

For OIDC federation, the condition is what stops any repository on the provider assuming your role:

"Condition": {
  "StringEquals": {
    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
  },
  "StringLike": {
    "token.actions.githubusercontent.com:sub": "repo:acme/payments:ref:refs/heads/main"
  }
}

Omitting that sub condition is a serious mistake, without it, any GitHub repository anywhere can assume the role.

Assuming a role

aws sts assume-role \
  --role-arn arn:aws:iam::111122223333:role/DeployRole \
  --role-session-name ci-build-482
{
  "Credentials": {
    "AccessKeyId": "ASIA...",
    "SecretAccessKey": "...",
    "SessionToken": "...",
    "Expiration": "2026-09-12T12:30:00Z"
  }
}

--role-session-name appears in CloudTrail, so putting a build number or username there turns an anonymous session into an auditable one. Use it meaningfully.

Sessions default to one hour and can be extended up to the role's maximum session duration, which itself caps at twelve hours. Role chaining, assuming a role from within an assumed role, is limited to one hour regardless.

In practice you rarely call assume-role by hand. A named profile does it for you:

# ~/.aws/config
[profile prod-deploy]
role_arn = arn:aws:iam::111122223333:role/DeployRole
source_profile = default
mfa_serial = arn:aws:iam::111122223333:mfa/alice
aws s3 ls --profile prod-deploy

Policy types

TypeAttached toNotes
AWS managedPrincipalsMaintained by AWS, generally too broad
Customer managedPrincipalsYours, reusable, versioned (prefer these)
InlineOne principalInvisible in policy listings, not reusable
Resource-basedA resourceS3 bucket policies, KMS key policies, SQS queue policies
Permissions boundaryA principalA ceiling, never a grant
Service control policyAn Organization OUA ceiling across accounts

Prefer customer-managed policies to inline ones. An inline policy does not appear when you list policies, cannot be attached elsewhere, and is easy to miss in an audit. AWS managed policies are convenient starting points and almost always grant more than needed, AmazonS3FullAccess on a role that reads one prefix is a real risk with no benefit.

Customer-managed policies are versioned, with up to five versions kept, so rolling back a bad change is straightforward:

aws iam list-policy-versions --policy-arn arn:aws:iam::111122223333:policy/PaymentsRead
aws iam set-default-policy-version --policy-arn <arn> --version-id v3

Permissions boundaries

A boundary sets the maximum permissions a principal can have. The effective permissions are the intersection of its policies and its boundary. A boundary never grants anything.

The main use is safe delegation: letting a team create roles for their own workloads without being able to escalate to administrator.

{
  "Effect": "Allow",
  "Action": "iam:CreateRole",
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "iam:PermissionsBoundary": "arn:aws:iam::111122223333:policy/TeamBoundary"
    }
  }
}

That condition means they may create roles only if they attach your boundary, so anything they create is capped by it.

This is also a common cause of confusing denials. A principal with AdministratorAccess and a restrictive boundary is not an administrator, and nothing in the error says so.

How workloads get roles

ComputeMechanism
EC2Instance profile
LambdaExecution role
ECSTask role for your code; task execution role is for pulling images and writing logs
EKSPod Identity, or IRSA
CodeBuildService role
GitHub Actions / GitLab CIOIDC federation

The ECS distinction is worth stating clearly because it causes a lot of confusion: the task execution role is used by the ECS agent to pull the image from ECR and write logs; the task role is what your application code uses to call AWS. Putting your application's S3 permissions on the execution role does not work.

The SDK finds these credentials automatically through the default provider chain, so application code should never reference them explicitly.

Debugging

# Who am I actually?
aws sts get-caller-identity

# Would this be allowed?
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::111122223333:role/DeployRole \
  --action-names s3:PutObject \
  --resource-arns 'arn:aws:s3:::acme-artifacts/build.zip'

# What is attached?
aws iam list-attached-role-policies --role-name DeployRole
aws iam list-role-policies --role-name DeployRole          # inline ones
aws iam get-role --role-name DeployRole --query 'Role.AssumeRolePolicyDocument'

Run both list-attached-role-policies and list-role-policies. The second returns inline policies, which the first does not, and an unnoticed inline policy is a classic source of "where is this permission coming from".

Common causes, in the order worth checking:

SymptomCause
Denied on sts:AssumeRoleTrust policy does not name your principal, or a condition is unmet
Denied on the action after assumingPermissions policy is missing it
AdministratorAccess still deniedAn SCP or permissions boundary is capping it
Works for you, not for CIDifferent principal (check get-caller-identity in the pipeline)
Cross-account failsBoth sides must allow it; check the external ID condition

Quick reference

TaskCommand
Who am Iaws sts get-caller-identity
Assume a roleaws sts assume-role --role-arn <arn> --role-session-name <name>
Trust policyaws iam get-role --role-name <n> --query 'Role.AssumeRolePolicyDocument'
Attached policiesaws iam list-attached-role-policies --role-name <n>
Inline policiesaws iam list-role-policies --role-name <n>
Simulateaws iam simulate-principal-policy --policy-source-arn <arn> --action-names <action>
Policy versionsaws iam list-policy-versions --policy-arn <arn>
Unused permissionsaws iam generate-service-last-accessed-details --arn <arn>

Key takeaways

  • Two policies per role. Trust decides who may assume it; permissions decide what it can do.
  • The denied action tells you which one failed: sts:AssumeRole means trust.
  • :root as a principal means the whole account, which is broader than most people intend.
  • OIDC trust policies must condition on sub, or any repository can assume the role.
  • Prefer customer-managed policies. Inline ones are invisible in listings and unreusable.
  • A permissions boundary is a ceiling, never a grant: and a silent cause of denials.
  • ECS task role is for your code; task execution role is for the agent. They are not interchangeable.
  • Put something meaningful in --role-session-name so CloudTrail is useful.

Check your understanding

Exercise

An OIDC trust policy that is too open

A GitHub Actions OIDC trust policy conditions only on the audience claim, not on sub. What is the consequence?

Exercise

ECS task cannot reach S3

An ECS task pulls its image and writes logs successfully, but the application gets AccessDenied calling S3. S3 permissions were added to the task execution role. What is wrong?


Frequently Asked Questions

What is the difference between a trust policy and a permissions policy?

Both attach to a role and answer different questions. The trust policy names which principals are allowed to assume the role: an AWS service, another account, a federated identity provider. The permissions policy defines what the role can do once assumed. When you get AccessDenied, the action named in the error tells you which to look at: a denial on sts:AssumeRole is a trust policy problem, while a denial on anything else means you assumed the role successfully and its permissions are lacking.

How do I assume an IAM role from the CLI?

Either explicitly with aws sts assume-role --role-arn <arn> --role-session-name <name>, which returns temporary credentials, or, much more conveniently, by configuring a named profile in ~/.aws/config with role_arn and source_profile, after which aws s3 ls --profile prod-deploy handles the assumption transparently. Adding mfa_serial to the profile prompts for an MFA code automatically. Always set a meaningful session name, since it appears in CloudTrail and is what makes role activity attributable to a person or a build.

What is the difference between managed and inline policies?

A managed policy is a standalone object that can be attached to many principals, is versioned so changes can be rolled back, and appears when you list policies. An inline policy is embedded in a single principal, cannot be reused, has no version history, and does not show up in policy listings, which makes it easy to miss during an audit and a frequent source of unexplained permissions. Prefer customer-managed policies. AWS managed policies are useful starting points but are generally far broader than a specific workload needs.

What is a permissions boundary and when do I need one?

A permissions boundary caps the maximum permissions a principal can have; effective permissions are the intersection of its policies and the boundary. It never grants anything. The main use is safe delegation. Allowing a team to create IAM roles for their own workloads while requiring that every role they create carries your boundary, so nothing they create can exceed it. It is also a common cause of confusing denials, since a principal with AdministratorAccess and a restrictive boundary is not an administrator and the error message does not mention the boundary.

Why does my ECS task get AccessDenied when calling AWS APIs?

Almost certainly because the permissions are on the wrong role. ECS uses two: the task execution role, assumed by the ECS agent to pull images from ECR and write logs to CloudWatch, and the task role, which your application code uses via the SDK. If image pulls and logging work but your application's API calls fail, the permissions were added to the execution role and need to be on the task role instead.

How do I set up cross-account role access safely?

Name the specific role in the other account rather than using :root, which grants to any principal in that account whose own IAM permits it. Add an external ID condition when a third party will assume the role, which protects against the confused deputy problem where another customer of that third party could otherwise have them assume your role. Remember both sides must allow it, the trust policy on your role and an IAM policy in the calling account, and if the target resource uses KMS encryption, the key policy needs the external principal as well.

Continue Learning

Explore Related Topics

When it breaks

Related Resources