IAM decides who can do what in an AWS account. It feels complicated because there are many policy types, but the basics fit on one page.
The building blocks
| Thing | What it is | Use it for |
|---|---|---|
| IAM user | A long-lived identity with a password and/or access keys | Rarely now: break-glass or legacy tools. People should use IAM Identity Center |
| Group | A set of users that share policies | Managing users, if you have them |
| Role | An identity with no permanent credentials, assumed for a time | Workloads (EC2, Lambda, ECS), cross-account access, Identity Center users |
| Policy | A JSON document that allows or denies actions on resources | Attached to users, groups, roles, or to resources like S3 buckets |
Reading a policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadReports",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::acme-reports",
"arn:aws:s3:::acme-reports/*"
]
}
]
}
- Effect:
AlloworDeny. - Action: API calls, as
service:Operation. Wildcards work (s3:Get*) but be careful. - Resource: the ARN(s) the statement applies to. Note
ListBucketapplies to the bucket ARN andGetObjectto the objects (/*). - Condition (optional): extra checks such as source IP, tags or MFA.
How AWS decides
- Everything starts as denied.
- An explicit Allow in an applicable policy can allow it.
- An explicit Deny anywhere always wins.
- Organisation guardrails (SCPs), permission boundaries and session policies can only reduce what is allowed, never add to it.
So "I attached AdministratorAccess and still get AccessDenied" usually means a Deny somewhere else, often an SCP.
Roles and trust policies
A role has two policies:
- Trust policy: who may assume it.
- Permission policies: what it can do once assumed.
Trust policy for EC2:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}]
}
Worked example: EC2 reads one bucket, no keys
aws iam create-role --role-name app-reports-reader \
--assume-role-policy-document file://trust-ec2.json
aws iam put-role-policy --role-name app-reports-reader \
--policy-name read-acme-reports --policy-document file://read-reports.json
aws iam create-instance-profile --instance-profile-name app-reports-reader
aws iam add-role-to-instance-profile --instance-profile-name app-reports-reader --role-name app-reports-reader
aws ec2 associate-iam-instance-profile --instance-id i-0abc1234def567890 \
--iam-instance-profile Name=app-reports-reader
On the instance:
aws sts get-caller-identity # shows the assumed role
aws s3 ls s3://acme-reports/
The SDKs and CLI pick up the role credentials automatically from the instance metadata service. No keys on disk.
Tip
Prefer AWS managed policies to start (for example
ReadOnlyAccess), then replace them with customer managed policies scoped to your resources. IAM Access Analyzer can generate a least-privilege policy from CloudTrail activity.
Checking what someone can do
aws iam list-attached-role-policies --role-name app-reports-reader
aws iam list-role-policies --role-name app-reports-reader
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::111111111111:role/app-reports-reader \
--action-names s3:GetObject s3:PutObject \
--resource-arns arn:aws:s3:::acme-reports/2026/report.csv \
--query "EvaluationResults[].[EvalActionName,EvalDecision]" --output table
Beginner mistakes
| Mistake | Better |
|---|---|
| Access keys for an app on EC2 | Instance profile role |
"Action": "*", "Resource": "*" to "make it work" | Start from a managed policy, narrow with Access Analyzer |
| Root user for daily work | MFA on root, lock it away, use Identity Center |
| Sharing one IAM user among people | One identity per person through Identity Center |
Prevent it next time
- Turn on MFA for the root user and any remaining IAM users.
- Run IAM Access Analyzer for unused access and external access findings monthly.
- Use roles for everything that runs in AWS.