AWS Builder Center

IAM Least Privilege on AWS: How I Stopped Giving Everything Admin Access

A practical, honest look at moving from broad admin policies to scoped IAM roles on AWS — with a simple before-and-after policy, a CloudTrail-based workflow, and a quick checklist I use before shipping.

yub 😎
When I first started building on AWS, I did what a lot of beginners do: if something failed with "AccessDenied", I just attached a broader policy. Sometimes that meant `AmazonS3FullAccess`. Sometimes, if I was in a hurry, it meant admin.
It worked — until it didn't feel right anymore.
One day I looked at a Lambda function that only needed to read one S3 prefix and realized it could technically delete almost everything in the account. That's when IAM least privilege stopped being a textbook term for me and became a habit I had to actually practice.
## What least privilege really means
Least privilege doesn't mean "make permissions as small as possible at all costs." For me, it means this: an identity should be able to do exactly its job, on exactly the resources it needs, for as long as needed — and nothing more.
In practice, that breaks down into three questions I ask before I write any policy:
1. **What actions does it really need?** Not "S3 access", but `s3:GetObject` and `s3:ListBucket` if it's a read-only job.
2. **On which resources?** A bucket is not the same as `my-bucket/user-uploads/*`. Specific ARNs are your friend.
3. **Under what conditions?** Same account? Specific prefix? Only over TLS? IAM conditions help a lot here.
If I can't answer those three, I'm not ready to deploy that role yet.
## A simple before and after
This is the kind of change that helped me the most. Instead of a broad policy like this:
```json
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}
```
I now write something closer to this for a Lambda that processes uploaded files:
```json
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-app-uploads/incoming/*"
}
```
It's not perfect, but it's a huge difference. That function can no longer list other buckets, delete objects it doesn't own, or touch data outside `incoming/`. If it ever gets misused or has a bug, the blast radius is small.
## How I tighten permissions without guessing
I used to guess what permissions I needed. I don't do that anymore.
My current workflow is pretty boring, but it works:
**Start in dev, observe, then shrink.** I deploy to a dev account first, let the workload run, and check CloudTrail to see what API calls it actually made. IAM Access Analyzer can also generate a policy based on that activity.
**Use roles, not long-lived users.** For Lambda, ECS, and EC2, I always use roles. No access keys sitting in .env files longer than they need to.
**Separate duties.** My deployment role can push code. My application role can read data. They are not the same role, and that separation has saved me from messy mistakes.
**Test the negative case.** I intentionally try an action the role should *not* be able to do. If I can't delete that S3 object, I know the policy is actually doing its job.
## The trade-off nobody talks about
Being too strict also has a cost. If every small change requires a policy update and a redeploy, you slow yourself down. I've broken my own pipelines by forgetting `s3:ListBucket` when `s3:GetObject` alone wasn't enough.
What helped me was treating IAM as part of the code review — not an afterthought. Small, scoped policies per service, versioned in Terraform/CDK, with a short comment explaining *why* that permission exists. Six months later, I thank myself for writing that comment.
Least privilege is not a one-time setup. As your app grows, you revisit it, just like you refactor code.
## My quick checklist before I ship
- [ ] Is this a role instead of a user with long-lived keys?
- [ ] Are actions limited to only what this workload calls?
- [ ] Are resource ARNs scoped to a bucket, table, or prefix?
- [ ] Did I check CloudTrail for unused permissions I can remove?
- [ ] Would I be okay if this role leaked for an hour? If not, it's too broad.
That last question is the one that changes my mind the fastest.
I'm still learning this part of AWS, and I definitely don't get it perfect the first time. But moving from "just make it work" to "make it work with only what it needs" has made my architectures cleaner and my sleep a bit better.
**How do you handle IAM in your projects — do you write policies by hand, generate them from CloudTrail, or use tools like Access Analyzer? I'd love to hear what works for you.**
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article