IAM is one of those AWS services that looks simple at first.
Create a user or role, attach a policy, give it access, and move on.
Then one day you deploy an application and see:
AccessDenied
And suddenly you're asking:
Which permission is missing?
Working with AWS has taught me that IAM isn't just about making something work.
It's about giving the application exactly the permissions it needs—and nothing more.
Here are some of the lessons I've learned.
Mistake 1: Using * just to get it working
One of the easiest ways to solve an AWS permission problem is to make the policy extremely broad.
For example:
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
Technically, this can make many permission errors disappear.
But it creates a much bigger security problem.
The application now has permission to perform actions it may never need.
For example, if a backend only needs to upload objects to one S3 bucket, it shouldn't automatically have permission to:
Delete buckets
Create IAM users
Modify EC2 instances
Read unrelated databases
Access every S3 bucket
The better approach is to scope permissions to the actual requirement.
For example:
{
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-app-uploads/*"
}
Now the role has a much smaller permission boundary.
The lesson is simple:
Don't use broad permissions as the permanent solution to a permission error.
Mistake 2: Confusing users and roles
Another important concept is understanding when to use an IAM user and when to use an IAM role.
For applications running on AWS, I prefer the application to assume a role rather than embedding long-term AWS credentials inside the application.
For example:
EC2
↓
IAM Role
↓
AWS Services
Instead of:
EC2
↓
AWS Access Key
↓
AWS Services
The role-based approach allows AWS services to obtain temporary credentials without storing permanent access keys in the application.
This is especially important for workloads running on services such as EC2, Lambda, and ECS.
Mistake 3: Giving the wrong resource access
Another common IAM problem is getting the action right but the resource wrong.
For example, you may allow:
"Action": "s3:GetObject"
but the resource doesn't match the actual object ARN.
A policy can look correct at first glance while still producing:
AccessDenied
That's why debugging IAM requires checking both:
Action
+
Resource
For S3, for example, bucket-level and object-level ARNs can be different depending on the operation.
Mistake 4: Forgetting that permissions can come from multiple places
IAM authorization isn't always as simple as looking at one policy.
When debugging access, I consider:
Identity-based policies
+
Resource-based policies
+
Permissions boundaries
+
Session policies
+
Service control policies
+
Resource configuration
This is one reason IAM errors can sometimes be confusing.
You might add an Allow policy and still receive AccessDenied.
An explicit Deny somewhere else can override an Allow.
So my debugging process starts with understanding where the permission decision is actually coming from.
How I approach an AccessDenied error
When an AWS operation fails, I don't immediately start adding more permissions.
I first identify:
1. Who is making the request?
Is it:
IAM User?
IAM Role?
EC2 Instance Role?
Lambda Execution Role?
ECS Task Role?
2. What action is being attempted?
For example:
s3:GetObject
s3:PutObject
dynamodb:GetItem
secretsmanager:GetSecretValue
3. Which resource is being accessed?
For example:
S3 bucket/object
DynamoDB table
Secrets Manager secret
4. Which policy should allow the operation?
Then I check the relevant policy instead of randomly adding permissions.
This makes debugging much more systematic.
Least privilege
The principle I try to follow is:
Give the application the minimum permissions required to perform its job.
Imagine an API that only needs to upload user profile images.
Instead of:
S3: *
I'd want something closer to:
s3:PutObject
↓
specific bucket
↓
specific path
The same principle applies to other AWS services.
Instead of:
DynamoDB: *
use the specific actions required by the application.
Instead of:
Secrets Manager: *
allow access only to the secrets the application actually needs.
This reduces the potential impact if credentials or an application component are compromised.
Tools that help
AWS provides several tools that make IAM troubleshooting easier.
IAM Policy Simulator
The IAM Policy Simulator can help test whether a policy allows or denies specific actions.
Instead of repeatedly deploying an application just to test permissions, you can reason about the policy directly.
IAM Access Analyzer
Access Analyzer can help identify overly broad access and unintended external access.
It's particularly useful when reviewing policies and trying to move toward least-privilege permissions.
CloudTrail
When debugging AWS activity, CloudTrail can provide valuable information about API calls.
It helps answer questions such as:
Who made the request?
What AWS API was called?
When did it happen?
What resource was involved?
That context can make an IAM problem much easier to understand.
A better IAM workflow
My preferred workflow looks like this:
Application needs AWS access
↓
Identify exact AWS action
↓
Identify exact resource
↓
Create minimum required policy
↓
Test
↓
Review permissions
↓
Monitor
Not:
AccessDenied
↓
Add AdministratorAccess
↓
Problem solved
The second approach may be faster for five minutes, but it's much harder to defend from a security perspective.
IAM checklist
Before considering an application role ready for production, I'd ask:
[ ] Does the application use an IAM role where appropriate?
[ ] Are long-term credentials avoided?
[ ] Are permissions limited to required actions?
[ ] Are resources scoped as narrowly as practical?
[ ] Have wildcard permissions been reviewed?
[ ] Have explicit denies been considered?
[ ] Have IAM Access Analyzer findings been reviewed?
[ ] Have policies been tested?
[ ] Are production and development permissions separated?
[ ] Are unused permissions removed?
What I learned
The biggest lesson is that IAM isn't something you should solve by trial and error forever.
When I see:
AccessDenied
I now try to understand the authorization path instead of immediately adding another policy.
The question isn't:
"How can I give this application more access?"
It's:
"What is the smallest amount of access this application actually needs?"
That mindset changes the way you design AWS infrastructure.
Final takeaway
IAM can feel frustrating when you're starting with AWS.
But once you stop treating permissions as random configuration and start thinking in terms of:
Principal
↓
Action
↓
Resource
↓
Policy
↓
Authorization
the system becomes much easier to reason about.
And one of the most important AWS security habits I've learned is:
Make it work with the minimum permissions—not maximum permissions.
Top comments (0)