TL;DR
- Use AWS IAM Access Analyzer’s ValidatePolicy API in CI/CD to catch overly permissive policies before deployment—it’s free and catches grammar errors plus best practice violations
- Combine static analysis (Checkov) with policy simulation (IAM Policy Simulator) for complete coverage—static catches patterns, simulation catches runtime behavior
- Cross-cloud IAM testing requires different tools: AWS Access Analyzer, Azure Policy Insights, GCP Policy Analyzer each have unique capabilities
Best for: Teams managing IAM policies in IaC who need to prevent privilege escalation and ensure least-privilege Skip if: You use managed identity services where policies are abstracted (e.g., AWS SSO with pre-built permission sets) Read time: 14 minutes
Overly permissive IAM policies remain one of the top causes of cloud breaches. A single "Resource": "*" or missing condition can expose your entire AWS account. Studies show 82% of cloud misconfigurations stem from human error—making automated IAM policy testing essential, not optional.
For broader infrastructure testing context, see Policy as Code Testing and Compliance Testing for IaC.
AI-Assisted Approaches
AI tools excel at analyzing complex IAM policies and generating comprehensive test suites.
Analyzing IAM policies for excessive permissions:
Analyze this AWS IAM policy for security issues:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:*"],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": ["iam:PassRole"],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": ["ec2:*"],
"Resource": "*",
"Condition": {
"StringEquals": {"ec2:Region": "us-east-1"}
}
}
]
}
Identify: Privilege escalation risks, overly broad permissions, missing
conditions, and provide a least-privilege alternative for a CI/CD pipeline role.
Generating IAM policy test cases:
Generate pytest test cases for validating AWS IAM policies using boto3:
1. Verify no policies allow iam:* or iam:PassRole to Resource "*"
2. Check that S3 policies require encryption conditions
3. Ensure no policies allow assume-role to external accounts without conditions
4. Validate that admin policies are only attached to break-glass roles
Include proper AWS session setup, policy document parsing, and clear assertions.
Creating Checkov custom checks:
Write a custom Checkov check in Python that validates:
1. IAM policies must not allow actions ending in "*" (e.g., s3:*, ec2:*)
2. All iam:PassRole statements must have explicit Resource ARNs
3. Policies with administrative actions must have MFA conditions
4. Cross-account trust policies must specify explicit account IDs
Include the check class, evaluation logic, and supported resource types.
When to Use Different Testing Approaches
Testing Strategy Decision Framework
| Test Type | Tool | When to Run | What It Catches |
|---|---|---|---|
| Grammar validation | IAM Access Analyzer | Pre-commit, CI | Syntax errors, deprecated elements |
| Best practices | Access Analyzer ValidatePolicy | CI pipeline | Overly permissive actions, missing conditions |
| Static analysis | Checkov, cfn-nag | Pre-commit, CI | Known bad patterns (Resource: “*”) |
| Policy simulation | IAM Policy Simulator | Pre-deploy | Actual permission behavior |
| Access analysis | Access Analyzer findings | Continuous | External access, unused permissions |
| Custom rules | OPA/Rego | CI pipeline | Organization-specific requirements |
Critical IAM Policy Checks
| Check ID | Description | Risk Level |
|---|---|---|
| CKV_AWS_1 | IAM policies should not allow full “*” administrative privileges | Critical |
| CKV_AWS_49 | IAM policies should not allow PassRole to “*” | Critical |
| CKV_AWS_40 | IAM policies should not allow privilege escalation | Critical |
| CKV_AWS_62 | IAM policies should not have empty SID | Low |
| CKV_AWS_109 | IAM policies should not allow credentials exposure | High |
| CKV_AWS_110 | IAM policies should not allow permissions management without constraints | High |
AWS IAM Policy Testing
IAM Access Analyzer Policy Validation
# Install the Terraform IAM Policy Validator
pip install tf-policy-validator
# Validate Terraform template
tf-policy-validator validate \
--template-path ./terraform \
--region us-east-1
# Run specific checks
tf-policy-validator validate \
--template-path ./terraform \
--region us-east-1 \
--check-type VALIDATE_POLICY \
--check-type CHECK_NO_NEW_ACCESS
GitHub Actions Integration
name: IAM Policy Validation
on:
pull_request:
paths:
- 'terraform/**/*.tf'
- 'cloudformation/**/*.yaml'
jobs:
validate-iam-policies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Validate Terraform IAM Policies
uses: aws-actions/terraform-aws-iam-policy-validator@v1
with:
template-path: terraform/
region: us-east-1
check-no-new-access: true
check-access-not-granted: true
check-no-public-access: true
- name: Run Checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: terraform/
check: CKV_AWS_1,CKV_AWS_40,CKV_AWS_49,CKV_AWS_109,CKV_AWS_110
framework: terraform
Checkov for IAM Policies
# Install Checkov
pip install checkov
# Scan for IAM-specific issues
checkov -d ./terraform --check CKV_AWS_1,CKV_AWS_40,CKV_AWS_49
# Output as JUnit for CI
checkov -d ./terraform --framework terraform \
--check CKV_AWS_1,CKV_AWS_40,CKV_AWS_49,CKV_AWS_109,CKV_AWS_110 \
--output junitxml > iam-checkov-results.xml
Custom Checkov Policy Check
# custom_checks/iam_no_passrole_star.py
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckResult, CheckCategories
import json
class IAMNoPassRoleStar(BaseResourceCheck):
def __init__(self):
name = "Ensure IAM policies do not allow PassRole to all resources"
id = "CKV_CUSTOM_IAM_1"
supported_resources = ['aws_iam_policy', 'aws_iam_role_policy']
categories = [CheckCategories.IAM]
super().__init__(name=name, id=id, categories=categories,
supported_resources=supported_resources)
def scan_resource_conf(self, conf):
policy = conf.get('policy', [None])[0]
if not policy:
return CheckResult.UNKNOWN
# Handle both string and dict formats
if isinstance(policy, str):
try:
policy = json.loads(policy)
except json.JSONDecodeError:
return CheckResult.UNKNOWN
statements = policy.get('Statement', [])
for statement in statements:
if statement.get('Effect') != 'Allow':
continue
actions = statement.get('Action', [])
if isinstance(actions, str):
actions = [actions]
resources = statement.get('Resource', [])
if isinstance(resources, str):
resources = [resources]
# Check for PassRole with * resource
has_passrole = any('PassRole' in action for action in actions)
has_star_resource = '*' in resources
if has_passrole and has_star_resource:
return CheckResult.FAILED
return CheckResult.PASSED
check = IAMNoPassRoleStar()
Python Policy Testing with boto3
import boto3
import json
import pytest
class TestIAMPolicies:
@pytest.fixture
def iam_client(self):
return boto3.client('iam')
@pytest.fixture
def access_analyzer_client(self):
return boto3.client('accessanalyzer')
def test_no_admin_star_policies(self, iam_client):
"""Verify no policies grant full admin access."""
paginator = iam_client.get_paginator('list_policies')
for page in paginator.paginate(Scope='Local'):
for policy in page['Policies']:
version = iam_client.get_policy_version(
PolicyArn=policy['Arn'],
VersionId=policy['DefaultVersionId']
)
document = version['PolicyVersion']['Document']
statements = document.get('Statement', [])
for stmt in statements:
if stmt.get('Effect') != 'Allow':
continue
actions = stmt.get('Action', [])
if isinstance(actions, str):
actions = [actions]
resources = stmt.get('Resource', [])
if isinstance(resources, str):
resources = [resources]
# Fail if Action: "*" and Resource: "*"
assert not (
'*' in actions and '*' in resources
), f"Policy {policy['PolicyName']} has admin privileges"
def test_validate_policy_with_access_analyzer(self, access_analyzer_client):
"""Use Access Analyzer to validate policy."""
policy_document = {
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
}]
}
response = access_analyzer_client.validate_policy(
policyDocument=json.dumps(policy_document),
policyType='IDENTITY_POLICY'
)
# Check for errors and security warnings
findings = response.get('findings', [])
errors = [f for f in findings if f['findingType'] == 'ERROR']
security_warnings = [f for f in findings
if f['findingType'] == 'SECURITY_WARNING']
assert len(errors) == 0, f"Policy validation errors: {errors}"
assert len(security_warnings) == 0, f"Security warnings: {security_warnings}"
IAM Policy Simulator Testing
def test_policy_denies_unauthorized_actions(iam_client):
"""Simulate policy to verify denied actions."""
# Test that developer role cannot access production
response = iam_client.simulate_principal_policy(
PolicySourceArn='arn:aws:iam::123456789012:role/DeveloperRole',
ActionNames=[
'ec2:TerminateInstances',
's3:DeleteBucket',
'iam:CreateUser'
],
ResourceArns=[
'arn:aws:ec2:us-east-1:123456789012:instance/*',
'arn:aws:s3:::production-*',
'arn:aws:iam::123456789012:user/*'
]
)
for result in response['EvaluationResults']:
assert result['EvalDecision'] == 'implicitDeny', \
f"Developer role should not have {result['EvalActionName']} permission"
def test_policy_allows_required_actions(iam_client):
"""Verify role has required permissions."""
response = iam_client.simulate_principal_policy(
PolicySourceArn='arn:aws:iam::123456789012:role/CICDRole',
ActionNames=[
's3:PutObject',
'ecr:PushImage',
'ecs:UpdateService'
],
ResourceArns=[
'arn:aws:s3:::deployment-artifacts/*',
'arn:aws:ecr:us-east-1:123456789012:repository/my-app',
'arn:aws:ecs:us-east-1:123456789012:service/my-cluster/my-service'
]
)
for result in response['EvaluationResults']:
assert result['EvalDecision'] == 'allowed', \
f"CICD role missing required permission: {result['EvalActionName']}"
Azure IAM Testing
Azure Policy for RBAC Validation
# List custom role definitions
az role definition list --custom-role-only true --output table
# Check role assignments
az role assignment list --scope /subscriptions/<sub-id> --output table
# Validate with Azure Policy
az policy assignment list --query "[?policyDefinitionId contains 'rbac']"
InSpec for Azure RBAC
control 'azure-no-owner-custom-roles' do
impact 1.0
title 'Custom roles should not have Owner-equivalent permissions'
azure_role_definitions.where(role_type: 'CustomRole').ids.each do |role_id|
describe azure_role_definition(name: role_id) do
its('permissions.first.actions') { should_not include '*' }
its('permissions.first.not_actions') { should_not be_empty }
end
end
end
control 'azure-no-subscription-owner-assignments' do
impact 1.0
title 'Owner role should not be assigned at subscription level'
describe azure_role_assignments.where(
scope: "/subscriptions/#{input('subscription_id')}",
role_name: 'Owner'
) do
its('count') { should be <= 2 } # Allow only break-glass accounts
end
end
GCP IAM Testing
GCP Policy Analyzer
# Analyze IAM policy for a project
gcloud policy-intelligence analyze-iam-policy \
--project=my-project \
--full-resource-name="//cloudresourcemanager.googleapis.com/projects/my-project"
# Check for overly permissive bindings
gcloud projects get-iam-policy my-project --format=json | \
jq '.bindings[] | select(.members[] | contains("allUsers") or contains("allAuthenticatedUsers"))'
Terraform Validation for GCP IAM
# variables.tf - Define allowed roles
variable "allowed_project_roles" {
type = list(string)
default = [
"roles/viewer",
"roles/editor",
# Explicitly list allowed roles
]
}
# Validation in resource
resource "google_project_iam_binding" "binding" {
project = var.project_id
role = var.role
members = var.members
lifecycle {
precondition {
condition = contains(var.allowed_project_roles, var.role)
error_message = "Role ${var.role} is not in the allowed roles list."
}
precondition {
condition = !contains(var.members, "allUsers") && !contains(var.members, "allAuthenticatedUsers")
error_message = "Public IAM bindings (allUsers/allAuthenticatedUsers) are not allowed."
}
}
}
CI/CD Pipeline Integration
Complete GitHub Actions Workflow
name: IAM Policy Security
on:
pull_request:
paths:
- 'terraform/iam/**'
- 'policies/**'
jobs:
static-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Checkov IAM checks
uses: bridgecrewio/checkov-action@v12
with:
directory: terraform/iam/
check: CKV_AWS_1,CKV_AWS_40,CKV_AWS_49,CKV_AWS_109,CKV_AWS_110
output_format: sarif
output_file_path: checkov-iam.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: checkov-iam.sarif
access-analyzer:
runs-on: ubuntu-latest
needs: static-analysis
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Terraform Init
run: terraform init
working-directory: terraform/iam/
- name: Validate with Access Analyzer
uses: aws-actions/terraform-aws-iam-policy-validator@v1
with:
template-path: terraform/iam/
region: us-east-1
check-no-new-access: true
policy-simulation:
runs-on: ubuntu-latest
needs: access-analyzer
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: pip install boto3 pytest
- name: Run policy simulation tests
run: pytest tests/iam/ -v --tb=short
Measuring Success
| Metric | Before Testing | After Testing | How to Track |
|---|---|---|---|
| Overly permissive policies | Found in audits | 0 in production | Checkov reports |
| Privilege escalation paths | Unknown | 0 detected | Access Analyzer findings |
| Policy validation time | Manual review (hours) | Automated (minutes) | CI/CD metrics |
| Unused permissions | Accumulated over time | Quarterly cleanup | Access Analyzer |
Warning signs your IAM testing isn’t working:
- Checkov passing but Access Analyzer findings in production
- “Emergency” policy changes bypassing CI/CD
- Roles accumulating permissions over time
- Service accounts with admin privileges
Conclusion
Effective IAM policy testing requires multiple validation layers:
- Static analysis (Checkov) catches known bad patterns in code
- Grammar validation (Access Analyzer ValidatePolicy) ensures policy syntax and structure
- Policy simulation (IAM Policy Simulator) verifies actual permission behavior
- Continuous analysis (Access Analyzer findings) detects drift and unused permissions
The key insight: IAM policies are too critical for manual review alone. Automated testing in CI/CD pipelines catches issues before they reach production, while continuous monitoring detects policy drift over time.
Official Resources
See Also
- Policy as Code Testing - OPA and Sentinel for custom IAM rules
- Security Group Testing - Network-level access control testing
- Compliance Testing for IaC - Meeting regulatory IAM requirements
- Terraform Testing Strategies - Complete IaC testing pyramid
- AWS Infrastructure Testing - Broader AWS testing strategies
