AWS IAM Policy Structure Explained: JSON Elements and Examples
Every permission in AWS is defined by a JSON document called a policy. Policies control who can do what to which resources, and under what conditions. This page walks through every field in that JSON document so you can read, write, and debug any IAM policy you encounter.
If you have ever stared at an AccessDenied error and had no idea which part
of which policy caused it, this is the page that fixes that.
Simple explanation
An IAM policy is a set of instructions you hand to AWS that says: “For this identity (or this resource), allow or deny these specific actions on these specific resources, and only when these conditions are met.”
Analogy
Think of a policy like a security badge for a building. The badge does not say “you can go anywhere.” It says “you can enter Floor 3, Rooms 301 through 305, during business hours.” The JSON fields in a policy (Effect, Action, Resource, Condition) are the rules printed on that badge. A badge with no restrictions is a master key, and you would not hand a master key to a delivery driver.
Why this matters
AWS permissions errors are notoriously confusing. An AccessDenied error
might come from a missing action, a wrong resource ARN, an unmet condition, or an
explicit deny overriding an allow somewhere else. You cannot
debug AccessDenied errors
without being able to read a policy fluently.
Writing policies matters just as much. Vague wildcard policies lead to over-permissioning and expand your blast radius. Too-narrow policies cause unexpected failures in production. Applying the principle of least privilege requires writing policies that are precise. That starts with understanding the structure.
This page builds on the concepts in IAM roles explained and managed vs customer managed policies. If you have not read those, start there.
Using “Action”: ”*” or “Resource”: ”*” in production policies is one of the most common security findings in AWS accounts. Wildcard permissions feel convenient during development, but they turn every compromised credential into a master key. This page teaches you how to avoid that.
How IAM policy structure works
The top-level JSON shape
Every IAM policy document has the same minimal skeleton:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}Two top-level fields. Version sets the policy language version.
Statement is an array containing one or more permission rules. Everything
else lives inside individual statement objects.
What each field means
| Field | Required? | Where valid | What it does | Beginner note |
|---|---|---|---|---|
Version | Recommended | Top level | Sets the policy language grammar. Always “2012-10-17”. | This is NOT a managed policy version (v1, v2). See the warning below. |
Statement | Yes | Top level | Array of one or more permission rules. | A single policy can contain many statements with different effects. |
Sid | No | Inside a statement | Statement ID, a human-readable label. | Use descriptive Sids like “AllowS3Read”. Makes policies readable months later. |
Effect | Yes | Inside a statement | ”Allow” or “Deny”. Explicit Deny always wins. | The default is implicit deny. You only need explicit Deny to override an Allow from another policy. |
Action | Yes (or NotAction) | Inside a statement | Lists the API operations this statement applies to. Format: service:ApiName. | Name specific actions. Avoid ”*” in production. |
NotAction | No | Inside a statement (instead of Action) | Matches every action except the ones listed. | Dangerous. Automatically includes new services AWS adds in the future. |
Resource | Yes (or NotResource) | Inside a statement | The ARNs this statement applies to. | Some actions (like ec2:DescribeInstances) require ”*” because they do not support resource-level scoping. |
NotResource | No | Inside a statement (instead of Resource) | Matches every resource except the ones listed. | Rarely needed. Auto-includes any new resources created later. |
Condition | No | Inside a statement | Extra requirements (MFA, IP, region, tags) that must be true for the statement to apply. | See IAM policy conditions explained for the full reference. |
Principal | Only in resource-based and trust policies | Inside a statement | Specifies who (which account, user, role, or service) the statement applies to. | Never put Principal in an identity-based policy. It causes a validation error. |
The Version field in a policy JSON document (“2012-10-17”) is the policy language version. It is not the same as a managed policy version (v1, v2, v3). Managed policy versions track revisions of a customer managed policy over time. IAM stores up to 5 versions so you can roll back. These are completely separate concepts. Confusing them is one of the most common beginner mistakes.
Identity-based vs resource-based vs trust policies
AWS uses three main types of policies. The biggest structural difference is the
Principal field: whether it appears, and what it means.
- Identity-based policies are attached to an IAM user, group, or
role. The principal is whoever
the policy is attached to, so no
Principalfield is needed or allowed. - Resource-based policies are attached to a resource (S3 bucket, KMS key,
SQS queue). They must include a
Principalfield to say who is granted access to the resource. See S3 IAM vs bucket policies for a practical comparison. - Trust policies are attached to an IAM role and define who can
assume that role. They include
a
Principalfield naming the trusted service, account, or user.
| Policy type | Attached to | Controls what | Includes Principal? | Common examples |
|---|---|---|---|---|
| Identity-based | IAM user, group, or role | What the identity can do | No | Customer managed policies, AWS managed policies, inline policies |
| Resource-based | AWS resource (S3, KMS, SQS, Lambda) | Who can access the resource | Yes | S3 bucket policies, KMS key policies, SQS queue policies |
| Trust policy | IAM role | Who can assume the role | Yes | EC2 service trust, Lambda service trust, cross-account trust |
Analogy
Identity-based policies are like a list of places you are allowed to visit. Resource-based policies are like a guest list posted on the door of a specific room. Trust policies are like a sign that says “only these people may pick up the key to this room.” In all three cases, permission must be granted before entry. The difference is who writes the rules and where they are posted.
If you put a Principal field in an identity-based policy, the policy fails validation. If you omit Principal from a resource-based policy, AWS does not know who the policy applies to. Getting this wrong is a common source of confusing errors.
How to read any IAM policy step by step
Analogy
Reading an IAM policy is like reading a short legal contract. You check who it applies to, what it allows or forbids, which specific things it covers, and whether there are any special conditions. Once you know the pattern, every policy reads the same way.
When you see an unfamiliar policy, or you are debugging an AccessDenied error, work through each statement with this checklist:
- Effect: Is this statement granting or blocking access?
- Action / NotAction: Which API calls does it cover? If it uses NotAction, what is excluded and what massive set is included?
- Resource / NotResource: Is this scoped to a specific ARN, or is it a wildcard? If NotResource, which resources are excluded?
- Condition: Are there extra requirements like MFA, region, IP, or tags? See IAM policy conditions explained for how to interpret these.
- Principal (if present): Who does this statement apply to? Is it a service, a specific role, or an entire account?
- Other policies: What other policies apply to this same principal or resource? An Allow here can be overridden by a Deny elsewhere. Check Service Control Policies, permissions boundaries, and session policies too.
The IAM evaluation order: (1) Explicit Deny wins over everything. (2) Explicit Allow grants access. (3) If neither applies, the default is implicit Deny. A request must be explicitly allowed and not explicitly denied by any applicable policy.
For hands-on testing, the IAM Policy Simulator in the IAM console lets you pick a principal, action, and resource and shows you exactly which policy allowed or denied the request.
Read policies from the inside out. Start with the Action and Resource to understand what the statement covers, then check the Effect to see whether it is granting or blocking. Conditions and Principal add context, but Action and Resource tell you the scope.
How to write a safe policy
Analogy
Writing a policy is like writing a recipe. You would not write “use some ingredients, cook for a while.” You list the exact ingredients, the exact measurements, and the exact temperature. A safe IAM policy works the same way: exact actions, exact resource ARNs, and specific conditions.
Follow this workflow when creating a new customer managed policy:
- List the exact actions. What API calls does this workload need? Check the AWS documentation for the service’s available actions. Name each one explicitly. Avoid wildcards like
s3:*. - List the exact resources. What specific ARNs do those actions operate on? Use full ARNs where the action supports resource-level permissions.
- Decide if a condition is needed. Should this permission only work with MFA? From a specific VPC? In a specific region? Add a Condition block if so.
- Test with the IAM Policy Simulator before attaching the policy to a production role.
Starting with AdministratorAccess and “narrowing down later” almost never happens. Teams ship the broad policy, forget about it, and move on. Start narrow from day one. It is much easier to add a missing permission when you get an error than to find and remove 15 unused permissions six months later.
Some actions do not support resource-level permissions. For example, ec2:DescribeInstances and sts:GetCallerIdentity always require “Resource”: ”*”. This is not a security shortcut. It is how those APIs are designed. Check the Actions, resources, and condition keys reference page for each service in the AWS docs to know which actions need ”*”.
Example policies
Every JSON example below is valid and copy-paste safe. Replace the account IDs, bucket names, and table names with your own values. All examples use straight quotes and correct ARN formatting.
Example 1: S3 read access to one bucket
This policy allows reading objects from a single S3 bucket and listing its contents.
It uses two statements because s3:ListBucket targets the bucket ARN while
s3:GetObject targets the objects inside it.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowListBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-app-data"
},
{
"Sid": "AllowReadObjects",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-app-data/*"
}
]
}This is safe because it names one bucket, uses read-only actions, and separates the
bucket-level and object-level ARNs correctly. Forgetting the bucket ARN for
ListBucket is a common mistake. See the
common mistakes section below.
The S3 bucket ARN (arn:aws:s3:::my-app-data) and the object ARN (arn:aws:s3:::my-app-data/*) look almost identical but they target different things. Bucket-level actions like ListBucket need the bucket ARN. Object-level actions like GetObject need the /* ARN. Getting this wrong is the number one S3 policy mistake. See S3 IAM vs bucket policies for a deeper look.
Example 2: Lambda access to one DynamoDB table + CloudWatch Logs
A Lambda function that processes orders needs to read and write items in a DynamoDB table and write logs to CloudWatch. This policy gives it exactly that and nothing more.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DynamoDBOrdersAccess",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/orders"
},
{
"Sid": "CloudWatchLogsWrite",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/order-processor:*"
}
]
}This follows the principle of least privilege. The Lambda function cannot delete items, cannot scan the full table, and can only write logs to its own log group. If this function is compromised, the blast radius is one table and one log group.
Example 3: IAM user self-managing their own credentials
This policy lets each IAM user manage their own password and access keys without
giving them access to other users’ credentials. The ${aws:username}
variable resolves to the name of the user making the request.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSelfManageCredentials",
"Effect": "Allow",
"Action": [
"iam:ChangePassword",
"iam:GetUser",
"iam:CreateAccessKey",
"iam:DeleteAccessKey",
"iam:ListAccessKeys",
"iam:GetLoginProfile"
],
"Resource": "arn:aws:iam::123456789012:user/${aws:username}"
}
]
}The ${aws:username} variable makes this one policy work for every
IAM user in the account. Without it, you would need a separate policy per user. This
variable requires “Version”: “2012-10-17”. The older 2008 policy language
does not support variables.
Policy variables like ${aws:username} only work when you set “Version”: “2012-10-17”. If you omit the Version field or use the old “2008-10-17” value, AWS treats the variable as a literal string. Your policy will silently match nothing, and every request will be denied.
Example 4: Trust policy on an IAM role (Principal example)
This is a trust policy attached to an IAM role. It allows the Lambda
service to assume this role
as its execution role. Notice the Principal field. This is what makes it a
trust policy rather than an identity-based policy.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowLambdaAssume",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}This trust policy says: “Only the Lambda service can assume this role.” The permissions policy (a separate document) attached to the same role controls what the Lambda function can actually do once it assumes the role. See IAM roles explained for the full two-policy model on roles.
Every example above is deliberately narrow. That is the point. A well-written policy should feel almost too specific. If your policy looks short and boring, you are probably doing it right. If it is full of wildcards and looks powerful, that is a red flag.
When to use this
You need to understand IAM policy structure whenever you:
- Write customer managed policies, any time you create your own policies instead of using AWS managed ones.
- Review role permissions, auditing what an IAM role can and cannot do.
- Debug AccessDenied errors, tracing which statement is blocking a request and fixing it.
- Read bucket policies or trust policies, understanding who has access to a resource or who can assume a role. See S3 IAM vs bucket policies.
- Manage IAM via CLI or infrastructure as code, writing policy JSON for the AWS CLI or Terraform.
If you are just starting out, focus on identity-based policies first. They cover 90% of day-to-day IAM work. You can learn resource-based and trust policies once you start working with cross-account access or S3 bucket policies.
Common mistakes
- Confusing the policy language Version with managed policy versions. The
“Version”: “2012-10-17”field in JSON is the policy language grammar. It never changes. Managed policy versions (v1, v2, v3) track edits to a customer managed policy over time. Mixing these up leads to confusion about why changing the Version field does nothing useful. - Using wildcard actions or resources in production.
“Action”: “s3:*”or“Resource”: ”*”makes policies easy to write but dangerous to run. Always list specific actions and ARNs. Apply the principle of least privilege. - Forgetting the bucket ARN vs object ARN for S3.
s3:ListBucketoperates on the bucket (arn:aws:s3:::my-bucket).s3:GetObjectands3:PutObjectoperate on objects (arn:aws:s3:::my-bucket/*). A policy with only the/*ARN will fail on list operations. - Putting Principal in an identity-based policy. Identity-based policies (attached to users, groups, roles) must not include
Principal. Only resource-based and trust policies use it. Adding Principal to the wrong type causes a policy validation error.
When a policy does not work as expected, check three things first: (1) Is the resource ARN formatted correctly? (2) Is there an explicit Deny somewhere else overriding your Allow? (3) Are you looking at the right policy type? These three account for the majority of IAM debugging sessions.
- Using NotAction or NotResource without understanding blast radius.
NotActionmeans “everything except these actions,” including any new service AWS launches tomorrow. Unless you have a specific advanced use case, prefer explicitActionlists. - Omitting the Version field. Without
“Version”: “2012-10-17”, AWS defaults to the older 2008 grammar, which does not support policy variables like${aws:username}. Always include the Version field. - Misformatting IAM resource ARNs. IAM ARNs do not include a region:
arn:aws:iam::123456789012:user/alice. Note the empty region segment. Putting a region there creates an invalid ARN that silently fails to match.
Summary
- Every IAM policy has a
Version(always“2012-10-17”) and aStatementarray. - Each statement has Effect, Action (or NotAction), and Resource (or NotResource). Sid and Condition are optional.
- Explicit Deny always overrides Allow. The absence of Allow is an implicit Deny.
- Identity-based policies have no Principal. Resource-based and trust policies require Principal.
- Use specific ARNs and action names. Avoid wildcards in production.
- The JSON
Versionfield is the policy language version, not a managed policy revision number. - Policy variables like
${aws:username}require the“2012-10-17”version and make policies reusable.
Frequently asked questions
What does Effect: Deny do in an IAM policy?
A statement with Effect: Deny explicitly blocks the specified actions. Explicit denies always override any Allow statements from other policies. If a user has one policy that allows S3 access and another that explicitly denies S3 access, the deny wins. You never need to deny everything. The default in AWS is implicit deny, meaning anything not explicitly allowed is already blocked.
What is the difference between Action and NotAction in IAM?
Action lists the specific API calls a statement applies to. NotAction means "every action except these." For example, a statement with NotAction on iam:* and Effect Allow would allow all actions on all services except IAM. NotAction is a power tool with a large blast radius. If AWS adds a new service tomorrow, NotAction automatically includes it. Most policies should use Action with explicit lists.
What is the difference between Resource and NotResource in IAM?
Resource names the specific ARNs a statement applies to. NotResource means "every resource except these." Like NotAction, NotResource is risky because it automatically includes any new resources created in the future. In most cases, listing exact Resource ARNs is safer and more predictable.
When does an IAM policy need a Principal field?
Resource-based policies (like S3 bucket policies and KMS key policies) and trust policies on IAM roles require a Principal field to specify who is allowed access. Identity-based policies, the ones attached to IAM users, groups, or roles, never include Principal because the principal is already implied by who the policy is attached to.
What is the difference between the policy Version field and a managed policy version?
The Version field inside a policy JSON document (always set to "2012-10-17") specifies which IAM policy language grammar to use. It is not a version number you control. A managed policy version (v1, v2, v3, etc.) is a separate concept that tracks revisions of a customer managed policy over time. IAM stores up to 5 versions of a managed policy so you can roll back changes. These are completely independent concepts that share the word "version."