Reading and Writing IAM Policies

~15 min read

The JSON structure of an IAM policy: Effect, Action, Resource, and Condition.

An IAM policy is a JSON document containing one or more Statement blocks. Each statement has: an Effect ("Allow" or "Deny"), an Action (one or more API actions, e.g. "s3:GetObject", or a wildcard like "s3:"), a Resource (the ARN(s) this applies to, e.g. "arn:aws:s3:::my-bucket/" for every object in one bucket, or "*" for all resources of that type — used sparingly), and optionally a Condition block restricting when the statement applies (e.g. only if the request comes from a specific IP range, or only if MFA was used in the last hour).

Evaluation logic matters: AWS evaluates every policy attached to the calling identity — directly attached, inherited from group membership, and from any assumed role — and combines them. If ANY statement anywhere has an explicit Deny matching the request, the request is denied, full stop, regardless of any Allow elsewhere. If there's no explicit Deny, the request is allowed only if at least one statement explicitly allows it; the default, with no matching statement at all, is deny. This 'explicit deny always wins, default deny otherwise' logic is the single most important mental model for debugging IAM permission issues.

AWS provides Managed Policies (pre-written by AWS for common use cases, like "AmazonS3ReadOnlyAccess") that you can attach directly without writing JSON, which is the recommended starting point before reaching for custom policies.

In the AWS Console

  1. 1

    IAM → Policies → Create policy → JSON tab

    Paste or write the policy JSON directly, or use the Visual editor to build it by selecting a service, actions, and resources from dropdowns.

    The Visual editor is a good way to discover exact action names (e.g. 's3:GetObject') without memorizing them.

  2. 2

    IAM → Policies → [your policy] → Policy usage tab

    Check which users/groups/roles this policy is currently attached to before editing it.

    Editing a shared policy affects every identity it's attached to — always check blast radius first.

💻 Code example

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::my-app-bucket",
        "arn:aws:s3:::my-app-bucket/*"
      ]
    },
    {
      "Effect": "Deny",
      "Action": "s3:DeleteObject",
      "Resource": "arn:aws:s3:::my-app-bucket/*"
    }
  ]
}

💬 Deep Dive with AI

Key points

  • A policy statement = Effect + Action + Resource (+ optional Condition)
  • Explicit Deny always wins over any Allow, anywhere in any attached policy
  • With no matching statement at all, the default is deny — nothing is allowed unless explicitly granted
  • Prefer AWS Managed Policies for common cases before writing custom JSON from scratch