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 TypeToolWhen to RunWhat It Catches
Grammar validationIAM Access AnalyzerPre-commit, CISyntax errors, deprecated elements
Best practicesAccess Analyzer ValidatePolicyCI pipelineOverly permissive actions, missing conditions
Static analysisCheckov, cfn-nagPre-commit, CIKnown bad patterns (Resource: “*”)
Policy simulationIAM Policy SimulatorPre-deployActual permission behavior
Access analysisAccess Analyzer findingsContinuousExternal access, unused permissions
Custom rulesOPA/RegoCI pipelineOrganization-specific requirements

Critical IAM Policy Checks

Check IDDescriptionRisk Level
CKV_AWS_1IAM policies should not allow full “*” administrative privilegesCritical
CKV_AWS_49IAM policies should not allow PassRole to “*”Critical
CKV_AWS_40IAM policies should not allow privilege escalationCritical
CKV_AWS_62IAM policies should not have empty SIDLow
CKV_AWS_109IAM policies should not allow credentials exposureHigh
CKV_AWS_110IAM policies should not allow permissions management without constraintsHigh

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

MetricBefore TestingAfter TestingHow to Track
Overly permissive policiesFound in audits0 in productionCheckov reports
Privilege escalation pathsUnknown0 detectedAccess Analyzer findings
Policy validation timeManual review (hours)Automated (minutes)CI/CD metrics
Unused permissionsAccumulated over timeQuarterly cleanupAccess 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:

  1. Static analysis (Checkov) catches known bad patterns in code
  2. Grammar validation (Access Analyzer ValidatePolicy) ensures policy syntax and structure
  3. Policy simulation (IAM Policy Simulator) verifies actual permission behavior
  4. 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