TerraformPilot

Cloud Computing

AWS IAM Policy Simulator with Terraform: Test Permissions Before Deploying

Use the AWS IAM Policy Simulator — also known as an IAM policy tester — to validate Terraform policies before apply and catch AccessDenied errors.

LLuca Berton2 min read

What Is the AWS IAM Policy Simulator?

#

The AWS IAM Policy Simulator — often called an IAM policy tester — is a tool that tests whether IAM policies grant or deny access to specific API actions — without actually executing them. Think of it as a dry-run for IAM permissions.

When combined with Terraform, you can:

  • Validate IAM policies before terraform apply
  • Debug AccessDenied errors in Terraform deployments
  • Ensure least-privilege policies actually work
  • Test cross-account roles and resource-based policies

Why You Need This

#

Common Terraform IAM problems:

Error: error creating EC2 Instance: UnauthorizedOperation:
  You are not authorized to perform this operation.

Instead of deploying → failing → editing → redeploying, use the Policy Simulator to test first.

Using the Policy Simulator Console

#

Navigate to: https://policysim.aws.amazon.com/

  1. Select a user/role to simulate
  2. Choose the service (EC2, S3, RDS, etc.)
  3. Select actions to test (RunInstances, CreateBucket, etc.)
  4. Add resource ARNs for resource-level conditions
  5. Run simulation — see Allow/Deny results per action

Automating Policy Testing with Terraform

#

Method 1: AWS CLI in null_resource

#

Test policies as part of your Terraform workflow:

variable "role_name" {
  default = "terraform-deploy-role"
}
 
# Get the role ARN
data "aws_iam_role" "deploy" {
  name = var.role_name
}
 
# Simulate required permissions after creating the policy
resource "null_resource" "policy_test" {
  depends_on = [aws_iam_role_policy.deploy]
 
  provisioner "local-exec" {
    command = <<-EOF
      aws iam simulate-principal-policy \
        --policy-source-arn ${data.aws_iam_role.deploy.arn} \
        --action-names \
          ec2:RunInstances \
          ec2:TerminateInstances \
          ec2:DescribeInstances \
          s3:CreateBucket \
          s3:PutObject \
          rds:CreateDBInstance \
        --output table
    EOF
  }
}

Method 2: Terraform Test Framework

#

Use terraform test (Terraform 1.6+) to validate policies:

Create tests/iam_policy.tftest.hcl:

run "verify_deploy_role_permissions" {
  command = plan
 
  assert {
    condition     = aws_iam_role_policy.deploy.policy != ""
    error_message = "Deploy role policy must not be empty"
  }
}
 
run "simulate_ec2_access" {
  command = apply
 
  module {
    source = "./tests/modules/policy-simulator"
  }
 
  assert {
    condition     = output.ec2_allowed == true
    error_message = "Deploy role must have EC2 access"
  }
}

Method 3: External Data Source

#
data "external" "policy_check" {
  program = ["bash", "${path.module}/scripts/check-policy.sh"]
 
  query = {
    role_arn = aws_iam_role.deploy.arn
    actions  = "ec2:RunInstances,s3:PutObject,rds:CreateDBInstance"
  }
}
 
output "policy_simulation_result" {
  value = data.external.policy_check.result
}

scripts/check-policy.sh:

#!/bin/bash
set -e
 
# Read input from Terraform
eval "$(jq -r '@sh "ROLE_ARN=\(.role_arn) ACTIONS=\(.actions)"')"
 
# Convert comma-separated actions to space-separated
IFS=',' read -ra ACTION_ARRAY <<< "$ACTIONS"
 
# Run simulation
RESULT=$(aws iam simulate-principal-policy \
  --policy-source-arn "$ROLE_ARN" \
  --action-names "${ACTION_ARRAY[@]}" \
  --query 'EvaluationResults[?EvalDecision!=`allowed`].{Action:EvalActionName,Decision:EvalDecision}' \
  --output json)
 
# Count denied actions
DENIED_COUNT=$(echo "$RESULT" | jq 'length')
 
# Return result to Terraform
jq -n --arg denied "$DENIED_COUNT" --arg details "$RESULT" \
  '{"denied_count": $denied, "details": $details}'

Writing Least-Privilege Policies for Terraform

#

Bad: Overly Permissive

#
# ❌ Never do this
resource "aws_iam_role_policy" "deploy" {
  name = "deploy-policy"
  role = aws_iam_role.deploy.id
 
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = "*"
      Resource = "*"
    }]
  })
}

Good: Scoped to What Terraform Actually Needs

