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

Every AWS permission decision — whether a Lambda function can write to S3, or a developer can delete a DynamoDB table — ultimately traces back to a JSON IAM policy document. Misreading even one of its four core elements can mean the difference between a working system and a silent access denial that takes hours to debug.

TL;DR — The Four Core IAM Policy Elements

Element Role in the Policy Valid Values / Format Required?
Effect The verdict — allow or deny this statement "Allow" or "Deny" ✅ Yes
Action The operation(s) being governed Service prefix + action (e.g., s3:GetObject), supports wildcards ✅ Yes
Resource The specific AWS resource(s) the action applies to ARN string or "*" for all resources ✅ Yes
Condition Contextual guard — when the statement applies Condition operator + key + value map ❌ Optional

The Anatomy of an IAM Policy Statement

An IAM policy is a JSON document containing one or more statements. Each statement is a self-contained access rule. Think of the policy document as a rulebook, and each statement as a single rule within it.

Here is a canonical policy with all four elements present:

🔽 [Click to expand] — Full IAM Policy Example
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowS3ReadFromCorpNetwork",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-company-data-bucket",
        "arn:aws:s3:::my-company-data-bucket/*"
      ],
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": "203.0.113.0/24"
        }
      }
    }
  ]
}

Deep-Dive: Each Element Explained

1. Effect — The Verdict

Effect is binary: "Allow" or "Deny". It is the final word of the statement. AWS IAM operates on a default-deny model — unless an explicit Allow exists, access is denied. An explicit Deny always overrides any Allow, regardless of policy source or order.

Real-World Analogy: Think of IAM policy evaluation like a building's security system. By default, every door is locked (implicit deny). A keycard grant (Allow) opens specific doors. But a security lockdown order (explicit Deny) overrides all keycards — even the CEO's — for those specific doors. The lockdown always wins.

2. Action — The Operation

Action specifies which AWS API operation(s) the statement controls. The format is always service-prefix:OperationName. You can specify a single action, a list, or use the * wildcard.

  • Single action: "Action": "s3:GetObject"
  • Multiple actions: "Action": ["s3:GetObject", "s3:PutObject"]
  • Wildcard (use with caution): "Action": "s3:*" — grants all S3 operations
  • Full admin (avoid in production): "Action": "*"

⚠️ Least Privilege Principle: Always enumerate only the specific actions your workload requires. Avoid "Action": "*" in any production policy. Use IAM Access Advisor to identify unused permissions and tighten policies over time.

3. Resource — The Target

Resource scopes the statement to specific AWS resources identified by their ARN (Amazon Resource Name). This is where many engineers make critical mistakes — applying overly broad permissions by defaulting to "Resource": "*".

Key nuance for S3: A bucket and its objects have separate ARNs. To allow both listing a bucket and reading its objects, you must specify both:

"Resource": [
  "arn:aws:s3:::my-company-data-bucket",
  "arn:aws:s3:::my-company-data-bucket/*"
]

The first ARN covers bucket-level actions (e.g., s3:ListBucket). The second covers object-level actions (e.g., s3:GetObject). Omitting either is a common source of AccessDenied errors.

4. Condition — The Contextual Guard

Condition is optional but powerful. It adds contextual constraints that must be satisfied for the statement to apply. A condition block has the structure: ConditionOperator → ConditionKey → Value.

"Condition": {
  "StringEquals": {
    "aws:RequestedRegion": "us-east-1"
  }
}

Common use cases for conditions:

  • IP restriction: aws:SourceIp — limit access to a corporate IP range
  • MFA enforcement: aws:MultiFactorAuthPresent — require MFA for sensitive actions
  • Region lock: aws:RequestedRegion — prevent actions outside approved regions
  • Tag-based access: aws:ResourceTag/Environment — scope access by resource tags (ABAC)
  • Secure transport: aws:SecureTransport — enforce HTTPS-only access

How IAM Evaluates a Policy Statement — Logic Flow

When an API call is made, IAM evaluates all applicable policies. The following diagram shows how the four elements interact during a single statement evaluation:

