Every IAM permission is a JSON document. Once you can read one element by element, most AccessDenied puzzles become quick to solve. This note walks through the policy grammar, the ARN format that names resources, and the conditions that add context such as HTTPS, MFA or account. Which policy types exist and how they combine is in IAM policy types.
All examples use the placeholder account 123456789012 and bucket example-bucket.
Anatomy of a policy#
Start with the overall shape before the individual parts. A policy is an object with a Version and a list of Statements. Each statement allows or denies some actions on some resources, optionally under some conditions.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadReports",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::example-bucket/reports/*"
}
]
}
Read it as one sentence: "Allow s3:GetObject on objects under reports/ in example-bucket." The order of keys inside a statement doesn't matter.
The elements#
The example used only a few elements. The full set is small, and each element has one job. The table lists them in the order you usually write them.
| Element | Purpose | Notes |
|---|---|---|
Version |
Policy language version | Always "2012-10-17". Without it, policy variables don't work |
Id |
Optional label for the whole policy | Some services ask for it, e.g. a UUID |
Statement |
One or more permission rules | An array of objects |
Sid |
Optional label for one statement | Unique within the policy; helps reviews |
Effect |
"Allow" or "Deny" |
An explicit deny beats any allow |
Principal |
Who the statement applies to | Resource-based and trust policies only |
Action |
API actions, e.g. s3:GetObject |
Wildcards allowed: s3:Get* |
Resource |
ARNs the actions apply to | Required in identity policies; "*" means all |
Condition |
Extra tests on the request context | Optional; see below |
Identity-based policies have no Principal. The principal is whoever the policy is attached to.
The "Not" elements#
Three of those elements have negated twins. A statement can use one of each pair, never both.
| Element | Matches | Example use |
|---|---|---|
NotAction |
Every action except those listed | Deny everything except IAM self-service until MFA is used |
NotResource |
Every resource except those listed | Deny writes to all buckets except one |
NotPrincipal |
Every principal except those listed | Rare; AWS advises conditions instead |
Use them with care. "Effect": "Allow" with NotAction grants every other action in AWS, including ones that will launch next year. "Effect": "Deny" with NotAction is the safer pattern: it denies everything except a short list.
ARNs: naming resources#
Apart from "*", every Resource value is an ARN, so you need to read them fluently. An Amazon Resource Name (ARN) uniquely names a resource. The general shapes are:
arn:partition:service:region:account-id:resource-id
arn:partition:service:region:account-id:resource-type/resource-id
arn:partition:service:region:account-id:resource-type:resource-id
Each field has a fixed meaning.
| Field | Meaning | Example |
|---|---|---|
partition |
Group of Regions | aws, aws-cn, aws-us-gov |
service |
Service namespace | s3, ec2, iam |
region |
Region code | us-east-1 |
account-id |
12-digit owner account, no hyphens | 123456789012 |
| resource part | Type and name, ID or path | instance/i-0abc1234 |
Some services leave fields empty. S3 bucket ARNs have no Region or account, and IAM is global, so its ARNs have no Region:
arn:aws:s3:::example-bucket
arn:aws:s3:::example-bucket/reports/2026.csv
arn:aws:iam::123456789012:role/deploy
arn:aws:ec2:us-east-1:123456789012:instance/i-0abc1234
arn:aws:dynamodb:us-east-1:123456789012:table/Orders
Bucket-level actions such as s3:ListBucket act on the bucket ARN. Object-level actions such as s3:GetObject act on object ARNs (bucket/key). Mixing them up is the most common S3 policy bug.
Conditions#
Effect, Action and Resource say what is allowed. Conditions say when. The Condition block tests values from the request context: who is calling, from where, how, and on what. It has the shape operator: { key: value }.
"Condition": {
"Bool": { "aws:SecureTransport": "true" },
"StringEquals": { "aws:PrincipalAccount": "123456789012" }
}
Once a block has more than one test, it matters how they combine:
- Different operators or keys in one block are joined with AND. All must match.
- Several values for one key are joined with OR. Any one may match.
- If a key is missing from the request, most operators evaluate to false.
Common operators#
Each data type has its own operators. The type of the key decides which ones are valid.
| Type | Operators | Example |
|---|---|---|
| String | StringEquals, StringNotEquals, StringLike (with * and ?) |
Match a tag value |
| Numeric | NumericLessThan, NumericGreaterThanEquals, ... |
Limit s3:max-keys |
| Date | DateGreaterThan, DateLessThan |
Valid only before a date |
| Boolean | Bool |
Require HTTPS or MFA |
| IP address | IpAddress, NotIpAddress |
Allow only office CIDRs |
| ARN | ArnEquals, ArnLike |
Match a source ARN |
| Null | Null |
Check whether a key is present |
Add IfExists to an operator, such as StringEqualsIfExists, to pass when the key is absent.
One more wrinkle: for keys that hold several values (for example a list of tag keys), add a set qualifier:
ForAllValues:passes when every value in the request is in your list.ForAnyValue:passes when at least one value in the request is in your list.
Global and service-specific keys#
Operators are half of a condition; the key is the other half. Global keys start with aws: and work across services, though not every key is present in every request. Service keys start with the service prefix and work only for some of that service's actions.
| Key | What it tests |
|---|---|
aws:SecureTransport |
The request used TLS |
aws:MultiFactorAuthPresent |
The session was created with MFA |
aws:SourceIp |
The caller's public IP |
aws:CurrentTime |
The time of the request |
aws:PrincipalAccount |
The account of the caller |
aws:ResourceAccount |
The account that owns the resource |
aws:PrincipalOrgID |
The caller's AWS Organization |
aws:SourceAccount |
The account a service acts for (service-to-service calls) |
aws:PrincipalTag/key, aws:ResourceTag/key |
Tags on the caller or the resource |
s3:prefix |
The key prefix in a ListBucket request |
To limit who can call, use aws:PrincipalAccount or aws:PrincipalOrgID. To limit which resources can be touched, use aws:ResourceAccount. Keep aws:SourceAccount for resource policies that let an AWS service (such as SNS or CloudTrail) act on your resource. It stops the "confused deputy" problem, where another account tricks the service into using your resource.
Example: one folder in a bucket#
Here's how elements, ARNs and conditions fit together in one policy. The goal: a user may list and read only the folder/ prefix of example-bucket, over HTTPS.
s3:prefix exists only on s3:ListBucket, so it goes in the list statement on the bucket ARN. Reading is limited through the object ARN in Resource, not through a condition.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListOnlyFolder",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::example-bucket",
"Condition": {
"StringLike": { "s3:prefix": ["folder/", "folder/*"] },
"Bool": { "aws:SecureTransport": "true" }
}
},
{
"Sid": "ReadOnlyFolder",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example-bucket/folder/*",
"Condition": {
"Bool": { "aws:SecureTransport": "true" }
}
}
]
}
Here's how that policy handles a few requests:
| Request | Result |
|---|---|
List example-bucket with prefix folder/ |
Allowed |
Get example-bucket/folder/file1.txt |
Allowed |
Get example-bucket/other/file3.txt |
Denied (no allow matches) |
| Any request over plain HTTP | Denied (condition fails) |
Example: a trust policy for a service#
The folder example was an identity policy, so it had no Principal. A trust policy is a resource-based policy on a role, so it does have one. This one lets EC2 instances assume the role.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
Policy variables#
Both examples so far hard-code their values. Policy variables fill in values from the request at evaluation time. They let one policy serve many users. They need "Version": "2012-10-17".
This statement gives each IAM user their own home prefix in a shared bucket:
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::example-bucket/home/${aws:username}/*"
}
Tags work the same way. Matching aws:ResourceTag/team to ${aws:PrincipalTag/team} gives attribute-based access control (ABAC): anyone tagged team=payments can reach resources tagged team=payments, without a policy per team.
Reading the service authorization reference#
Everything above assumes you know which actions, ARNs and keys a service supports. Every service documents its actions, resource types and condition keys in the Service Authorization Reference. Check it whenever you write a policy for a service you haven't used before.
Actions table#
This table lists each action and how it can be scoped.
- Access level: List, Read, Write, Permissions management or Tagging. Permissions management actions such as
s3:PutBucketPolicydeserve extra review. - Resource types: which ARNs the action accepts. A required type has an asterisk. If the column is empty, the action doesn't support resource-level permissions, and you must use
"Resource": "*". - Condition keys: keys you can use with that action.
- Dependent actions: other permissions the action needs to succeed.
- Some actions are marked permission only. They control access but don't match a single API call.
Resource types and condition keys tables#
The resource types table gives the ARN format for each type, such as arn:aws:s3:::${BucketName} for a bucket. The condition keys table gives each key's data type, which decides the operators you can use.
If an action and a resource in one statement don't fit together, that pairing never matches. The statement then silently grants nothing.
Validate before you ship#
A policy can be valid JSON and still be wrong. Check it before it reaches production.
- IAM Access Analyzer policy validation flags syntax errors, invalid actions and risky patterns as you edit.
- IAM policy simulator tests a policy against sample requests.
- Access Analyzer also reports resources shared outside your account or organization.
Key takeaways#
- A statement is Effect + Action + Resource, optionally with Principal and Condition.
- Bucket actions take the bucket ARN; object actions take
bucket/keyARNs. - Conditions join different keys with AND and multiple values with OR.
s3:prefixapplies tos3:ListBucket. Limit object reads through theResourceARN.- Limit callers with
aws:PrincipalAccountoraws:PrincipalOrgID, resources withaws:ResourceAccount. - Check the Service Authorization Reference for valid actions, resources and keys, then validate.