IAM Policies are written in JSON and every field has a specific meaning. Understanding the policy structure is essential for writing correct permissions and debugging access issues.
{
"Version": "2012-10-17", // Always use this date - it is a policy language version
"Statement": [ // Array of permission rules
{
"Sid": "AllowS3Read", // Optional: human-readable identifier
"Effect": "Allow", // Allow or Deny
"Action": [ // What actions are permitted
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [ // Which resources does this apply to?
"arn:aws:s3:::my-bucket", // The bucket itself (for ListBucket)
"arn:aws:s3:::my-bucket/*" // All objects inside (for GetObject)
],
"Condition": { // Optional: additional constraints
"IpAddress": {
"aws:SourceIp": "203.0.113.0/24" // Only from this IP range
}
}
}
]
}
| Mistake | Problem | Correct Approach |
|---|---|---|
| s3:* on * | Grants ALL S3 actions on ALL buckets — too broad | Specify exact actions and bucket ARN |
| Missing /* | s3:GetObject on bucket ARN won't work — needs bucket/* for objects | Add /* for object-level permissions |
| No Condition block | Access allowed from any IP, any time | Add IP/MFA conditions for sensitive actions |
| Deny before Allow | Wrong order thinking — AWS evaluates ALL statements | Order doesn't matter — explicit Deny always wins |
Request comes in --> IAM evaluates all applicable policies
1. Is there an explicit DENY anywhere? --> DENY (stop)
2. Is there an explicit ALLOW? --> ALLOW
3. Default: IMPLICIT DENY
Note: SCPs, Permission Boundaries, and Resource Policies
are also evaluated — ALL must allow for access to succeed