AWS IAM Policy Structure: Effect, Action, Resource, and Condition Explained

Understanding AWS IAM policy structure is essential for every engineer who needs to grant or restrict access without creating security gaps or silent permission failures.

TL;DR: IAM Policy Elements at a Glance

Element What It Controls Valid Values Required?
Effect Whether the statement permits or denies Allow or Deny Yes
Action Which API operations are covered Service prefix + action name (e.g., s3:GetObject) Yes
Resource Which AWS resources the statement applies to ARN, ARN with wildcard, or * Yes
Condition Contextual constraints that must be satisfied Condition operator + key + value No

What Is an AWS IAM Policy? A Direct Answer

An IAM policy is a JSON document that defines permissions. Each policy contains one or more statements, and each statement is the atomic unit of permission logic. A statement answers four questions: should this be allowed or denied (Effect), what operations (Action), on what resources (Resource), and under what circumstances (Condition). AWS evaluates all applicable statements together using a defined precedence model before granting or denying a request.

IAM Policy Evaluation Flow

Before examining each element individually, it helps to see how AWS processes a policy statement end-to-end. The diagram below shows the evaluation path from an incoming API call to a final access decision.

graph LR A[API Call] --> B{Explicit Deny?} B -- Yes --> C[ACCESS DENIED] B -- No --> D{Explicit Allow?} D -- No --> E[Implicit Deny] D -- Yes --> F{Condition Met?} F -- No --> E F -- Yes --> G[ACCESS GRANTED]
  1. API call arrives — AWS identifies the caller's identity and the requested action.
  2. Explicit Deny check — If any policy attached to the principal contains an explicit Deny for this action and resource, access is denied immediately. No other statement can override this.
  3. Explicit Allow check — AWS looks for at least one Allow statement covering the action and resource. If a Condition is present, it must also evaluate to true.
  4. Implicit Deny — If no explicit Allow is found, access is denied by default. IAM is deny-by-default.

Element 1: Effect — The Binary Decision Gate

Effect accepts exactly two values: Allow or Deny. This is the only element with no ambiguity. An explicit Deny in any attached policy — identity-based, resource-based, permissions boundary, or SCP — overrides any number of Allow statements for the same action and resource. Engineers who rely solely on identity-based Allow policies without auditing resource-based policies or SCPs frequently encounter unexpected access denials in multi-account environments.

{
  "Effect": "Deny",
  "Action": "s3:DeleteBucket",
  "Resource": "*"
}

Explicit Deny is absolute — it cannot be overridden by any Allow, including those granted by an administrator.

Element 2: Action — Mapping to AWS API Operations

The Action element specifies one or more AWS API operations the statement covers. Each action follows the format service-prefix:OperationName, where the service prefix matches the namespace defined in the AWS Service Authorization Reference. Wildcards are supported: s3:Get* matches all S3 read operations, and * alone matches every action across every service.

{
  "Effect": "Allow",
  "Action": [
    "s3:GetObject",
    "s3:ListBucket"
  ],
  "Resource": [
    "arn:aws:s3:::example-bucket",
    "arn:aws:s3:::example-bucket/*"
  ]
}

Notice that s3:ListBucket applies to the bucket ARN itself, while s3:GetObject applies to objects inside it. Mixing these up is one of the most common causes of AccessDenied errors when an application can authenticate but cannot list or read objects.

Think of the Action element as a whitelist of verbs. The service prefix is the noun category — you cannot use an EC2 prefix to authorize an S3 operation, regardless of how the resource ARN is written.

Element 3: Resource — Scoping the IAM Policy Structure to Specific ARNs

The Resource element defines which AWS resources the statement applies to, expressed as ARNs. A value of * means all resources. Partial wildcards within an ARN (e.g., arn:aws:s3:::logs-*) are also valid and useful for environment-based scoping.

Critical constraint: not all IAM actions support resource-level restrictions. Certain List and Describe actions — such as s3:ListAllMyBuckets or ec2:DescribeInstances — require "Resource": "*" because they operate at the account or region scope, not on a specific resource. If you specify a concrete ARN for one of these actions, the policy will not grant the permission and the caller will receive an AccessDenied error, even though the action appears to be allowed. Always verify resource-level support in the Service Authorization Reference before scoping to a specific ARN.

