IAM Policy JSON Explained: Effect, Action, Resource and Condition

Read and write AWS IAM policies with confidence. Every field of the policy JSON explained, with real examples, condition keys, policy variables and the mistakes that lock people out.

IAM policies are short JSON documents, but every character matters. A missing /* or a wrong condition operator is the difference between "works" and "AccessDenied", or worse, between "least privilege" and "anyone can delete production". This guide walks through each part of a policy so you can read any policy and write your own.

The anatomy of a policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadReports",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::acme-reports",
        "arn:aws:s3:::acme-reports/*"
      ],
      "Condition": {
        "Bool": {"aws:SecureTransport": "true"}
      }
    }
  ]
}
  • Version: always 2012-10-17. It is the policy language version, not a date you change. The older version does not support policy variables.
  • Statement: a list of rules. Each is evaluated independently.
  • Sid: an optional label that makes the policy readable and easier to debug.

Effect

Allow or Deny. Requests are denied by default; an Allow grants access; an explicit Deny in any applicable policy always wins. Use Deny for guardrails ("never delete this bucket", "never act outside these Regions") rather than for routine permissions.

Action

Actions are written service:Operation, for example ec2:StartInstances or dynamodb:Query. Wildcards are allowed: s3:Get* matches every S3 action starting with Get. Be careful: wildcards also match actions AWS adds in the future.

NotAction inverts the list: "everything except these actions". Combined with Allow it grants a lot, so it is usually paired with Deny, for example denying everything except IAM and STS when MFA is missing.

Resource

Resources are identified by their Amazon Resource Name (ARN):

arn:partition:service:region:account-id:resource

Some details that trip people up:

  • S3 buckets vs objects. s3:ListBucket applies to the bucket ARN (arn:aws:s3:::acme-reports); s3:GetObject applies to objects (arn:aws:s3:::acme-reports/*). Most "I can list but not download" problems come from listing only one of them.
  • Global services like IAM have an empty Region field: arn:aws:iam::123456789012:user/alice.
  • Some actions do not support resource-level permissions (many Describe* and List* calls) and need "Resource": "*". The Service Authorization Reference lists which resources and condition keys each action supports.

Condition

Conditions add context checks. The structure is operator → key → value:

"Condition": {
  "StringEquals": {"aws:RequestedRegion": ["eu-west-1", "eu-central-1"]},
  "IpAddress": {"aws:SourceIp": "203.0.113.0/24"}
}

Multiple operators in one Condition block must all match (AND). Multiple values for one key match if any value matches (OR).

Useful condition keys:

KeyUse
aws:RequestedRegionLimit actions to approved Regions
aws:MultiFactorAuthPresentRequire MFA for sensitive actions
aws:SourceVpce / aws:SourceVpcOnly allow requests through your VPC endpoint
aws:PrincipalTag/…, aws:ResourceTag/…Attribute-based access control (team owns resources with its tag)
aws:SecureTransportReject plain HTTP requests
iam:PolicyArnAllow attaching only specific policies

Watch the operator. StringEquals is exact; StringLike supports wildcards; ArnLike and ArnEquals compare ARNs. The ...IfExists suffix makes a condition pass when the key is absent, which is handy but easy to misuse.

Policy variables

Variables let one policy adapt to whoever uses it. The classic example gives each user their own S3 "home folder":

"Resource": "arn:aws:s3:::team-home/${aws:username}/*"

Variables only work with "Version": "2012-10-17".

Principal (resource-based policies only)

Identity-based policies have no Principal: the identity they are attached to is the principal. Resource-based policies, such as S3 bucket policies, KMS key policies or SQS queue policies, name who is allowed:

"Principal": {"AWS": "arn:aws:iam::111122223333:role/analytics"}

For cross-account access, both sides must agree: the resource policy must allow the other account's principal, and that principal's own identity policy must allow the action.

Evaluation logic in one picture

  1. Is there an explicit Deny in any applicable policy? Then deny.
  2. Do SCPs, permission boundaries or session policies fail to allow it? Then deny.
  3. Does an identity-based or resource-based policy allow it? Then allow.
  4. Otherwise, deny (implicit).

Five common mistakes

  • Using "Resource": "*" with write actions because a single action needed it.
  • Forgetting the bucket ARN (for ListBucket) or the /* object ARN (for GetObject).
  • Writing a condition on a key the action never sends, so the statement never matches.
  • Adding a broad Deny for safety that also blocks your own admin role.
  • Testing only with an admin user. Always test as the actual user or role, or use the IAM Policy Simulator.

Practice it

Try Resource Policies: S3 Bucket Policy vs Lambda Execution Role to see identity and resource policies working together, and Attribute-Based Access Control with Session Tags for advanced conditions. The incident lab Order Writer Gets AccessDenied from DynamoDB is a good debugging exercise.

FAQ

Does a Deny always win?

Yes. An explicit Deny in any policy that applies to the request overrides every Allow.

How do I test a policy before deploying it?

Use the IAM Policy Simulator in the console, or IAM Access Analyzer's policy validation, which flags errors and overly broad statements.

What is the maximum size of a policy?

Managed policies can be up to 6,144 characters (excluding whitespace); inline policies have their own per-identity limits. Large policies usually mean it is time to split responsibilities into separate roles.

Browse all AWS tutorials · Hands-on AWS labs