flowchart TD A["API Call Made"] --> B{"Does Action match statement?"} B -- No --> Z1["Statement Skipped"] B -- Yes --> C{"Does Resource ARN match statement?"} C -- No --> Z2["Statement Skipped"] C -- Yes --> D{"Condition block present?"} D -- No --> E["Apply Effect (Allow or Deny)"] D -- Yes --> F{"All Conditions evaluate TRUE?"} F -- No --> Z3["Statement Skipped"] F -- Yes --> E E --> G["Allow: Add to authorized set"] E --> H["Deny: Immediately DENY request"]
  1. API Call Received: A principal (user, role, or service) makes an AWS API request.
  2. Action Match: IAM checks if the requested operation matches the Action element. If not, this statement is skipped.
  3. Resource Match: IAM checks if the target resource ARN matches the Resource element. If not, this statement is skipped.
  4. Condition Evaluation: If a Condition block exists, all conditions must evaluate to true. A single false condition causes the entire statement to be skipped.
  5. Effect Applied: If all checks pass, the Effect (Allow or Deny) is applied to the authorization decision.

Complete Policy Evaluation Order (Multi-Policy Context)

Understanding individual statement elements is step one. In real environments, multiple policies apply simultaneously. Here is the AWS policy evaluation order:

flowchart TD A["Incoming API Request"] --> B{"Explicit Deny in any policy?"} B -- Yes --> DENY1["❌ ACCESS DENIED"] B -- No --> C{"AWS Organizations SCP present?"} C -- Yes --> D{"SCP allows the action?"} D -- No --> DENY2["❌ ACCESS DENIED"] D -- Yes --> E{"Resource-based policy exists?"} C -- No --> E E -- Yes --> F{"Resource policy grants access?"} F -- Yes --> ALLOW["✅ ACCESS GRANTED"] F -- No --> G{"Identity-based policy allows?"} E -- No --> G G -- Yes --> ALLOW G -- No --> DENY3["❌ ACCESS DENIED (Default Deny)"]
  1. Explicit Deny check: If any policy contains an explicit Deny matching the request, access is immediately denied. No further evaluation occurs.
  2. Organizations SCP check: If the account is in an AWS Organization, Service Control Policies are evaluated. The request must be allowed by the SCP.
  3. Resource-based policy check: If the target resource has a resource-based policy (e.g., S3 bucket policy), it is evaluated.
  4. Identity-based policy check: The principal's attached IAM policies (inline + managed) are evaluated for an explicit Allow.
  5. Default Deny: If no explicit Allow is found across all evaluated policies, access is denied.

Practical Example: MFA-Protected S3 Delete

This policy allows deleting objects from a specific S3 bucket only when the request is authenticated with MFA — a common pattern for protecting critical data:

🔽 [Click to expand] — MFA-Protected Delete Policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyDeleteWithoutMFA",
      "Effect": "Deny",
      "Action": "s3:DeleteObject",
      "Resource": "arn:aws:s3:::my-company-data-bucket/*",
      "Condition": {
        "BoolIfExists": {
          "aws:MultiFactorAuthPresent": "false"
        }
      }
    },
    {
      "Sid": "AllowDeleteWithMFA",
      "Effect": "Allow",
      "Action": "s3:DeleteObject",
      "Resource": "arn:aws:s3:::my-company-data-bucket/*",
      "Condition": {
        "Bool": {
          "aws:MultiFactorAuthPresent": "true"
        }
      }
    }
  ]
}

Glossary

Term Definition
Principal The IAM entity (user, role, service) making the API request.
ARN Amazon Resource Name — a globally unique identifier for any AWS resource (e.g., arn:aws:s3:::bucket-name).
Explicit Deny A statement with "Effect": "Deny" that unconditionally overrides any Allow in any policy.
Condition Operator The comparison method in a Condition block (e.g., StringEquals, IpAddress, Bool).
ABAC Attribute-Based Access Control — using resource/principal tags in Condition elements to dynamically scope permissions.

Next Steps

Mastering these four elements is the foundation of all AWS security work. From here, explore these official resources to deepen your IAM expertise:

Comments

Popular posts from this blog

EC2 No Internet Access in Custom VPC: Attaching an Internet Gateway and Fixing Route Tables

IAM User vs. IAM Role: Why Your EC2 Instance Should Never Use a User

Lambda Infinite Loop with S3: How to Prevent Recursive Triggers