advanced35 min readcloud-providersUpdated: 2025-06-28

AWS IAM Policy Mastery

A deep dive into crafting, managing, and auditing effective AWS IAM policies using policy-as-code principles.

📋 Prerequisites

  • AWS account with IAM access
  • Basic understanding of JSON syntax
  • Familiarity with AWS CLI or AWS Console
  • Understanding of cloud security principles
  • Basic knowledge of Terraform (for automation examples)

🎯 What You'll Learn

  • Master the four types of IAM policies and when to use each
  • Understand AWS policy evaluation logic and decision flow
  • Implement least privilege access with advanced techniques
  • Hands-on - Create dynamic policies using variables and conditions
  • Hands-on - Build tag-based access control (TBAC) systems
  • Hands-on - Automate IAM policy management with Terraform
  • Use IAM Access Analyzer for continuous security improvement
  • Test and validate policies before deployment

🏷️ Topics Covered

aws iam policy examples jsoniam policy generator best practicesaws least privilege policy examplesiam policy validation and testingaws permissions boundary tutorialiam policy troubleshooting guide

AWS IAM Policy Examples JSON: Understanding Architecture for Beginners

AWS Identity and Access Management (IAM) is the cornerstone of AWS security. It controls who can access what resources and what actions they can perform. With over 280 AWS services and thousands of actions, mastering IAM is essential for secure cloud operations.

IAM operates on two core principles: authentication (proving who you are) and authorization (determining what you can do). Understanding this distinction is crucial for effective policy design.

👤 Users & Groups

Permanent identities for people and applications. Groups provide a way to assign permissions to multiple users simultaneously.

🎭 Roles

Temporary identities that can be assumed by trusted entities. No permanent credentials needed.

📋 Policies

JSON documents that define permissions. Can be attached to users, groups, roles, or resources.

IAM Policy Generator Best Practices: Four Types Explained

Understanding policy types is fundamental to IAM mastery. Each type serves different purposes and has distinct evaluation logic.

🔧 1. Identity-Based Policies

Attached to IAM identities (users, groups, roles). These define what actions the identity can perform.

identity-based-policy.json

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

🔧 2. Resource-Based Policies

Attached directly to AWS resources (S3 buckets, KMS keys, etc.). These specify who can access the resource.