#
# ✅ Least privilege for a typical web app deployment
resource "aws_iam_role_policy" "deploy" {
  name = "terraform-deploy-policy"
  role = aws_iam_role.deploy.id
 
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "EC2Management"
        Effect = "Allow"
        Action = [
          "ec2:RunInstances",
          "ec2:TerminateInstances",
          "ec2:DescribeInstances",
          "ec2:DescribeImages",
          "ec2:DescribeSecurityGroups",
          "ec2:CreateSecurityGroup",
          "ec2:DeleteSecurityGroup",
          "ec2:AuthorizeSecurityGroupIngress",
          "ec2:RevokeSecurityGroupIngress",
          "ec2:CreateTags",
          "ec2:DescribeVpcs",
          "ec2:DescribeSubnets",
        ]
        Resource = "*"
        Condition = {
          StringEquals = {
            "aws:RequestedRegion" = ["us-east-1", "eu-west-1"]
          }
        }
      },
      {
        Sid    = "S3BucketManagement"
        Effect = "Allow"
        Action = [
          "s3:CreateBucket",
          "s3:DeleteBucket",
          "s3:PutBucketVersioning",
          "s3:PutEncryptionConfiguration",
          "s3:PutBucketPublicAccessBlock",
          "s3:GetBucketLocation",
          "s3:ListBucket",
        ]
        Resource = "arn:aws:s3:::myapp-*"
      },
      {
        Sid    = "TerraformStateAccess"
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:PutObject",
          "s3:DeleteObject",
        ]
        Resource = "arn:aws:s3:::my-terraform-state/*"
      },
      {
        Sid    = "StateLocking"
        Effect = "Allow"
        Action = [
          "dynamodb:GetItem",
          "dynamodb:PutItem",
          "dynamodb:DeleteItem",
        ]
        Resource = "arn:aws:dynamodb:us-east-1:*:table/terraform-locks"
      }
    ]
  })
}

Simulating Cross-Account Access

#

Test whether an assumed role in another account has the right permissions:

# Simulate a cross-account role
aws iam simulate-principal-policy \
  --policy-source-arn "arn:aws:iam::987654321098:role/CrossAccountTerraform" \
  --action-names s3:GetObject s3:PutObject \
  --resource-arns "arn:aws:s3:::shared-bucket/*" \
  --output table

CI/CD Integration: Pre-Apply Permission Check

#

Add to your GitHub Actions or GitLab CI pipeline:

# .github/workflows/terraform.yml
- name: Validate IAM Permissions
  run: |
    DENIED=$(aws iam simulate-principal-policy \
      --policy-source-arn ${{ secrets.TERRAFORM_ROLE_ARN }} \
      --action-names ec2:RunInstances s3:CreateBucket rds:CreateDBInstance \
      --query 'EvaluationResults[?EvalDecision!=`allowed`].EvalActionName' \
      --output text)
    
    if [ -n "$DENIED" ]; then
      echo "❌ Missing permissions: $DENIED"
      exit 1
    fi
    echo "✅ All required permissions available"

Common Policy Simulator Results

#
ResultMeaningAction
allowedPolicy explicitly grants access✅ Good
implicitDenyNo policy grants accessAdd permission to policy
explicitDenyAn SCP or boundary denies accessCheck SCPs, permission boundaries

Debugging AccessDenied in Terraform

#

When terraform apply fails with AccessDenied:

# 1. Find which action failed (check the error message)
# Error: creating EC2 Instance: UnauthorizedOperation
 
# 2. Simulate that specific action
aws iam simulate-principal-policy \
  --policy-source-arn $(aws sts get-caller-identity --query Arn --output text) \
  --action-names ec2:RunInstances \
  --resource-arns "arn:aws:ec2:us-east-1:*:instance/*" \
  --output json | jq '.EvaluationResults[] | {Action: .EvalActionName, Decision: .EvalDecision, MatchedStatements: .MatchedStatements}'
 
# 3. Check if SCPs are blocking
aws organizations list-policies-for-target \
  --target-id $(aws sts get-caller-identity --query Account --output text) \
  --filter SERVICE_CONTROL_POLICY
#

Hands-On Courses

#

Conclusion

#

The AWS IAM Policy Simulator prevents the frustrating deploy-fail-fix cycle. Integrate it into your Terraform workflow — either via CLI scripts, external data sources, or CI/CD gates — to validate permissions before applying changes. Combined with least-privilege policies, you'll have secure deployments that work on the first try.

#AWS#IAM#Terraform#Security#Policy Simulator

Share this article