Skip to content
AWS aws iam 6 min read

IAM Policies Explained

An IAM policy (Identity and Access Management policy) is a small JSON (JavaScript Object Notation) document that lists what actions are allowed or denied in your AWS account. Nothing in AWS is permitted by default — every permission a user, group, or role has comes from a policy attached to it. If you understand how policies are written and how AWS reads them, you understand the heart of AWS security. This page explains what policies are, the two main kinds, and the exact rules AWS uses to decide “yes” or “no” on every request.

What a policy actually is

A policy is just text. It is a JSON object that describes one or more permissions. Each permission says three things: which actions it covers (like s3:GetObject), which resources they apply to (like a specific bucket), and whether the effect is Allow or Deny. When someone tries to do something in AWS, the service gathers up every policy that applies to them and uses those rules to decide whether to let the request through.

Here is a minimal example that lets the holder read objects from one S3 bucket (S3 is AWS’s object storage service):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadReportsBucket",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::devcraftly-reports/*"
    }
  ]
}

The Version is always the date string "2012-10-17" (this is the policy language version, not a date you change). Statement holds one or more permission blocks. Sid is just a human-friendly label. A full breakdown of every field lives on the Policy Document Anatomy page.

Identity-based vs resource-based policies

There are two broad families of policy, and the difference is simply what they attach to.

Policy typeAttaches toAnswers the questionExample
Identity-basedAn IAM user, group, or role”What is this identity allowed to do?”A role policy letting an app write to DynamoDB
Resource-basedA resource itself (an S3 bucket, an SQS queue, a KMS key)Who is allowed to touch this resource?”A bucket policy allowing another account to read the bucket

The key extra difference: a resource-based policy has a Principal field that names who the policy applies to. An identity-based policy has no Principal because it is already attached to a specific identity. Resource-based policies are also the main way to grant access across accounts without creating new users.

When to use which. Reach for identity-based policies for almost everything — they are how you grant your own users and roles their day-to-day permissions. Use a resource-based policy when you need to let an outside identity (another AWS account, or an AWS service) reach into a specific resource. Do not try to solve cross-account access with identity policies alone; the resource usually has to opt in too.

How AWS evaluates a request

This is the part that trips up almost everyone. When a request arrives, AWS does not just look for an Allow. It runs a fixed evaluation process, and the order matters:

  1. Start at deny. Every request begins as an implicit (default) deny. If nothing ever says Allow, the answer is no.
  2. Look for an explicit Deny. AWS scans all applicable policies. If any one of them contains a Deny that matches the request, the answer is no, and evaluation stops. An explicit Deny always wins.
  3. Look for an Allow. If there is no explicit Deny, AWS checks whether some policy grants an Allow for this action and resource. If yes, the request is permitted.
  4. Fall back to deny. If no Allow was found, the implicit deny stands and the request is rejected.

In one sentence: explicit Deny beats Allow, and Allow beats the default deny.

Request → Explicit Deny anywhere?  → YES → DENIED
                 │ NO

          Any matching Allow?        → NO  → DENIED (default)
                 │ YES

              ALLOWED

(Other layers like Service Control Policies and permission boundaries can also restrict access, but the Deny-beats-Allow rule above is the core you need first.)

The gotcha: Allow plus Deny always equals Deny

Say a user is in a Developers group whose policy allows s3:DeleteObject. Separately, a security policy attached to the same user denies s3:DeleteObject on the production bucket. The result is deny — every time. It does not matter that the group clearly grants it. One explicit Deny anywhere in the picture overrides any number of Allows.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ProtectProdBucket",
      "Effect": "Deny",
      "Action": "s3:DeleteObject",
      "Resource": "arn:aws:s3:::devcraftly-prod/*"
    }
  ]
}

This is why debugging “access denied” errors almost always comes down to two questions: Is there an explicit Deny somewhere I forgot about? or Is there simply no Allow granting this at all? Both produce the same error message, so you have to check both.

Debugging tip. When access is unexpectedly blocked, do not start by adding more Allow statements — they will not override a Deny. First hunt down the explicit Deny (it may live in a different policy, a permission boundary, or an organization-wide Service Control Policy). Only if there is no Deny should you suspect a missing Allow.

Working with policies in the Console

  1. Open the IAM console at https://console.aws.amazon.com/iam/.
  2. In the left navigation choose Policies, then Create policy.
  3. Use the Visual editor to pick a service, actions, and resources, or switch to the JSON tab to paste a document directly.
  4. Choose Next, give the policy a Name like ReadReportsBucket, and choose Create policy.
  5. To attach it, go to a user, group, or role, open Add permissions → Attach policies, and select your policy.

Working with policies in the CLI

Create a customer-managed policy from a local JSON file, then attach it to a role:

aws iam create-policy \
  --policy-name ReadReportsBucket \
  --policy-document file://read-reports.json

Output:

{
    "Policy": {
        "PolicyName": "ReadReportsBucket",
        "PolicyId": "ANPA0A1B2C3D4E5F6G7H8",
        "Arn": "arn:aws:iam::123456789012:policy/ReadReportsBucket",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "CreateDate": "2026-06-15T10:50:00+00:00"
    }
}
aws iam attach-role-policy \
  --role-name report-reader \
  --policy-arn arn:aws:iam::123456789012:policy/ReadReportsBucket

This command prints nothing on success. To check what a request would do before relying on it, use the Policy Simulator rather than guessing.

Cost note

IAM policies, users, groups, and roles are free. AWS never charges for IAM itself — you only pay for the underlying resources that the permissions let you use. There is no cost reason to avoid creating well-scoped, specific policies.

Best Practices

  • Grant the smallest set of actions and resources a task needs (least privilege) instead of broad "Action": "*" wildcards.
  • Prefer many small, purpose-named policies over one giant catch-all policy — they are far easier to audit.
  • Use explicit Deny deliberately as a guardrail (for example, blocking deletion of production data), knowing it overrides every Allow.
  • When debugging access errors, check for an explicit Deny first, then a missing Allow — do not pile on more Allow statements blindly.
  • Use the Policy Simulator to test a policy before attaching it to anything that matters.
  • Reserve resource-based policies for cross-account and service-to-service access; keep everyday permissions in identity-based policies.
Last updated June 15, 2026
Was this helpful?