Laptop to Production

Who can do what: IAM

Users, roles, policies, and why root keys are dangerous.

Time
35 minutes
Level
Beginner

Part 1: The problem

QuickCart, the first AWS account

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

IdentityWhat it isQuickCart uses it for
Root userThe login you created the account with. Can do everything, and no IAM policy can limit itAlmost nothing. Locked away with MFA
IAM userA named person or program with its own password or access keys, which last until someone deletes themPeople on small teams. Modern setups prefer single sign-on
IAM groupA set of users that share the same policiesGiving every developer the same access in one place
IAM roleAn identity with no password and no permanent keys. Something assumes it and gets temporary credentialsThe 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:

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/*"
    }
  ]
}
FieldWhat it doesIf you leave it outWhen you’d change it
VersionThe version of the policy language. It is always 2012-10-17: a fixed value, not today’s dateAWS uses an old language version, and features like policy variables stop workingNever
StatementA list of rules. Each one allows or denies somethingRequired — the policy has no rules and is rejectedAdd a statement for each separate permission
SidA label for the statement, for people to read. Must be unique within the policy, letters and numbers onlyNothing changes. It’s optionalTo describe what the statement is for
EffectAllow or DenyRequiredDeny only for guard rails, like “never delete”
ActionWhich API calls, written service:Action. Can be one string or a list. * is a wildcardRequired (or its opposite, NotAction)When the app needs another call, such as s3:PutObject to upload
ResourceWhich things, by ARN — Amazon Resource Name, the unique ID of every AWS resourceRequired for identity policiesTo point at a different bucket or file path

Two details catch everyone out:

  • ListBucket acts on the bucket, so its ARN has no slash. GetObject acts 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:

StepThe questionIf yes
1Start: the answer is deny—
2Does any attached policy have a Deny that matches?Denied. Stop. Nothing can override it
3Does any attached policy have an Allow that matches?Allowed
4Nothing matchedDenied. This is called the implicit deny
Key point: Everything is denied unless a policy allows it, and an explicit Deny beats every Allow.

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:

PolicyThe questionFor QuickCart’s API role
Trust policyWho may assume this role?The EC2 service — so servers can use it
Permissions policyWhat 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:

trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
FieldWhat it doesIf you leave it outWhen you’d change it
VersionThe policy language version, as beforeOld language rules applyNever
EffectAllow: the principal below may assume the roleRequiredAlmost never Deny in a trust policy
PrincipalWho is trusted. Here, the EC2 service, which fetches the credentials on the server’s behalfRequired in a trust policy — the role can’t be savedecs-tasks.amazonaws.com for containers, or another account’s ARN for cross-account access
Actionsts:AssumeRole — the call that swaps a trusted identity for temporary credentialsRequiredOther 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:

RuleWhy
Turn on MFA for rootA stolen password alone is no longer enough to sign in
Never create root access keys, and delete any that existA leaked root key is a leaked account
Use root only for the few jobs that need it, like closing the accountEverything 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 request

The API server asks to

Action s3:GetObject
Resource arn:aws:s3:::quickcart-menu-photos-111122223333/burger.jpg

Statements attached to its role

Choose a statement to attach or detach it.

How AWS decides

  1. Start at deny. Every request is refused unless something allows it.
  2. Any matching Deny? If so, the answer is deny. Nothing can override it.
  3. Any matching Allow? If so, the answer is allow.
  4. Nothing matched? Still deny. This is the implicit deny.
Not checked yet
ReadyPick a request, attach or detach statements, then choose Check the request.

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.

  1. Check the root user is protected

    In the AWS console: IAMDashboard

    Look at the Security recommendations panel.

    You should see

    Green 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.

  2. 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.

    FieldEnter
    Policy namequickcart-menu-photos-read
    DescriptionRead-only access to the menu photos bucket

    Choose Create policy.

    You should see

    A 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.

  3. Create a role that uses it

    In the AWS console: IAMRolesCreate role

    FieldEnter
    Trusted entity typeAWS service
    Service or use caseEC2
    Use caseEC2 — the first option, “Allows EC2 instances to call AWS services on your behalf”

    Choose Next. In Permissions policies, search for quickcart-menu-photos-read and tick it. Choose Next.

    FieldEnter
    Role namequickcart-menu-reader
    DescriptionQuickCart API servers: read menu photos

    Before 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 see

    Role quickcart-menu-reader created. Open it: the Permissions tab lists your one policy, and the Trust relationships tab lists the EC2 service.

  4. 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 simulator into 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.

  5. Test reading a photo

    On the right:

    FieldEnter
    Select serviceAmazon S3
    Select actionstick GetObject and DeleteObject

    Expand GetObject in the list of actions and set its resource:

    FieldEnter
    Resourcearn:aws:s3:::quickcart-menu-photos-111122223333/burger.jpg

    Do the same for DeleteObject, with the same ARN. Then choose Run Simulation.

    You should see

    GetObject: allowed. DeleteObject: denied, with a note that it’s implicitly denied — no statement matched. Nobody wrote “don’t delete”; it simply wasn’t allowed.

  6. Test a bucket it shouldn't touch

    Change the GetObject resource to the Terraform state bucket:

    FieldEnter
    Resourcearn:aws:s3:::quickcart-tfstate-111122223333/dev/terraform.tfstate

    Choose Run Simulation again.

    You should see

    GetObject: denied. Same action, different bucket. The role can read menu photos and nothing else — least privilege, proven.

  7. 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-reader to confirm and choose Delete.

    In the AWS console: IAMPoliciesquickcart-menu-photos-readActionsDelete

    Type quickcart-menu-photos-read to confirm and choose Delete.

    You should see

    Neither 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

On one card
TermIn one line
Root userThe account’s first login. No IAM policy can limit it. MFA on, no access keys, almost never used
IAM userA named identity with long-lived credentials
IAM groupUsers that share the same policies
IAM roleNo password, no keys. Assumed, and hands out temporary credentials
PolicyJSON: Effect + Action + Resource in each statement
VersionAlways 2012-10-17
Implicit denyNothing allowed it, so it’s denied. The default
Explicit denyA matching Deny. Beats every Allow
Least privilegeExactly the actions and resources needed, nothing more
Trust policyWho may assume a role
Permissions policyWhat the role can do
S3 ARNsBucket: 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

Finished the build and the quiz? Mark it done to fill in its stop on the route map.