AWS IAM Policy Mastery
A deep dive into crafting, managing, and auditing effective AWS IAM policies using policy-as-code principles.
What You'll Learn
📋 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 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 grantsAWS 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.jsonTesting 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