Create least-privilege IAM users
Learn to create least-privilege IAM users with this hands-on Cloud security essentials tutorial. Reduce IAM blast radius, troubleshoot edge cases, and prepare for the next lesson.
Focus: create least-privilege iam users
You've probably heard the horror stories: a leaked access key in a public GitHub repo, a misconfigured S3 bucket, a developer who had admin rights 'just in case.' Each of these is a direct result of violating the principle of least privilege. When you create IAM users with overly broad permissions, you're not just giving them access — you're expanding your blast radius, making every credential a potential entry point for attackers. This lesson is your practical guide to creating least-privilege IAM users in AWS, step by step, so you can dramatically reduce the damage a compromised credential can do.
The problem this lesson solves
Imagine you're the admin of a busy AWS account. Developers join, leave, and change roles. To keep things moving, you hand out AdministratorAccess or a broad * policy to anyone who asks. It's convenient, but it's a security disaster waiting to happen.
The core issue is credential compromise. If an attacker gets their hands on a user's access key — through a phishing email, a leaked .env file, or an exposed CI/CD pipeline — they inherit every permission that user has. With full admin rights, they can spin up expensive EC2 instances to mine cryptocurrency, exfiltrate your entire database, or delete your backups. Your only defense is to ensure that each user has the minimum permissions needed for their specific job. This is the principle of least privilege, and creating least-privilege IAM users is how you put it into practice.
When you embrace least privilege, you reduce your blast radius: the potential damage an attacker can cause with a single compromised credential. A developer who only needs to read from a specific S3 bucket can't touch your EC2 instances or RDS databases. A CI/CD pipeline that only deploys to a staging environment can't take down production. This lesson gives you the tools and methodology to achieve that, without slowing down your team.
Core concept / mental model
Think of IAM policies as a visitor's badge for your cloud infrastructure. An administrator badge opens every door in the building; a visitor badge only opens the front lobby and the elevator to the specific floor they need. In AWS, you create IAM users for people or applications, and you attach policies that define which actions they can perform on which resources. Least privilege is about designing that badge with the fewest doors possible.
Key terms
- IAM user — A permanent identity for a person or application that needs access to AWS. It has long-term credentials: a password and/or access keys.
- Policy — A JSON document that defines permissions. It states which actions (
s3:GetObject,ec2:StartInstances) are allowed or denied on which resources (e.g., a specific S3 bucket ARN). - Managed policy — A standalone policy that you can attach to multiple users, groups, or roles. AWS provides many pre-built ones (e.g.,
AmazonS3ReadOnlyAccess), and you can create your own customer managed policies. - Inline policy — A policy embedded directly into a single user, group, or role. Useful for one-off exceptions, but harder to manage at scale.
A mental model: permissions as a matrix
Imagine a spreadsheet where rows are IAM users and columns are AWS actions/resources. Each cell is "allowed" or "denied." Your goal is to have as few an 'allowed' cells as possible, yet still let each user do their job. Over time, as needs change, you should audit and shrink those allowed cells.
Pro tip: Always start with a deny-by-default mindset. IAM denies everything unless you explicitly allow it. Write policies that are as tight as possible — use specific ARNs, specific action names, and conditions (like IP ranges or MFA) to narrow access even further.
How it works step by step
Here's the mental workflow you'll follow every time you need to create a least-privilege IAM user:
-
Identify the job role. What tasks does this user need to perform? A data analyst needs
s3:GetObjecton a specific bucket, notec2:*. A CI/CD pipeline needss3:PutObjectto an artifacts bucket andlambda:UpdateFunctionCodeon one function. -
List the minimum actions and resources. Write down the exact API calls (actions) they need. For each, specify the resource ARN (e.g.,
arn:aws:s3:::my-data-bucket/*). -
Write the policy JSON. Turn that list into a policy document. Use
Effect: Allow,Actionwith the exact action names, andResourcewith the ARNs you listed. -
Create the IAM user. Use the AWS Console, CLI, or Infrastructure as Code (CloudFormation, Terraform) to create the user. Attach the policy you wrote, instead of using a broad managed policy.
-
Assign credentials. Set a password (if they need the console) or create an access key (if they use the CLI/SDK). Store and download the secret key immediately — you can't see it again.
-
Test the permissions. Log in as that user and attempt to do the allowed actions, and also try a denied action to confirm they're blocked. This verifies your policy is correct.
-
Audit and refine. Periodically review users and their policies. Remove unused policies, and tighten any that are too broad.
Hands-on walkthrough
Let's put that into practice. We'll create a least-privilege IAM user for a data analyst who needs to read (and only read) from an S3 bucket named company-reports. We'll do it using the AWS CLI for a repeatable, scriptable approach.
Step 1: Write the policy JSON
Create a file called analyst-policy.json with the following content:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::company-reports",
"arn:aws:s3:::company-reports/*"
]
}
]
}
Note that ListBucket requires the bucket ARN without a /*, while GetObject needs the ARN with /* to refer to objects inside. This is a common gotcha — more on that in the troubleshooting section.
Step 2: Create the policy and user
Use the AWS CLI (make sure you have aws configure set up with admin credentials):
# Create the customer managed policy
POLICY_ARN=$(aws iam create-policy \
--policy-name company-reports-readonly \
--policy-document file://analyst-policy.json \
--query 'Policy.Arn' --output text)
# Create the IAM user
aws iam create-user --user-name analyst-jane
# Attach the policy to the user
aws iam attach-user-policy --user-name analyst-jane --policy-arn $POLICY_ARN
echo "Policy ARN: $POLICY_ARN"
echo "User created and policy attached."
Expected output:
Policy ARN: arn:aws:iam::123456789012:policy/company-reports-readonly
User created and policy attached.
Step 3: Create credentials and test
Now generate an access key for the user, and test the permissions:
# Create access key
aws iam create-access-key --user-name analyst-jane
# Output includes AccessKeyId and SecretAccessKey
# Test as the new user: configure a profile (use the keys in the prompt)
aws configure --profile analyst-jane
# List the bucket (should succeed)
aws s3 ls s3://company-reports --profile analyst-jane
# Try to download an object (should succeed)
aws s3 cp s3://company-reports/sales.csv . --profile analyst-jane
# Try to upload an object (should FAIL - no permission)
aws s3 cp sales.csv s3://company-reports/ --profile analyst-jane
Expected output: The first two commands succeed; the third command fails with an AccessDenied error. That failure proves least privilege is working.
Pro tip: Always test both ways. If the denied action succeeds, your policy is too broad and you need to tighten it.
Compare options / when to choose what
When creating least-privilege IAM users, you have several options for managing policies and permissions. Here's how they compare:
| Approach | Pros | Cons | Best for |
|---|---|---|---|
AWS managed policies (e.g., AmazonS3ReadOnlyAccess) |
Quick, AWS-maintained, well-tested | Often too broad (apply to all buckets), may grant more than needed | Starting point, generic roles, or when you need a broad common baseline |
| Customer managed policies | Fully customize, can scope to specific resources, reusable | Need to write and maintain JSON, risk of misconfiguration | Following least privilege precisely, specific job functions |
| Inline policies | Tied to entity, good for one-off exceptions | Hard to audit, no reuse, easy to forget | Special one-off permissions that don't belong in a shared policy |
| IAM Groups & Roles | Easier management, dynamic permissions per role | Requires extra setup, adds complexity | Teams with well-defined roles, applications needing temporary credentials |
When to choose what?
- Use AWS managed policies as a starting point, but always check if they are scoped too widely. For example,
AmazonS3ReadOnlyAccessgrants read access to all S3 buckets in your account — almost always too broad. If the user only needs one bucket, write a custom policy. - Use customer managed policies when you need to enforce least privilege across multiple users. For instance, a
data-analystpolicy that grants read-only oncompany-reportscan be attached to all analysts. - Use inline policies for temporary or exceptional access, such as a one-off grant to a specific user to debug a production issue. But be aware: inline policies are hard to track, so document them.
- Prefer IAM roles over users for applications or services, because roles give temporary credentials and avoid long-term keys. But for human users who need the console or stable CLI access, users are fine — just follow least privilege.
Troubleshooting & edge cases
Creating least-privilege policies is error-prone. Here are the most common mistakes and how to fix them.
1. AccessDenied when you least expect it
- Issue: The user can't list the bucket even though you allowed
s3:ListBucket. - Fix: Check if you used the correct resource ARN.
s3:ListBucketrequires the bucket ARN (arn:aws:s3:::bucket-name) without/, whiles3:GetObjectrequires the ARN with/to cover objects. Use both in the resource array as shown in our example.
2. Too broad — policy grants access to all buckets
- Issue: You used
"Resource": "*"or an AWS managed policy likeAmazonS3ReadOnlyAccess. This is a classic least-privilege violation. - Fix: Always specify the exact bucket ARN(s) and object ARNs. Use conditions (like
aws:ResourceTag) to further restrict.
3. Forgetting to test denied actions
- Issue: You only tested the happy path. A denied action that actually succeeds indicates a policy gap.
- Fix: Always test at least one action you expect to be denied, and verify you get
AccessDenied. If you don't, narrow the policy.
4. Access key visible again?
- Issue: AWS shows you the secret key only once at creation. If you lose it, you must delete and recreate the access key.
- Fix: Save the key securely (e.g., in a password manager) the moment you create it. Consider using roles with temporary credentials for applications to avoid long-term keys entirely.
5. Policy size limits
- Issue: Huge JSON policies exceed AWS's size limits (2KB for user inline policies, 5KB for managed policy documents).
- Fix: Keep policies focused. If you need many permissions, split into multiple managed policies and attach them as needed. Use conditions to reduce repetition.
6. Confusing Action syntax
- Issue: You wrote
"s3:*"to allow all S3 actions, and now the user can delete objects and configure lifecycle rules. - Fix: Be explicit. List each action your user needs, like
s3:GetObject,s3:PutObject,s3:ListBucket. This is verbose but honest.
What you learned & what's next
You now know how to create least-privilege IAM users in AWS. You can start from a job role, write a precise policy, attach it to a user, and verify that both allowed and denied actions behave correctly. You've learned to compare AWS managed policies, customer managed policies, inline policies, and roles, and you can choose the right tool for the job. You've also been equipped to troubleshoot common IAM access errors — from missing resource ARNs to overly permissive actions.
You've met both learning objectives: you can explain the core idea of least privilege, and you've completed a practical exercise of creating a least-privilege IAM user.
Your next step in the Cloud security essentials path is learning how to reduce IAM blast radius further — using IAM roles, permission boundaries, and service control policies (SCPs) at the organization level. That lesson will show you how to enforce least privilege across your entire AWS organization, not just individual users.
Quick recap before you go
- Least privilege = minimum permissions needed, reducing blast radius.
- Write explicit policies with specific resources, not
*. - Always test both allowed and denied actions.
- Prefer customer managed policies over broad AWS managed ones.
- Use roles with temporary credentials for applications whenever possible.
Now, go create — and least-privilege — your IAM users!
Practice recap
As a follow-up exercise, create a least-privilege IAM user for an EC2 instance admin who only needs to start and stop instances with a specific tag. Write the policy JSON from scratch, attach it, and test that the user can start/stop those instances but cannot terminate them or access other resources. This will solidify your understanding of resource-level conditions and scoped actions.
Common mistakes
- Using AWS managed policies like
AmazonS3ReadOnlyAccesswhich grant access to all S3 buckets, not just the one you need. - Forgetting to include the correct ARN format:
s3:ListBucketneeds the bucket ARN without/, whiles3:GetObjectneeds the ARN with/. - Testing only the allowed actions and never a denied action — a policy that accidentally grants more than intended goes unnoticed.
- Storing secret access keys in plain text or losing them after creation — AWS only shows them once, so you must securely store them immediately.
- Writing policies with wildcard actions like
s3:or resourceto save time, which defeats the purpose of least privilege.
Variations
- Use IAM roles instead of users for applications and services — roles provide temporary credentials via AWS STS, eliminating long-term access keys.
- Employ Infrastructure as Code tools like Terraform or AWS CloudFormation to define IAM policies and users reproducibly — making least privilege auditable and reviewable.
- Leverage IAM permission boundaries to set the maximum permissions a user can have, even if a policy grants more.
Real-world use cases
- A data analyst needs read-only access to a single S3 bucket for reporting, so you create a user with a customer managed policy that restricts
s3:GetObjectands3:ListBucketto that bucket only. - A CI/CD pipeline deploys a Lambda function to a staging environment; you create a user with a policy that allows
lambda:UpdateFunctionCodeonly on the specific function ARN, and denies all other actions. - An external auditor requires temporary access to your CloudTrail logs in a separate bucket — you create a user with time-limited credentials and a scoped policy that allows read-only on that log bucket, then rotate the keys after the audit.
Key takeaways
- Least privilege means granting only the minimum permissions needed to perform a job, directly reducing your blast radius.
- Write policies with explicit action names and resource ARNs, never using wildcards for actions or resources unless absolutely necessary.
- Always test both allowed and denied actions to verify your policy is correctly scoped; expected failures are a sign your policy works.
- Compare options: customer managed policies give fine-grained control, AWS managed policies are often too broad, and inline policies are for one-off cases.
- Prefer IAM roles with temporary credentials over long-term users for applications, to avoid key management and rotation issues.
- Troubleshoot common issues like ARN formatting and policy size limits before they become security incidents.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.