AWS for DevOps — IAM Policies

3 minute read

2.3 — IAM Policies

Policies are JSON documents that define what actions are allowed or denied on which AWS resources. Every permission in AWS is controlled by a policy.

Policy Structure

Every policy follows this structure:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowS3ReadAccess",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-deploy-bucket",
        "arn:aws:s3:::my-deploy-bucket/*"
      ]
    }
  ]
}
Field Purpose Values
Version Policy language version Always "2012-10-17"
Statement Array of permission blocks One or more statements
Sid Statement ID (optional label) Any descriptive string
Effect Allow or deny "Allow" or "Deny"
Action API actions "s3:GetObject", "ec2:*", etc.
Resource What the action applies to ARN or "*"
Condition Optional — when the policy applies IP range, time, MFA, etc.

How Policy Evaluation Works

When an IAM principal makes an API call, AWS evaluates all applicable policies:

  Request arrives
       │
       ▼
  Is there an explicit Deny?
       │
  YES ─┤── DENIED (always wins)
       │
       NO
       ▼
  Is there an explicit Allow?
       │
  YES ─┤── ALLOWED
       │
       NO
       ▼
  DENIED (implicit deny — default)

Key rule: An explicit Deny always beats an Allow. If no statement matches, the request is denied by default.

Types of Policies

Type Attached To Use Case
AWS Managed AWS creates and maintains Common tasks (ReadOnlyAccess, S3FullAccess)
Customer Managed You create and maintain Custom permissions for your org
Inline Embedded directly in a user/group/role One-off exceptions (avoid if possible)
Resource-based Attached to a resource (S3, SQS, Lambda) Cross-account access
Permission Boundary Sets max permissions for a user/role Delegation without over-privilege
Service Control Policy (SCP) Attached to an AWS Organization OU Account-level guardrails

Identity-Based vs Resource-Based Policies

Identity-based — attached to a user, group, or role:

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

Resource-based — attached to the resource itself (e.g., S3 bucket policy):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111111111111:role/CrossAccountRole"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-bucket/*"
    }
  ]
}

The key difference: resource-based policies have a Principal field that says who is allowed.

Permission Boundaries

A permission boundary limits the maximum permissions a user or role can have, even if their identity policy allows more.

  Identity Policy          Permission Boundary
  ┌──────────────┐        ┌──────────────┐
  │  s3:*        │        │  s3:Get*     │
  │  ec2:*       │   ∩    │  s3:List*    │
  │  iam:*       │        │  s3:Put*     │
  └──────────────┘        └──────────────┘
                  │
                  ▼
         Effective Permissions:
         s3:Get*, s3:List*, s3:Put*
         (ec2 and iam are blocked)

This is useful when you allow developers to create their own roles but want to prevent them from granting themselves admin access.

Creating a Custom Policy (CLI)

# Create a policy from a JSON file
$ aws iam create-policy \
    --policy-name DeployBucketReadOnly \
    --policy-document file://deploy-bucket-policy.json
{
    "Policy": {
        "PolicyName": "DeployBucketReadOnly",
        "Arn": "arn:aws:iam::123456789012:policy/DeployBucketReadOnly"
    }
}

# Attach it to a role
$ aws iam attach-role-policy \
    --role-name CodeBuildRole \
    --policy-arn arn:aws:iam::123456789012:policy/DeployBucketReadOnly

Useful Condition Keys

Conditions let you add fine-grained control:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:*",
      "Resource": "*",
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": "203.0.113.0/24"
        }
      }
    }
  ]
}
Condition Key Use Case
aws:SourceIp Restrict by IP range
aws:MultiFactorAuthPresent Require MFA
aws:RequestedRegion Restrict to specific regions
aws:PrincipalTag/team ABAC — tag-based access
s3:prefix Limit S3 access to specific prefixes

Policy Simulator

Before attaching a policy, test it:

$ aws iam simulate-principal-policy \
    --policy-source-arn arn:aws:iam::123456789012:user/alice \
    --action-names s3:GetObject \
    --resource-arns arn:aws:s3:::my-bucket/config.yaml
{
    "EvaluationResults": [
        {
            "EvalActionName": "s3:GetObject",
            "EvalDecision": "allowed"
        }
    ]
}

You can also use the IAM Policy Simulator in the Console.

Real-World DevOps Policy Examples

CodeBuild role — build and push Docker images:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ECRPush",
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:PutImage",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload"
      ],
      "Resource": "*"
    },
    {
      "Sid": "CloudWatchLogs",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:123456789012:log-group:/aws/codebuild/*"
    }
  ]
}

Lambda execution role — read from DynamoDB:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:Query"
      ],
      "Resource": "arn:aws:dynamodb:ap-south-1:123456789012:table/Orders"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    }
  ]
}

Common Mistakes

Mistake Why It Hurts Fix
Using "Resource": "*" everywhere Way too broad — violates least privilege Specify exact ARNs
Forgetting the /* on S3 bucket ARN Policy applies to bucket but not objects inside Add both bucket and bucket/*
Not understanding Deny overrides Allow Unexpected access blocks Check for explicit Deny statements
Ignoring policy version date Old version lacks features like variables Always use "2012-10-17"

Quick Reference

Task Command
Create policy aws iam create-policy --policy-name NAME --policy-document file://policy.json
Attach policy to user aws iam attach-user-policy --user-name NAME --policy-arn ARN
Attach policy to role aws iam attach-role-policy --role-name NAME --policy-arn ARN
List attached policies aws iam list-attached-role-policies --role-name NAME
Simulate a policy aws iam simulate-principal-policy --policy-source-arn ARN --action-names ACTION
Get policy details aws iam get-policy --policy-arn ARN

Next → MFA & Best Practices

Updated: