Lesson 1 of 9 · 25m
Reading an IAM policy like an attacker
Policy anatomy, the three evaluation rules that matter, the wildcards that leak, and how to simulate before you ship.
Most AWS incidents do not start with a zero-day. They start with a leaked access key and a policy that granted far more than anyone intended. Before you can write good IAM, you have to read it the way an attacker does: as a map of what a stolen credential can actually do.
Policy anatomy
Every IAM policy is a JSON document made of Statement objects. Each statement has four moving parts that matter:
Effect:AlloworDenyAction: the API calls, e.g.s3:GetObjectoriam:*Resource: the ARN(s) the actions apply toCondition: optional constraints that must hold true
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadAppLogs",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::acme-app-logs",
"arn:aws:s3:::acme-app-logs/*"
],
"Condition": {
"Bool": { "aws:SecureTransport": "true" }
}
}
]
}Two details in that small example trip people up constantly. ListBucket operates on the bucket ARN, GetObject on objects inside it — you need both ARNs, which is why the array has the bucket and bucket/*. And "Version": "2012-10-17" is a policy-language version stamp, not a date you should change to today.
Evaluation logic, the only three rules you need
- Everything is implicitly denied.
- An explicit
Allowoverrides the implicit deny. - An explicit
Denyoverrides every allow, always, no matter where it comes from — identity policy, resource policy, SCP, permission boundary.
An attacker reads a policy asking "what's the widest thing I can reach?" You should audit the same way. The clauses that make policies wide:
"Action": "s3:*"— every S3 API on whatever resources are listed. Use action families likes3:Get*sparingly too;Get*includesGetBucketPolicyandGetObjectTagging, not just reads of objects."Resource": "*"— sometimes legitimately required (actions likeiam:ListUsersorec2:DescribeInstancesdo not support resource-level scoping), but ons3:*oriam:*it is almost always a mistake.NotActionandNotResource— inverse matchers."NotAction": "iam:*"means "every action except IAM," which is what people reach for when they mean "a few S3 actions." This is the bug that ships.
Checking what a policy really allows
The console's policy summary hides subtle interactions. Use the simulator:
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/app-runner \
--action-names s3:DeleteObject iam:AttachRolePolicy \
--resource-arns arn:aws:s3:::acme-app-logsIt returns allowed/implicitDeny/explicitDeny per action and — critically — tells you which statement produced the decision.
Exercise
Write a policy that is supposed to grant read-only access to one bucket. Then add "Action": "s3:*" and "Resource": "*" as a second statement (simulating what a teammate might paste from a blog). Run simulate-principal-policy for s3:DeleteObject, s3:PutBucketPolicy, and s3:GetObject against an unrelated bucket ARN. Observe which calls the "read-only" role can now make — then fix it by scoping Resource and splitting s3:Get*/s3:List* from writes.
Field notes
- AWS managed
AdministratorAccessandPowerUserAccessare not the same thing; PowerUser still blocks IAM mutation. Neither belongs on a workload role. - Inline policy size limits (roles: 10,240 chars; users: 2,048; managed policies: 6,144) will force you into multiple statements — that is fine.
- A
Denyin an SCP or permission boundary is how you carve "never allowed" zones that even an account admin cannot escape. Remember them when a permission looks impossible on paper but works in practice — and when it looks possible but fails.