Who can do what: IAM
Users, roles, policies, and why root keys are dangerous.
Part 1: The problem
Leo opened QuickCart’s AWS account with his email address and a password. That login is the root user. It can do anything in the account, including close it.
He wanted the command line working quickly. So he created access keys for the root user and pasted them into a config file in the API’s code. It worked first time.
A week later, Noor pushed a copy of the code to a public repository, to show a friend how the API worked. The config file went with it.
Two things went wrong. The key belonged to an identity that could do everything, and it was a long-lived key sitting in a file. IAM is how QuickCart fixes both.
Part 2: The idea
IAM — Identity and Access Management — is the part of AWS that answers one question for every single request: is this caller allowed to do this, to this thing?
Every click in the console and every API call goes through it. There is no way around it.
Who can make a request
| Identity | What it is | QuickCart uses it for |
|---|---|---|
| Root user | The login you created the account with. Can do everything, and no IAM policy can limit it | Almost nothing. Locked away with MFA |
| IAM user | A named person or program with its own password or access keys, which last until someone deletes them | People on small teams. Modern setups prefer single sign-on |
| IAM group | A set of users that share the same policies | Giving every developer the same access in one place |
| IAM role | An identity with no password and no permanent keys. Something assumes it and gets temporary credentials | The API servers, the pipeline, and engineers doing risky jobs |
What they’re allowed to do
A policy is a JSON document listing what is allowed or denied. You attach it to a user, a group or a role. On its own, an identity can do nothing: every permission comes from a policy.
Here is the policy QuickCart’s API servers need. It lets them list and read menu photos in one bucket, and nothing else:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListMenuPhotos",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::quickcart-menu-photos-111122223333"
},
{
"Sid": "ReadMenuPhotos",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::quickcart-menu-photos-111122223333/*"
}
]
}| Field | What it does | If you leave it out | When you’d change it |
|---|---|---|---|
Version | The version of the policy language. It is always 2012-10-17: a fixed value, not today’s date | AWS uses an old language version, and features like policy variables stop working | Never |
Statement | A list of rules. Each one allows or denies something | Required — the policy has no rules and is rejected | Add a statement for each separate permission |
Sid | A label for the statement, for people to read. Must be unique within the policy, letters and numbers only | Nothing changes. It’s optional | To describe what the statement is for |
Effect | Allow or Deny | Required | Deny only for guard rails, like “never delete” |
Action | Which API calls, written service:Action. Can be one string or a list. * is a wildcard | Required (or its opposite, NotAction) | When the app needs another call, such as s3:PutObject to upload |
Resource | Which things, by ARN — Amazon Resource Name, the unique ID of every AWS resource | Required for identity policies | To point at a different bucket or file path |
Two details catch everyone out:
ListBucketacts on the bucket, so its ARN has no slash.GetObjectacts on the files inside, so its ARN ends in/*. They are different resources, so they need different statements.- S3 bucket ARNs have no region or account number in them — that’s why there are three colons in a row. Most other services do include them, like
arn:aws:iam::111122223333:role/quickcart-api.
How AWS decides
Every request is checked in the same order:
| Step | The question | If yes |
|---|---|---|
| 1 | Start: the answer is deny | — |
| 2 | Does any attached policy have a Deny that matches? | Denied. Stop. Nothing can override it |
| 3 | Does any attached policy have an Allow that matches? | Allowed |
| 4 | Nothing matched | Denied. This is called the implicit deny |
Least privilege
Least privilege means giving each identity exactly the actions and resources it needs, and nothing more. The API reads menu photos, so it gets s3:GetObject on that one bucket — not s3:* on *.
It matters most when something goes wrong. If the API server is ever broken into, the attacker gets whatever the server was allowed to do. With least privilege, that is “read some pictures of burgers”. Without it, that is “delete every bucket”.
Roles instead of keys
An access key is a pair of strings — an ID starting AKIA and a secret — that works until someone deletes it. It can be copied, committed to git, or left on an old laptop, and it still works.
A role has no keys to leak. When something assumes the role, AWS hands it temporary credentials that expire, usually within hours, and renew automatically. A role has two policies, answering two separate questions:
| Policy | The question | For QuickCart’s API role |
|---|---|---|
| Trust policy | Who may assume this role? | The EC2 service — so servers can use it |
| Permissions policy | What can the role do once assumed? | Read menu photos, as above |
This is the trust policy the console writes when you choose EC2 as the trusted service:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}| Field | What it does | If you leave it out | When you’d change it |
|---|---|---|---|
Version | The policy language version, as before | Old language rules apply | Never |
Effect | Allow: the principal below may assume the role | Required | Almost never Deny in a trust policy |
Principal | Who is trusted. Here, the EC2 service, which fetches the credentials on the server’s behalf | Required in a trust policy — the role can’t be saved | ecs-tasks.amazonaws.com for containers, or another account’s ARN for cross-account access |
Action | sts:AssumeRole — the call that swaps a trusted identity for temporary credentials | Required | Other values exist for web identity and SAML logins, used when a pipeline signs in without stored keys, in Continuous delivery |
There’s no Resource in a trust policy: it is attached to the role, so the resource is the role itself.
The root user
The root user can’t be limited by any IAM policy. So QuickCart does three things, once:
| Rule | Why |
|---|---|
| Turn on MFA for root | A stolen password alone is no longer enough to sign in |
| Never create root access keys, and delete any that exist | A leaked root key is a leaked account |
| Use root only for the few jobs that need it, like closing the account | Everything else is done as an IAM identity with less power |
Part 3: See it
Below is the API server’s role. Pick a request, attach or detach statements, and check it. Try each request with only the first statement attached. Then attach the s3:* shortcut. Then attach the Deny.
The API server asks to
s3:GetObjectarn:aws:s3:::quickcart-menu-photos-111122223333/burger.jpgStatements attached to its role
Choose a statement to attach or detach it.
How AWS decides
- Start at deny. Every request is refused unless something allows it.
- Any matching Deny? If so, the answer is deny. Nothing can override it.
- Any matching Allow? If so, the answer is allow.
- Nothing matched? Still deny. This is the implicit deny.
What to notice: with only the least-privilege statement, reading the state file is denied without any Deny being written — that is the implicit deny doing its job. Attaching s3:* makes every request work, which is exactly why it’s tempting and exactly why it’s dangerous. And the Deny wins even when s3:* is attached.
Part 4: Do it
Free — IAM itself costs nothing, and the policy simulator never touches a real bucket.
You’ll check your root user is protected, write QuickCart’s least-privilege policy, give it to a role, and then prove what the role can and can’t do. About 20 minutes.
Sign in as an IAM identity with administrator access, not as root. If you only have the root login so far, Set up your computer shows how to create an everyday admin identity first.
Check the root user is protected
In the AWS console: IAMDashboard
Look at the Security recommendations panel.
You should seeGreen ticks for Root user has MFA and Root user has no active access keys. The exact wording may differ slightly.
If either is missing, fix it before anything else. Sign in as root, open the account menu at the top right, choose Security credentials, and use Assign MFA device — an authenticator app on your phone is fine. On the same page, delete any root access keys under Access keys.
Create the least-privilege policy
In the AWS console: IAMPoliciesCreate policy
In the Policy editor, switch from Visual to JSON. Delete what’s there and paste the policy from the idea, above:
quickcart-menu-photos-read.json{ "Version": "2012-10-17", "Statement": [ { "Sid": "ListMenuPhotos", "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::quickcart-menu-photos-111122223333" }, { "Sid": "ReadMenuPhotos", "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::quickcart-menu-photos-111122223333/*" } ] }The bucket doesn’t need to exist for this lab. Choose Next.
Field Enter Policy name quickcart-menu-photos-readDescription Read-only access to the menu photos bucketChoose Create policy.
You should seeA green banner: Policy quickcart-menu-photos-read created. It’s listed with Type: Customer managed — one you wrote, as opposed to the ones AWS provides.
Create a role that uses it
In the AWS console: IAMRolesCreate role
Field Enter Trusted entity type AWS service Service or use case EC2 Use case EC2 — the first option, “Allows EC2 instances to call AWS services on your behalf” Choose Next. In Permissions policies, search for
quickcart-menu-photos-readand tick it. Choose Next.Field Enter Role name quickcart-menu-readerDescription QuickCart API servers: read menu photosBefore you create it, look at Step 1: Select trusted entities on this page. It shows the trust policy, with
"Service": "ec2.amazonaws.com"— the same as in the idea. Choose Create role.You should seeRole quickcart-menu-reader created. Open it: the Permissions tab lists your one policy, and the Trust relationships tab lists the EC2 service.
Open the policy simulator
In the AWS console: IAMDashboardToolsPolicy simulator
The Tools panel is on the right of the IAM dashboard. The simulator opens in a new tab. If you can’t see the link, type
policy simulatorinto the console’s search bar.The simulator works out what a policy would allow without making any real request. Nothing is read, changed or charged.
On the left, under Users, Groups, and Roles, change the drop-down to Roles and choose
quickcart-menu-reader. Its policy appears, ticked.Test reading a photo
On the right:
Field Enter Select service Amazon S3 Select actions tick GetObject and DeleteObject Expand GetObject in the list of actions and set its resource:
Field Enter Resource arn:aws:s3:::quickcart-menu-photos-111122223333/burger.jpgDo the same for DeleteObject, with the same ARN. Then choose Run Simulation.
You should seeGetObject: allowed. DeleteObject: denied, with a note that it’s implicitly denied — no statement matched. Nobody wrote “don’t delete”; it simply wasn’t allowed.
Test a bucket it shouldn't touch
Change the GetObject resource to the Terraform state bucket:
Field Enter Resource arn:aws:s3:::quickcart-tfstate-111122223333/dev/terraform.tfstateChoose Run Simulation again.
You should seeGetObject: denied. Same action, different bucket. The role can read menu photos and nothing else — least privilege, proven.
Clean-up step: Clean up
Nothing here costs money, but unused roles and policies are clutter that someone will one day be afraid to delete.
In the AWS console: IAMRolesquickcart-menu-readerDelete
Type
quickcart-menu-readerto confirm and choose Delete.In the AWS console: IAMPoliciesquickcart-menu-photos-readActionsDelete
Type
quickcart-menu-photos-readto confirm and choose Delete.You should seeNeither the role nor the policy appears in its list. Close the simulator tab. Keep root’s MFA on for good.
Part 5: Check yourself
One policy on a role allows s3:DeleteObject on every bucket. Another policy on the same role denies s3:DeleteObject on every bucket. What happens when the role tries to delete a file?
A brand-new role has no policies attached. What can it do?
QuickCart's API server needs to read menu photos from S3. What is the safest way to give it access?
A role has a trust policy and a permissions policy. What does the trust policy decide?
A policy allows s3:GetObject on arn:aws:s3:::quickcart-menu-photos-111122223333, with no /* on the end. Reading burger.jpg from that bucket fails. Why?
When should you sign in as the root user?
Part 6: Cheat sheet
| Term | In one line |
|---|---|
| Root user | The account’s first login. No IAM policy can limit it. MFA on, no access keys, almost never used |
| IAM user | A named identity with long-lived credentials |
| IAM group | Users that share the same policies |
| IAM role | No password, no keys. Assumed, and hands out temporary credentials |
| Policy | JSON: Effect + Action + Resource in each statement |
Version | Always 2012-10-17 |
| Implicit deny | Nothing allowed it, so it’s denied. The default |
| Explicit deny | A matching Deny. Beats every Allow |
| Least privilege | Exactly the actions and resources needed, nothing more |
| Trust policy | Who may assume a role |
| Permissions policy | What the role can do |
| S3 ARNs | Bucket: arn:aws:s3:::name. Files: arn:aws:s3:::name/* |
| AccessDenied? | Look for the missing Allow, or a Deny, in the policies. Never switch to root |