Taking a Policy JSON Apart Line by Line
In one line
A policy is, in the end, a document that writes down four things — what (Action) on where (Resource), allowed or denied (Effect), under which conditions (Condition).
Why it was needed
A policy can be syntactically valid and still open more widely than intended. If you check only the allow effect and do not read the resource scope and conditions, a policy that passed review becomes a privilege escalation path. So you need to break a policy down by component and read dangerous combinations in a fixed order.
How it works
The minimal form
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadReportsOnly",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::acme-reports",
"arn:aws:s3:::acme-reports/*"
]
}
]
}
Version looks like a date but is the version of the policy language. Use 2012-10-17 as it is. If you change it, features such as variable substitution disappear.
Why Resource has two lines
This is the place where beginners go wrong most often.
arn:aws:s3:::acme-reports— the bucket itself.ListBucketneeds this.arn:aws:s3:::acme-reports/*— the objects inside the bucket.GetObjectneeds this.
They are different resources. If you write only one, you get "the list shows but the file won't open," or the reverse.
The structure of an ARN
arn:aws:s3:::acme-reports/reports/2026/*
│ │ │ │ │ └ 자원 경로
│ │ │ │ └ 계정 ID (S3 는 비움)
│ │ │ └ 리전 (S3 는 전역이라 비움)
│ │ └ 서비스
│ └ 파티션 (aws / aws-cn / aws-us-gov)
└ 고정
The blank fields are not a typo; they mean that service is global.
Wildcards — the danger differs by position
| Expression | Meaning | Risk |
|---|---|---|
"Action": "s3:Get*" |
All read-type actions | Low |
"Action": "s3:*" |
All S3 actions (including delete) | High |
"Action": "*" |
All actions of all services | Administrator |
"Resource": "*" |
All resources | Depends on context |
If "Action": "*" and "Resource": "*" are together in one statement, that is administrator permission. It is the combination to look for first in a policy review.
Condition — where it is truly narrowed
It is the most powerful means of narrowing permissions, yet the least used.
"Condition": {
"IpAddress": { "aws:SourceIp": ["203.0.113.0/24"] },
"Bool": { "aws:SecureTransport": "true" },
"StringEquals": { "aws:PrincipalTag/team": "platform" },
"DateLessThan": { "aws:CurrentTime": "2026-12-31T00:00:00Z" }
}
- Only from the office IP
- Only over HTTPS (this one line blocks plaintext access)
- Only principals with a particular team tag
- Only until an expiry date — especially useful for temporary access
Multiple keys within one Condition block are joined by AND, and multiple values for one key are OR. If you mix up this rule, you get a policy that is the exact opposite of what you intended.
NotAction is a trap
{ "Effect": "Allow", "NotAction": "iam:*", "Resource": "*" }
It reads as "allow everything except IAM," but in fact every new service that appears in the future is also allowed automatically. That is because it is a blacklist, not a whitelist. Always write permissions as an allow list.
The order of reading
When reviewing, this order is quick.
- Start with the
Effect: Denystatements — what is absolutely blocked - Does
Actioncontain a*? - Does
Resourcecontain a*? - Is there a
Condition? — if not, that permission works from anywhere - Is there a
NotAction/NotResource? — if so, be suspicious
What it looks like in the field
- The list shows but download fails →
Resourceis missing the/*. - The policy is correct but it is blocked → a Deny in a higher-level organization policy, or a permissions boundary.
- Attached with
s3:*and forgotten → even delete permission is open.
What to look at next
How to grant permissions without handing out long-term keys — roles and temporary credentials.