s3-bucket-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCrossAccountAccess",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/DataAnalysisRole"
      },
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3:::shared-data-bucket/*"
      ],
      "Condition": {
        "StringEquals": {
          "s3:ExistingObjectTag/Classification": "Public"
        }
      }
    }
  ]
}

🔧 3. Permissions Boundaries

Advanced feature that sets the maximum permissions an identity can have, acting as a filter.

permissions-boundary.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyHighRiskActions",
      "Effect": "Deny",
      "Action": [
        "iam:CreateRole",
        "iam:DeleteRole",
        "iam:AttachRolePolicy",
        "iam:DetachRolePolicy",
        "organizations:*",
        "account:*"
      ],
      "Resource": "*"
    },
    {
      "Sid": "AllowDevelopmentActions",
      "Effect": "Allow",
      "Action": [
        "ec2:*",
        "s3:*",
        "lambda:*",
        "logs:*",
        "cloudformation:*"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:RequestedRegion": ["us-east-1", "us-west-2"]
        }
      }
    }
  ]
}

Policy Evaluation Logic

AWS follows a specific decision logic when evaluating policies. Understanding this flow is crucial for troubleshooting access issues and designing effective policies.

AWS Policy Evaluation Flow

// Simplified AWS Policy Evaluation Logic

1. Authentication: Verify requester identity
2. Context Collection: Gather request information (action, resource, conditions)
3. Policy Retrieval: Collect all applicable policies
4. Decision Logic:

    if (explicit_deny_exists) {
        return DENY;  // Explicit deny always wins
    }
    
    if (scp_allows && permissions_boundary_allows && 
        (identity_policy_allows || resource_policy_allows)) {
        return ALLOW;
    }
    
    return DENY;  // Default deny

Key Points:
- Default decision is DENY
- Explicit DENY always overrides ALLOW
- Need permission from identity OR resource policy
- SCPs and boundaries act as filters, not grants

AWS Least Privilege Policy Examples: Step-by-Step Implementation

Least privilege means granting only the minimum permissions necessary to perform required tasks. This section shows practical techniques for implementing this principle.

🔧 Progressive Permission Refinement

Start with broader permissions and progressively narrow them based on actual usage patterns.

least-privilege-evolution.json

// Stage 1: Broad permissions (starting point)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:*",
      "Resource": "*"
    }
  ]
}

// Stage 2: Service-level scoping
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:ListBucket"
      ],
      "Resource": "*"
    }
  ]
}

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

// Stage 4: Condition-based restrictions (final state)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3:::my-app-bucket/data/${aws:username}/*"
      ],
      "Condition": {
        "StringEquals": {
          "s3:ExistingObjectTag/Owner": "${aws:username}"
        },
        "DateGreaterThan": {
          "aws:CurrentTime": "2025-01-01T00:00:00Z"
        },
        "IpAddress": {
          "aws:SourceIp": "203.0.113.0/24"
        }
      }
    }
  ]
}

IAM Policy Validation and Testing: Dynamic Variables Tutorial

AWS policy variables and conditions enable dynamic, context-aware permissions that adapt to the requester and request context.

🔧 Self-Service IAM Management

self-service-iam.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowSelfPasswordChange",
      "Effect": "Allow",
      "Action": [
        "iam:ChangePassword",
        "iam:GetAccountPasswordPolicy"
      ],
      "Resource": [
        "arn:aws:iam::*:user/${aws:username}"
      ]
    },
    {
      "Sid": "AllowSelfAccessKeyManagement",
      "Effect": "Allow",
      "Action": [
        "iam:CreateAccessKey",
        "iam:DeleteAccessKey",
        "iam:GetAccessKeyLastUsed",
        "iam:ListAccessKeys",
        "iam:UpdateAccessKey"
      ],
      "Resource": [
        "arn:aws:iam::*:user/${aws:username}"
      ]
    },
    {
      "Sid": "AllowSelfMFAManagement",
      "Effect": "Allow",
      "Action": [
        "iam:CreateVirtualMFADevice",
        "iam:DeleteVirtualMFADevice",
        "iam:EnableMFADevice",
        "iam:ResyncMFADevice"
      ],
      "Resource": [
        "arn:aws:iam::*:mfa/${aws:username}",
        "arn:aws:iam::*:user/${aws:username}"
      ]
    }
  ]
}

🔧 Tag-Based Access Control (TBAC)

tag-based-access.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAccessToOwnDepartmentResources",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:StartInstances",
        "ec2:StopInstances",
        "ec2:RebootInstances"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "ec2:ResourceTag/Department": "${aws:PrincipalTag/Department}",
          "ec2:ResourceTag/Environment": ["development", "staging"]
        }
      }
    },
    {
      "Sid": "RequireTagsOnResourceCreation",
      "Effect": "Allow",
      "Action": [
        "ec2:RunInstances"
      ],
      "Resource": [
        "arn:aws:ec2:*:*:instance/*",
        "arn:aws:ec2:*:*:volume/*"
      ],
      "Condition": {
        "StringEquals": {
          "aws:RequestedRegion": ["us-east-1", "us-west-2"]
        },
        "ForAllValues:StringEquals": {
          "aws:TagKeys": ["Department", "Environment", "Owner", "Project"]
        },
        "StringEquals": {
          "ec2:CreateAction": "RunInstances"
        }
      }
    }
  ]
}

AWS Permissions Boundary Tutorial: Terraform Automation Guide

Managing IAM at scale requires Infrastructure as Code. Terraform provides powerful capabilities for automating IAM policy creation and management.

🔧 Reusable IAM Role Module

modules/iam-role/main.tf

# IAM Role with configurable trust policy and permissions
variable "role_name" {
  description = "Name of the IAM role"
  type        = string
}

variable "trusted_services" {
  description = "AWS services that can assume this role"
  type        = list(string)
  default     = []
}

variable "trusted_accounts" {
  description = "AWS accounts that can assume this role"
  type        = list(string)
  default     = []
}

variable "managed_policy_arns" {
  description = "List of managed policy ARNs to attach"
  type        = list(string)
  default     = []
}

variable "inline_policies" {
  description = "Map of inline policies to attach"
  type        = map(any)
  default     = {}
}

variable "permissions_boundary_arn" {
  description = "ARN of permissions boundary policy"
  type        = string
  default     = null
}

# Create the IAM role
resource "aws_iam_role" "this" {
  name = var.role_name
 
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = merge(
          length(var.trusted_services) > 0 ? { Service = var.trusted_services } : {},
          length(var.trusted_accounts) > 0 ? { AWS = var.trusted_accounts } : {}
        )
      }
    ]
  })

  permissions_boundary = var.permissions_boundary_arn
 
  tags = {
    ManagedBy = "Terraform"
    Module    = "iam-role"
  }
}

# Attach managed policies
resource "aws_iam_role_policy_attachment" "managed_policies" {
  for_each = toset(var.managed_policy_arns)
 
  role       = aws_iam_role.this.name
  policy_arn = each.value
}

# Create inline policies
resource "aws_iam_role_policy" "inline_policies" {
  for_each = var.inline_policies
 
  name = each.key
  role = aws_iam_role.this.id
 
  policy = jsonencode(each.value)
}

# Outputs
output "role_arn" {
  description = "ARN of the created IAM role"
  value       = aws_iam_role.this.arn
}

output "role_name" {
  description = "Name of the created IAM role"
  value       = aws_iam_role.this.name
}

🔧 Using the Module

environments/production/iam.tf

# Example usage of the IAM role module
module "lambda_execution_role" {
  source = "../../modules/iam-role"
 
  role_name           = "lambda-execution-role"
  trusted_services    = ["lambda.amazonaws.com"]
 
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
  ]
 
  inline_policies = {
    "DynamoDBAccess" = {
      Version = "2012-10-17"
      Statement = [
        {
          Effect = "Allow"
          Action = [
            "dynamodb:GetItem",
            "dynamodb:PutItem",
            "dynamodb:UpdateItem",
            "dynamodb:DeleteItem"
          ]
          Resource = [
            aws_dynamodb_table.app_data.arn,
            "${aws_dynamodb_table.app_data.arn}/*"
          ]
        }
      ]
    }
  }
}

# Create a role for cross-account access
module "cross_account_role" {
  source = "../../modules/iam-role"
 
  role_name        = "cross-account-access-role"
  trusted_accounts = ["123456789012", "210987654321"]
 
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/ReadOnlyAccess"
  ]
 
  inline_policies = {
    "S3SpecificAccess" = {
      Version = "2012-10-17"
      Statement = [
        {
          Effect = "Allow"
          Action = [
            "s3:GetObject",
            "s3:PutObject"
          ]
          Resource = [
            "arn:aws:s3:::shared-data-bucket/*"
          ]
          Condition = {
            StringEquals = {
              "s3:ExistingObjectTag/SharedAccess" = "true"
            }
          }
        }
      ]
    }
  }
}

IAM Policy Troubleshooting Guide: Access Analyzer Best Practices

IAM Access Analyzer helps you identify unused permissions, external access, and optimize your policies for least privilege.

🔧 Setup Access Analyzer with Terraform

Setup Access Analyzer with Terraform

# Create Access Analyzer
resource "aws_accessanalyzer_analyzer" "main" {
  analyzer_name = "main-analyzer"
  type          = "ACCOUNT"  # or "ORGANIZATION" for org-wide analysis
 
  tags = {
    Environment = "production"
    Purpose     = "security-analysis"
  }
}

# Archive expected findings (like approved external access)
resource "aws_accessanalyzer_archive_rule" "approved_external_access" {
  analyzer_name = aws_accessanalyzer_analyzer.main.analyzer_name
  rule_name     = "approved-partner-access"
 
  filter {
    criteria = "resource"
    eq       = ["arn:aws:s3:::partner-shared-bucket"]
  }
 
  filter {
    criteria = "principal"
    eq       = ["123456789012"]  # Approved partner account
  }
}

# EventBridge rule to alert on new findings
resource "aws_cloudwatch_event_rule" "access_analyzer_findings" {
  name = "access-analyzer-new-findings"
 
  event_pattern = jsonencode({
    source      = ["aws.access-analyzer"]
    detail-type = ["Access Analyzer Finding"]
    detail = {
      status = ["ACTIVE"]
      resourceType = ["AWS::S3::Bucket", "AWS::IAM::Role"]
    }
  })
}

# Send alerts to SNS topic
resource "aws_cloudwatch_event_target" "findings_to_sns" {
  rule      = aws_cloudwatch_event_rule.access_analyzer_findings.name
  target_id = "AccessAnalyzerFindings"
  arn       = aws_sns_topic.security_alerts.arn
}

🔧 Analyzing Unused Access

analyze-unused-access.sh

#!/bin/bash

# Script to analyze and report unused IAM access
ANALYZER_ARN="arn:aws:access-analyzer:us-east-1:123456789012:analyzer/main-analyzer"

echo "Generating unused access report..."

# Start unused access analysis
ANALYSIS_ID=$(aws accessanalyzer start-policy-generation \
  --policy-generation-details '{
    "principalArn": "arn:aws:iam::123456789012:role/MyApplicationRole"
  }' \
  --query 'jobId' \
  --output text)

echo "Analysis started with ID: $ANALYSIS_ID"

# Wait for analysis to complete
while true; do
  STATUS=$(aws accessanalyzer get-generated-policy \
    --job-id "$ANALYSIS_ID" \
    --query 'jobDetails.status' \
    --output text)
 
  if [ "$STATUS" = "SUCCEEDED" ]; then
    echo "Analysis completed successfully"
    break
  elif [ "$STATUS" = "FAILED" ]; then
    echo "Analysis failed"
    exit 1
  else
    echo "Analysis in progress... (Status: $STATUS)"
    sleep 30
  fi
done

# Get the generated policy (optimized for least privilege)
aws accessanalyzer get-generated-policy \
  --job-id "$ANALYSIS_ID" \
  --query 'generatedPolicyResult.generatedPolicies[0].policy' \
  --output text > optimized-policy.json

echo "Optimized policy saved to optimized-policy.json"

# Compare with current policy
echo "=== CURRENT POLICY ==="
aws iam get-role-policy \
  --role-name MyApplicationRole \
  --policy-name MyApplicationPolicy \
  --query 'PolicyDocument'

echo "=== RECOMMENDED POLICY ==="
cat optimized-policy.json

Testing and Validating Policies

Always test policies before deploying them to production. AWS provides several tools for policy validation and simulation.

🔧 Policy Test Suite

policy-test-suite.py

#!/usr/bin/env python3
"""
IAM Policy Testing Suite
Tests IAM policies using AWS Policy Simulator
"""

import boto3
import json
import sys
from typing import List, Dict, Any

class IAMPolicyTester:
    def __init__(self):
        self.iam = boto3.client('iam')
   
    def test_policy(self, policy_document: Dict[Any, Any], test_cases: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        """Test a policy document against multiple test cases"""
        results = []
       
        for test_case in test_cases:
            result = self.iam.simulate_custom_policy(
                PolicyInputList=[json.dumps(policy_document)],
                ActionNames=[test_case['action']],
                ResourceArns=[test_case['resource']],
                ContextEntries=test_case.get('context', [])
            )
           
            evaluation = result['EvaluationResults'][0]
            results.append({
                'test_name': test_case['name'],
                'action': test_case['action'],
                'resource': test_case['resource'],
                'expected': test_case['expected'],
                'actual': evaluation['EvalDecision'],
                'passed': evaluation['EvalDecision'] == test_case['expected'],
                'matched_statements': evaluation.get('MatchedStatements', [])
            })
       
        return results
   
    def validate_policy_syntax(self, policy_document: Dict[Any, Any]) -> bool:
        """Validate policy syntax using AWS"""
        try:
            result = self.iam.validate_policy_document(
                Document=json.dumps(policy_document)
            )
           
            if result['IsTruncated']:
                print("Warning: Validation result was truncated")
           
            return True
        except Exception as e:
            print(f"Policy validation failed: {e}")
            return False

def main():
    # Example policy to test
    policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "s3:GetObject",
                    "s3:PutObject"
                ],
                "Resource": [
                    "arn:aws:s3:::my-app-bucket/*"
                ],
                "Condition": {
                    "StringEquals": {
                        "s3:ExistingObjectTag/Department": "${aws:PrincipalTag/Department}"
                    }
                }
            }
        ]
    }
   
    # Test cases
    test_cases = [
        {
            'name': 'Allow GetObject on tagged resource',
            'action': 's3:GetObject',
            'resource': 'arn:aws:s3:::my-app-bucket/data/file.txt',
            'expected': 'allowed',
            'context': [
                {
                    'ContextKeyName': 's3:ExistingObjectTag/Department',
                    'ContextKeyValues': ['Engineering'],
                    'ContextKeyType': 'string'
                },
                {
                    'ContextKeyName': 'aws:PrincipalTag/Department',
                    'ContextKeyValues': ['Engineering'],
                    'ContextKeyType': 'string'
                }
            ]
        },
        {
            'name': 'Deny GetObject on differently tagged resource',
            'action': 's3:GetObject',
            'resource': 'arn:aws:s3:::my-app-bucket/data/file.txt',
            'expected': 'explicitDeny',
            'context': [
                {
                    'ContextKeyName': 's3:ExistingObjectTag/Department',
                    'ContextKeyValues': ['Finance'],
                    'ContextKeyType': 'string'
                },
                {
                    'ContextKeyName': 'aws:PrincipalTag/Department',
                    'ContextKeyValues': ['Engineering'],
                    'ContextKeyType': 'string'
                }
            ]
        },
        {
            'name': 'Deny DeleteObject (not in policy)',
            'action': 's3:DeleteObject',
            'resource': 'arn:aws:s3:::my-app-bucket/data/file.txt',
            'expected': 'implicitDeny'
        }
    ]
   
    tester = IAMPolicyTester()
   
    # Validate syntax first
    print("Validating policy syntax...")
    if not tester.validate_policy_syntax(policy_document):
        print("Policy syntax validation failed!")
        sys.exit(1)
    print("✓ Policy syntax is valid")
   
    # Run tests
    print("\nRunning policy tests...")
    results = tester.test_policy(policy_document, test_cases)
   
    # Report results
    passed = 0
    failed = 0
   
    for result in results:
        status = "✓ PASS" if result['passed'] else "✗ FAIL"
        print(f"{status}: {result['test_name']}")
        print(f"    Expected: {result['expected']}, Got: {result['actual']}")
       
        if result['passed']:
            passed += 1
        else:
            failed += 1
            print(f"    Action: {result['action']}")
            print(f"    Resource: {result['resource']}")
        print()
   
    print(f"Results: {passed} passed, {failed} failed")
   
    if failed > 0:
        sys.exit(1)

if __name__ == "__main__":
    main()

🎉 Congratulations!

AWS IAM Policy Mastery Achievement

You have mastered AWS IAM policy fundamentals and advanced techniques. You now have the skills to:

Design Secure Policies

Design secure, least-privilege IAM policies using all policy types

Implement TBAC

Implement dynamic, tag-based access control systems

Automate Management

Automate IAM policy management with Infrastructure as Code

Security Improvement

Use IAM Access Analyzer for continuous security improvement

Testing & Validation

Test and validate policies before production deployment

Enterprise Patterns

Apply enterprise-grade IAM patterns and avoid common pitfalls

Next Steps