graph LR A[IAM Action] --> B{Resource-level supported?} B -- Yes --> C[Use specific ARN] B -- No --> D[Must use Resource star] C --> E[Permission granted] D --> F[Specific ARN used?] F -- Yes --> G[AccessDenied at runtime] F -- No --> E
  1. Resource-level supported — The action can be restricted to a specific ARN (e.g., s3:GetObject on arn:aws:s3:::example-bucket/*).
  2. Resource-level NOT supported — The action requires "Resource": "*". Specifying a concrete ARN here causes an AccessDenied error at runtime, not a policy validation error. This failure is silent during policy creation.
  3. Wildcard scoping — Partial ARN wildcards (e.g., arn:aws:s3:::prod-*) are valid where resource-level restrictions are supported.

In practice, teams often write overly broad "Resource": "*" for every action to avoid AccessDenied errors, then never revisit the policy. The correct approach is to check the Service Authorization Reference once per action and apply the narrowest valid scope.

Element 4: Condition — Contextual Constraints in IAM Policy Structure

The Condition element is optional but powerful. It adds contextual constraints that must evaluate to true for the statement to apply. A condition block contains one or more condition operators, each mapping one or more condition keys to expected values.

{
  "Effect": "Allow",
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::example-bucket/*",
  "Condition": {
    "StringEquals": {
      "aws:RequestedRegion": "us-east-1"
    },
    "Bool": {
      "aws:SecureTransport": "true"
    }
  }
}

This statement allows s3:GetObject only when the request originates from us-east-1 AND uses HTTPS. Multiple conditions within the same block are evaluated with a logical AND. Multiple values within a single condition key are evaluated with a logical OR.

Condition keys fall into two categories: global condition keys (prefixed aws:, available for all services) and service-specific condition keys (prefixed with the service namespace, e.g., s3:prefix). Not every condition key is available for every action — verify availability in the Service Authorization Reference before relying on a condition key in a production policy.

Putting It Together: A Complete IAM Policy Example

The following policy demonstrates all four elements working together. It allows an application role to read objects from a specific S3 bucket, but only over HTTPS and only from the us-east-1 region. It also contains an explicit Deny for delete operations, which cannot be overridden.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowSecureRead",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::example-bucket",
        "arn:aws:s3:::example-bucket/*"
      ],
      "Condition": {
        "Bool": {
          "aws:SecureTransport": "true"
        },
        "StringEquals": {
          "aws:RequestedRegion": "us-east-1"
        }
      }
    },
    {
      "Sid": "DenyDelete",
      "Effect": "Deny",
      "Action": "s3:DeleteObject",
      "Resource": "arn:aws:s3:::example-bucket/*"
    }
  ]
}

Two separate ARNs are required for s3:ListBucket and s3:GetObject because they operate at different resource scopes — bucket and object respectively. Combining them into a single ARN with /* would cause s3:ListBucket to fail with AccessDenied.

Diagnosing IAM Policy Structure Failures in Production

Step 1: Use IAM Policy Simulator to isolate the denying policy

Production Gotcha: An engineer sees AccessDenied on s3:ListBucket. They verify the identity policy contains an explicit Allow for s3:ListBucket with the correct bucket ARN. The diagnosis stops there — but the actual cause is a Service Control Policy at the organization level that restricts S3 actions to specific regions, and the request is coming from eu-west-1.

— Why this step: AccessDenied errors do not identify which policy layer caused the denial, so the simulator is the only tool that surfaces the denying policy without manual inspection of every attached policy.

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/ExampleRole \
  --action-names s3:ListBucket \
  --resource-arns arn:aws:s3:::example-bucket

Step 2: Verify resource-level support for the failing action

— Why this step: specifying a concrete ARN for an action that requires "Resource": "*" produces an AccessDenied error at runtime with no policy validation warning — the policy saves successfully but never grants the permission.

Check the Service Authorization Reference for the action in question. If the "Resource types" column shows only *, the action does not support resource-level restrictions. Correct the policy by replacing the specific ARN with "Resource": "*" for that action, ideally in a separate statement scoped only to that action.

aws iam get-policy-version \
  --policy-arn arn:aws:iam::123456789012:policy/ExamplePolicy \
  --version-id v1

Step 3: Check for explicit Deny statements across all attached policies

— Why this step: an explicit Deny in any attached policy — including resource-based policies, permissions boundaries, and SCPs — overrides any Allow, and the simulator output will identify the source policy only if all policy types are included in the simulation.

aws iam list-attached-role-policies \
  --role-name ExampleRole

aws iam list-role-policies \
  --role-name ExampleRole

Run both commands. Inline policies and managed policies are stored separately and both must be reviewed. A Deny in an inline policy is just as absolute as one in a managed policy.

IAM Policy Structure: Glossary

Statement
The atomic unit of an IAM policy. A policy document contains one or more statements in the Statement array.
Effect
Specifies whether the statement results in an Allow or Deny. Explicit Deny always takes precedence.
Action
One or more AWS API operations in service-prefix:OperationName format. Wildcards are supported.
Resource
The ARN or ARN pattern identifying which resources the statement applies to. Some actions require * and do not support specific ARNs.
Condition
Optional contextual constraints using condition operators, global keys (aws:), or service-specific keys. Multiple conditions in a block are AND-evaluated.
Condition Operator
The comparison function applied to a condition key and value (e.g., StringEquals, Bool, ArnLike).
Global Condition Key
Condition keys prefixed with aws: that are available across all AWS services (e.g., aws:SecureTransport, aws:RequestedRegion).
Implicit Deny
The default outcome when no explicit Allow matches a request. IAM is deny-by-default — absence of an Allow is a denial.
Resource-Level Permission
The ability to restrict an IAM action to a specific ARN rather than *. Not all actions support this — verify in the Service Authorization Reference.
SCP (Service Control Policy)
An AWS Organizations policy that sets maximum permission boundaries for accounts. An SCP Deny overrides identity-based Allow policies in the affected account.

Related Posts

Comments

Popular posts from this blog

EC2 No Internet Access in Custom VPC: Fix Internet Gateway and Route Table

EC2 SSH Connection Timeout: Which Security Group Rules to Check

Difference Between IAM User and IAM Role: Which One Should Your EC2 Use?