IAM: who can do what
Users, groups, policies and roles, clicked through in the real console: write a least-privilege policy, give it to a group, add a user, set up MFA and prove what the user can and cannot do.
By the end of this lesson you'll be able to
0 of 6 completeEvery request asks one question
Every click in the console and every aws command is an API request, and IAM answers one question for each
one: is this identity allowed to do this action on this resource? The default answer is no.
| Piece | What it is | Example |
|---|---|---|
| User | a person or app with long-term credentials | demo-dev |
| Group | a set of users that share permissions | developers |
| Role | an identity anyone trusted can assume for a while, with temporary credentials | an EC2 server, a CI job, an admin from another account |
| Policy | a JSON document listing what's allowed or denied | academy-s3-read |
Find out who you are right now:
$ aws sts get-caller-identity { "UserId": "AIDA…", "Account": "111122223333", "Arn": "arn:aws:iam::111122223333:user/anythinkX" }
aws sts get-caller-identityawsthe AWS command line
The screenshots and outputs in this lesson come from a real practice account. The account ID is replaced with
111122223333, the example ID AWS uses in its own documentation, and other users' names are blurred.
Reading a policy
This policy lets someone list buckets and read objects, and nothing else:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "ReadOnlyS3", "Effect": "Allow", "Action": ["s3:ListAllMyBuckets", "s3:ListBucket", "s3:GetObject"], "Resource": "*" } ] }
| Field | Meaning |
|---|---|
Effect | Allow or Deny |
Action | API calls, as service:Action; wildcards like s3:Get* work |
Resource | which things, by ARN: arn:aws:s3:::my-bucket/*; * means all |
Condition | optional extra rules: source IP, MFA present, tags, time |
Three rules decide every request: everything is denied by default, an Allow opens it, and an explicit
Deny beats any Allow.
Policies come in three kinds: AWS managed (written by AWS, like AmazonS3ReadOnlyAccess), customer
managed (yours, reusable, versioned) and inline (stuck to one identity; avoid them). The ones above are
identity-based: attached to a user, group or role. Resource-based policies, such as an S3 bucket policy,
sit on the resource and say who may use it.
Create it in the console
IAM → Policies → Create policy opens the policy editor. Switch from Visual to JSON and paste the policy:
Click Next, name it academy-s3-read, add a description and Create policy. Its page shows what it
allows, in plain words:
The same from the CLI:
$ aws iam create-policy --policy-name academy-s3-read --policy-document file://pol.json
aws iam create-policy --policy-name academy-s3-read --policy-document file://pol.jsonawsthe AWS command line
Users and groups
Give permissions to groups, then put users in them. When someone changes team, you move them between groups instead of editing policies person by person.
IAM → User groups → Create group: name it developers, search for academy-s3-read and tick it.
Now the user. IAM → Users → Create user, three steps:
From the CLI it's two commands:
$ aws iam create-user --user-name demo-dev $ aws iam add-user-to-group --user-name demo-dev --group-name developers
aws iam create-user --user-name demo-devawsthe AWS command line
MFA
A password alone is one phishing email away from gone. Multi-factor authentication adds a code from a device you hold. Open the user, Security credentials tab:
Assign MFA device asks for a name and the type:
Step 2 shows a QR code: scan it with an authenticator app and type two codes in a row. Do it first for the root user (the email you signed up with), then for every human user. The account summary tells you whether root has it:
$ aws iam get-account-summary --query 'SummaryMap.{Users:Users,MFADevices:MFADevices,AccountMFAEnabled:AccountMFAEnabled}' { "Users": 2, "MFADevices": 2, "AccountMFAEnabled": 1 }
aws iam get-account-summary --query 'SummaryMap.{Users:Users,MFADevices:MFADevices,AccountMFAEnabled:AccountMFAEnabled}'awsthe AWS command line--querypick fields from the JSON response
AccountMFAEnabled: 1 means the root user has MFA.
Access keys
An access key (an ID starting AKIA plus a secret) lets the CLI and SDKs act as a user. It never expires
on its own, so it's the credential that leaks most: committed to git, baked into an image, pasted in a chat.
- Prefer no keys: humans use IAM Identity Center (
aws sso login), servers and CI use roles. - If you must have one, one per person, never for root, stored by
aws configure(not in code), and rotated: create a new key, switch to it, deactivate the old one, delete it. - A key that leaked is a key to delete now, not later: bots scan public GitHub for
AKIAwithin minutes.
Roles and temporary credentials
A role has no password or keys. Something it trusts assumes it and gets credentials that expire, usually after an hour. Assume a role from the CLI:
$ aws sts assume-role --role-arn arn:aws:iam::111122223333:role/academy-console --role-session-name lesson-demo \ --query '{Arn:AssumedRoleUser.Arn,Expires:Credentials.Expiration}' { "Arn": "arn:aws:sts::111122223333:assumed-role/academy-console/lesson-demo", "Expires": "2026-10-17T09:09:02+00:00" }
aws sts assume-role --role-arn arn:aws:iam::111122223333:role/academy-console --role-session-name lesson-demo \awsthe AWS command line
Every role has two policies: the trust policy (who may assume it) and permissions policies (what it can do once assumed). The console's Create role starts with the trust side:
| Trusted entity | Real use |
|---|---|
| AWS service → EC2 | an instance profile: the app on the server gets credentials from the instance, no keys on disk (lesson 3) |
| AWS account | cross-account access: admins in a security account assume a role in production |
| Web identity | OIDC for CI/CD: GitHub Actions assumes a role for one job, no stored AWS keys (the CI/CD course) |
| SAML 2.0 | a company login (Okta, Entra ID) mapped to AWS roles |
Prove it: the policy simulator
Don't guess what a policy allows: test it. The policy simulator evaluates real policies without making the calls:
$ aws iam simulate-principal-policy --policy-source-arn arn:aws:iam::111122223333:user/demo-dev \ --action-names s3:GetObject s3:PutObject s3:DeleteBucket ec2:RunInstances \ --query 'EvaluationResults[].[EvalActionName,EvalDecision]' --output table -------------------------------------- | SimulatePrincipalPolicy | +-------------------+----------------+ | s3:GetObject | allowed | | s3:PutObject | implicitDeny | | s3:DeleteBucket | implicitDeny | | ec2:RunInstances | implicitDeny | +-------------------+----------------+
aws iam simulate-principal-policy --policy-source-arn arn:aws:iam::111122223333:user/demo-dev \awsthe AWS command line
demo-dev can read objects and nothing else. Least privilege is exactly this: start with nothing and allow
only what the job needs. IAM Access Analyzer helps keep it that way: it finds resources shared outside the
account and permissions nobody has used for months.
Guard rails for bigger teams
IAM Identity Center is the human access portal
IAM users are a poor fit for a team: each account grows another password and another long-lived key. IAM Identity Center gives a person one sign-in portal, then assigns a permission set to a role in each account. The role supplies temporary credentials, so removing access means removing the assignment rather than hunting for keys. Use it for workforce access; keep IAM roles for workloads.
In the console, open IAM Identity Center → Settings and read the enable page and settings page. Do not press Enable. A read-only CLI check, when an organization already has an instance, is:
$ aws sso-admin list-instances --region ap-south-1 \ --query 'Instances[].{Status:Status,IdentityStore:IdentityStoreId}' --output table
aws sso-admin list-instances --region ap-south-1 \awsthe AWS command line--regionwhich Region
Permission sets are reusable policy bundles. Assign one to a group and account, then let the person use the access portal for temporary credentials.
ABAC uses tags as a permission condition
Attribute-based access control compares a principal tag with a resource tag. It scales better than one policy per team, but only if tag changes are restricted and audited:
{"Effect":"Allow","Action":"ec2:StopInstances","Resource":"*","Condition":{"StringEquals":{"ec2:ResourceTag/Project":"${aws:PrincipalTag/Project}"}}}
Use fixed resource ARNs for a small, high-risk set where the boundary must be obvious.
- IAM Identity Center (SSO): one login for people across all your accounts, with temporary credentials instead of IAM users. AWS recommends it for human access.
- AWS Organizations: many accounts (dev, staging, prod, security) under one bill, so a mistake in dev can't touch prod.
- Service control policies (SCPs): an organisation-wide ceiling: even an admin in a member account can't do what an SCP denies, such as leaving approved Regions or turning off CloudTrail.
- Permission boundaries: a ceiling on one user or role, so you can let a team create roles without letting them create something more powerful than themselves.
A least-privilege developer
In your own account, create the academy-s3-read policy, a developers group with it attached and a demo-dev user in the group, following the screenshots. Prove with the policy simulator that demo-dev can read S3 objects but not write them or start servers, then delete everything you created.
A simulator table with allowed for s3:GetObject and implicitDeny for the rest, then no demo-dev, developers or academy-s3-read leftReveal one safe solution
$ printf '%s\n' '{"Version":"2012-10-17","Statement":[{"Sid":"ReadOnlyS3","Effect":"Allow","Action":["s3:ListAllMyBuckets","s3:ListBucket","s3:GetObject"],"Resource":"*"}]}' > pol.json $ ACC=$(aws sts get-caller-identity --query Account --output text) $ aws iam create-policy --policy-name academy-s3-read --policy-document file://pol.json $ aws iam create-group --group-name developers $ aws iam attach-group-policy --group-name developers --policy-arn arn:aws:iam::$ACC:policy/academy-s3-read $ aws iam create-user --user-name demo-dev $ aws iam add-user-to-group --user-name demo-dev --group-name developers $ aws iam simulate-principal-policy --policy-source-arn arn:aws:iam::$ACC:user/demo-dev \ --action-names s3:GetObject s3:PutObject ec2:RunInstances \ --query 'EvaluationResults[].[EvalActionName,EvalDecision]' --output text s3:GetObject allowed s3:PutObject implicitDeny ec2:RunInstances implicitDeny $ aws iam remove-user-from-group --user-name demo-dev --group-name developers $ aws iam delete-user --user-name demo-dev $ aws iam detach-group-policy --group-name developers --policy-arn arn:aws:iam::$ACC:policy/academy-s3-read $ aws iam delete-group --group-name developers $ aws iam delete-policy --policy-arn arn:aws:iam::$ACC:policy/academy-s3-read
printf '%s\n' '{"Version":"2012-10-17","Statement":[{"Sid":"ReadOnlyS3","Effect":"Allow","Action":["s3:ListAllMyBuckets","s3:ListBucket","s3:GetObject"],"Resource":"*"}]}' > pol.json>write output to a file, replacing it
aws iam create-policy --policy-name academy-s3-read --policy-document file://pol.jsonawsthe AWS command line
IAM is free, so this costs nothing. The order of the clean-up matters: a group with members, or a policy still attached, can't be deleted.
Try it yourself
0 of 6 steps doneRun each task in your own account or terminal, then tick it off.
Quick check
A new developer needs read access to S3. What is the best way to give it?
Official reading for this lesson
Or get every quiz answer right and it completes itself.