bigonotes
CloudIdentity (IAM)

Writing IAM Policies: JSON, ARNs and Conditions

Read and write IAM policy JSON: each element, ARNs, condition operators and keys, and the service authorization reference.

8 min readLast updated

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:PutBucketPolicy deserve 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/key ARNs.
  • Conditions join different keys with AND and multiple values with OR.
  • s3:prefix applies to s3:ListBucket. Limit object reads through the Resource ARN.
  • Limit callers with aws:PrincipalAccount or aws:PrincipalOrgID, resources with aws:ResourceAccount.
  • Check the Service Authorization Reference for valid actions, resources and keys, then validate.
Revision notes · 2 min

Core idea#

An IAM policy is JSON. Each statement allows or denies actions on resources, optionally under conditions.

Elements#

Element Purpose
Version Always "2012-10-17"; needed for variables
Sid Optional statement label
Effect Allow or Deny
Principal Who; resource-based and trust policies only
Action API actions, wildcards allowed
Resource ARNs; required in identity policies
Condition Tests on the request context

Key points#

  • NotAction, NotResource and NotPrincipal match everything except the list. Pair NotAction with Deny, not Allow.
  • ARN shape: arn:partition:service:region:account-id:resource. S3 bucket ARNs have no Region or account.
  • Bucket actions (s3:ListBucket) take the bucket ARN. Object actions (s3:GetObject) take bucket/key ARNs.
  • Different condition keys combine with AND. Several values for one key combine with OR.
  • IfExists passes when the key is absent. ForAllValues and ForAnyValue handle multi-valued keys.
  • Policy variables such as ${aws:username} fill in values at evaluation time.
  • Matching aws:ResourceTag to aws:PrincipalTag gives ABAC.

Condition keys to know#

Key Use
aws:SecureTransport Require HTTPS
aws:MultiFactorAuthPresent Require MFA
aws:PrincipalAccount, aws:PrincipalOrgID Limit callers
aws:ResourceAccount Limit resources
aws:SourceAccount Service-to-service calls; stops confused deputy
s3:prefix Limit s3:ListBucket to a prefix

Service Authorization Reference#

  • Lists each service's actions, access levels, resource types and condition keys.
  • An empty resource-type column means you must use "Resource": "*".

Pitfalls#

  • Using s3:prefix on s3:GetObject. Limit reads through the object ARN.
  • An action and resource that don't fit together never match.
  • Valid JSON can still be wrong. Validate with IAM Access Analyzer and the policy simulator.

Say it in an interview#

  • "Bucket actions use the bucket ARN; object actions use object ARNs."
  • "I restrict callers with aws:PrincipalAccount, not aws:SourceAccount."

Found a mistake? Email ask@bigonotes.com

Further reading#