IAM Roles & When to Use Them
An IAM (Identity and Access Management) role is an identity in your AWS account that has permissions, but no permanent password or access keys attached to it. Instead of belonging to one person, a role is assumed by whoever (or whatever) is allowed to use it. When something assumes a role, AWS hands back short-lived credentials that expire on their own. This is the single most important pattern in AWS security: roles let you grant access without ever creating or storing long-lived secrets.
What an IAM role actually is
Think of a role as a hat anyone trusted can put on. While they wear the hat, they get the permissions the hat carries. When they take it off (the credentials expire), those permissions are gone.
A role is built from two separate pieces, and you need both for it to work:
| Piece | Question it answers | Example |
|---|---|---|
| Trust policy | Who is allowed to assume this role? | ”The EC2 service” or “Account 222233334444” |
| Permission policy | What can the role do once assumed? | ”Read objects from this S3 bucket” |
The trust policy is also called the assume-role policy document. It is attached to the role itself and lists the trusted principals (the AWS service, user, or account allowed to step into the role). The permission policies are the same kind of policies you attach to a user or group — they say what API calls are allowed.
Gotcha: A role needs BOTH a trust policy and permissions. If your permission policy is perfect but the trust policy does not name the right principal, the assume call simply fails with
AccessDenied— and nothing about the permission policy will hint at the real problem. When a role “can’t be assumed”, check the trust policy first.
Roles vs users — when to use which
An IAM user has long-lived credentials (a password and/or access keys) that sit around forever until you rotate them. A role hands out temporary credentials that expire automatically (typically after 1 hour, configurable up to 12 hours). Temporary and auto-expiring is almost always safer.
| Scenario | Use a role | Use a user |
|---|---|---|
| EC2 instance or Lambda needs AWS access | Yes (always) | Never |
| Application running in AWS | Yes | Never |
| Granting Account B access to Account A | Yes (cross-account role) | No |
| Human signing in via your company SSO | Yes (federation) | No |
| A legacy on-prem script with no other option | Prefer a role + STS | Only if unavoidable |
The rule of thumb: prefer roles for every workload and every cross-account need. Reach for an IAM user only when nothing can assume a role for you — and even then, consider IAM Identity Center first.
When NOT to overthink it
If your code runs inside AWS (EC2, ECS, Lambda, EKS), you should never put access keys in it. Attach a role and the SDK picks up the temporary credentials automatically. No keys to leak, no keys to rotate.
Creating a role (console)
Here we create a role that an EC2 (Elastic Compute Cloud, AWS’s virtual servers) instance can assume to read from S3.
- Open the IAM console and choose Roles in the left menu.
- Click Create role.
- Under Trusted entity type, pick AWS service, then choose EC2 as the use case. (This pre-builds the trust policy for you.)
- Click Next, then search for and select the AmazonS3ReadOnlyAccess managed policy.
- Click Next, name the role
ec2-s3-readonly, and click Create role.
Creating a role (AWS CLI)
With the CLI (Command Line Interface) you supply the trust policy as a JSON file, then attach permissions.
First, save the trust policy to trust-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
Then create the role and attach a permission policy:
aws iam create-role \
--role-name ec2-s3-readonly \
--assume-role-policy-document file://trust-policy.json
aws iam attach-role-policy \
--role-name ec2-s3-readonly \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
Output:
{
"Role": {
"Path": "/",
"RoleName": "ec2-s3-readonly",
"RoleId": "AROA1A2B3C4D5E6F7G8H9",
"Arn": "arn:aws:iam::111122223333:role/ec2-s3-readonly",
"CreateDate": "2026-06-15T10:42:11+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
}
}
A cross-account trust policy
To let another account assume a role, the Principal names that account instead of a service. This is the foundation of secure cross-account access.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::222233334444:root" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "sts:ExternalId": "devcraftly-prod-2026" }
}
}
]
}
The ExternalId condition is a shared secret that protects against the “confused deputy” problem — always use one when a third party assumes your role.
Infrastructure as Code
Defining roles in code makes the trust relationship reviewable and repeatable. Here is the same EC2 role in Terraform:
resource "aws_iam_role" "ec2_s3_readonly" {
name = "ec2-s3-readonly"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "ec2.amazonaws.com" }
}]
})
}
resource "aws_iam_role_policy_attachment" "s3_read" {
role = aws_iam_role.ec2_s3_readonly.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
Tip: IAM roles, policies, and trust relationships are completely free. You are never billed for creating or assuming a role — only for the AWS resources the role’s permissions let you use. There is no cost reason to avoid making narrowly scoped roles.
Best practices
- Use roles, not users, for every application and every piece of compute running in AWS — attach the role and let the SDK fetch temporary credentials.
- Always confirm the trust policy names the correct principal; a wrong trust policy is the most common reason an assume-role call fails silently.
- Apply least privilege to the permission policies, and scope the trust policy to the smallest set of principals that need it.
- For cross-account roles assumed by a third party, require an
ExternalIdcondition to prevent the confused-deputy problem. - Keep session durations as short as your workflow allows; longer sessions mean longer-lived credentials if they leak.
- Define roles as Infrastructure as Code so trust changes go through review, not the console.