AWS IAM & Least Privilege
Write a policy that grants exactly one action, prove what it blocks, and swap a long-lived key for a role.
- Time
- 50 min
- Level
- Beginner
- Objectives
- 4 objectives
- Cost
- Free
Where this fits in the platform
Already built
—
This lab adds
- An IAM identity scoped to what it actually needs
Before you start
You will need
- AWS CLI v2, configured
- An AWS account
You will be able to
- Read and write an IAM policy document
- Test a permission before shipping it, with the policy simulator
- Explain the difference between a trust policy and a permissions policy
The scenario#
The application has an access key with AdministratorAccess because that made it work. Everybody knows it is wrong; nobody knows what it actually needs.
This lab replaces it with a policy you can defend, and a role instead of a key.
The Terraform labs build IAM as code. This one works in the CLI and console deliberately — when a permission fails at 3am you will be reading the console, and a resource you have only ever seen through HCL is one you cannot debug.
Hands-on environment
Run it on AWS
This lab builds real cloud infrastructure, so it needs your own AWS account. Follow the cost and cleanup notes above — the resources are yours, and so is the bill.
Anything you tick here is your own record. EgyKode cannot see inside that terminal, so the success criteria stay self-assessed even when the environment checks your work for you.
The anatomy of a policy
Step 1 of 6
What you are proving: You can explain why ListBucket and GetObject need different resource ARNs
Marking this settles success criterion 2.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadOneBucket",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::egykode-lab-config/*"
},
{
"Sid": "ListThatBucket",
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::egykode-lab-config"
}
]
}Two statements, because the two actions act on different things.
ListBucket operates on the bucket; GetObject operates on objects inside
it. Granting only one produces the most common IAM confusion in existence: a
policy that "clearly allows S3" and returns AccessDenied.
What you are proving: You can attach a policy to a principal and know which of the two you are changing
This step settles no success criterion on its own.
aws iam create-user --user-name lab-reader
aws iam create-policy --policy-name lab-read-config \
--policy-document file://policy.json
aws iam attach-user-policy --user-name lab-reader \
--policy-arn arn:aws:iam::<account>:policy/lab-read-configWhat you are proving: You can prove what a policy allows and what it refuses, without waiting to be surprised
Marking this settles success criterion 1.
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::<account>:user/lab-reader \
--action-names s3:GetObject s3:DeleteObject s3:ListAllMyBuckets \
--resource-arns "arn:aws:s3:::egykode-lab-config/settings.yaml" \
--query 'EvaluationResults[].[EvalActionName,EvalDecision]' --output tableallowed for GetObject, implicitDeny for the others. Testing the denials
matters as much as the grants — a policy that works is not the same as a
policy that is narrow.
What you are proving: You can assume a role and receive credentials that expire, instead of carrying a key
Marking this settles success criterion 3.
A user carries long-lived credentials. A role is assumed and issues credentials that expire in an hour.
cat > trust.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::<account>:user/lab-reader" },
"Action": "sts:AssumeRole"
}]
}
EOF
aws iam create-role --role-name lab-reader-role \
--assume-role-policy-document file://trust.json
aws sts assume-role \
--role-arn arn:aws:iam::<account>:role/lab-reader-role \
--role-session-name demo \
--query 'Credentials.[AccessKeyId,Expiration]' --output tableTwo different policies, and confusing them is the classic mistake:
| Policy | Answers | Failure looks like |
|---|---|---|
| Trust policy | Who may become this role? | AccessDenied on sts:AssumeRole |
| Permissions policy | What may the role then do? | AccessDenied on s3:GetObject |
Read which action the error names, and you know which document to open.
What you are proving: You can say what an explicit Deny does to an Allow, from any policy
Marking this settles success criterion 4.
{ "Effect": "Deny", "Action": "s3:DeleteObject", "Resource": "*" }An explicit Deny overrides every Allow, from any policy, including an
administrator's. That is how guardrails and SCPs work — you cannot grant your
way past one.
What you are proving: You can check which principal you actually are before blaming the policy
This step settles no success criterion on its own.
aws sts get-caller-identityRun this first, always. The most common cause of "the policy is not working" is that you are not the principal you assumed you were — a profile, an assumed role, or an instance role is in play.
AccessDenied on a policy that clearly allows it
Check the resource ARN. Bucket-level and object-level actions need arn:...:bucket and arn:...:bucket/* respectively.
sts assume-role denied
That is the trust policy, not the permissions policy. The principal must be listed in the role's trust document.
The simulator says allowed but the real call fails
Something else denies it — an SCP, a permission boundary, or a bucket policy. The simulator does not evaluate resource-based policies by default.
Cannot delete the user
Detach all policies and delete access keys and login profile first.
Clean up#
Run this even if you did not finish.
Destructive — This removes real resources. Check which environment you are in first.
aws iam detach-user-policy --user-name lab-reader --policy-arn <arn>
aws iam delete-policy --policy-arn <arn>
aws iam delete-user --user-name lab-reader
aws iam delete-role --role-name lab-reader-roleCost of this lab: Free — IAM users, roles and policies cost nothing.
Success criteria
0 of 4
The concept behind it
Next up
Lab 14 of 59 on the project path