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.”

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.

Warning

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

FieldRequired?Where validWhat it doesBeginner note
VersionRecommendedTop levelSets the policy language grammar. Always “2012-10-17”.This is NOT a managed policy version (v1, v2). See the warning below.
StatementYesTop levelArray of one or more permission rules.A single policy can contain many statements with different effects.
SidNoInside a statementStatement ID, a human-readable label.Use descriptive Sids like “AllowS3Read”. Makes policies readable months later.
EffectYesInside 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.
ActionYes (or NotAction)Inside a statementLists the API operations this statement applies to. Format: service:ApiName.Name specific actions. Avoid ”*” in production.
NotActionNoInside a statement (instead of Action)Matches every action except the ones listed.Dangerous. Automatically includes new services AWS adds in the future.
ResourceYes (or NotResource)Inside a statementThe ARNs this statement applies to.Some actions (like ec2:DescribeInstances) require ”*” because they do not support resource-level scoping.
NotResourceNoInside a statement (instead of Resource)Matches every resource except the ones listed.Rarely needed. Auto-includes any new resources created later.
ConditionNoInside a statementExtra requirements (MFA, IP, region, tags) that must be true for the statement to apply.See IAM policy conditions explained for the full reference.
PrincipalOnly in resource-based and trust policiesInside a statementSpecifies who (which account, user, role, or service) the statement applies to.Never put Principal in an identity-based policy. It causes a validation error.
Warning

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 Principal field is needed or allowed.
  • Resource-based policies are attached to a resource (S3 bucket, KMS key, SQS queue). They must include a Principal field 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 Principal field naming the trusted service, account, or user.
Policy typeAttached toControls whatIncludes Principal?Common examples
Identity-basedIAM user, group, or roleWhat the identity can doNoCustomer managed policies, AWS managed policies, inline policies
Resource-basedAWS resource (S3, KMS, SQS, Lambda)Who can access the resourceYesS3 bucket policies, KMS key policies, SQS queue policies
Trust policyIAM roleWho can assume the roleYesEC2 service trust, Lambda service trust, cross-account trust
Note

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

When you see an unfamiliar policy, or you are debugging an AccessDenied error, work through each statement with this checklist:

  1. Effect: Is this statement granting or blocking access?
  2. Action / NotAction: Which API calls does it cover? If it uses NotAction, what is excluded and what massive set is included?
  3. Resource / NotResource: Is this scoped to a specific ARN, or is it a wildcard? If NotResource, which resources are excluded?
  4. Condition: Are there extra requirements like MFA, region, IP, or tags? See IAM policy conditions explained for how to interpret these.
  5. Principal (if present): Who does this statement apply to? Is it a service, a specific role, or an entire account?
  6. 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.
Note

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.

Tip

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

Follow this workflow when creating a new customer managed policy:

  1. 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:*.
  2. List the exact resources. What specific ARNs do those actions operate on? Use full ARNs where the action supports resource-level permissions.
  3. 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.
  4. Test with the IAM Policy Simulator before attaching the policy to a production role.
Warning

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.

Tip

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

Note

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.

Tip

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.

Warning

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.

Note

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.
Tip

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

  1. 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.
  2. 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.
  3. Forgetting the bucket ARN vs object ARN for S3. s3:ListBucket operates on the bucket (arn:aws:s3:::my-bucket). s3:GetObject and s3:PutObject operate on objects (arn:aws:s3:::my-bucket/*). A policy with only the /* ARN will fail on list operations.
  4. 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.
Tip

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.

  1. Using NotAction or NotResource without understanding blast radius. NotAction means “everything except these actions,” including any new service AWS launches tomorrow. Unless you have a specific advanced use case, prefer explicit Action lists.
  2. 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.
  3. 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.

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."

Last verified: 30 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.