TerraformPilot

Cloud Computing

AWS IAM Policy Simulator with Terraform: Test Permissions Before Deploying

Use the AWS IAM Policy Simulator to validate Terraform IAM policies before applying. Automate permission testing with Terraform data sources and avoid AccessDenied errors.

LLuca Berton2 min read

What Is the AWS IAM Policy Simulator?

#

The AWS IAM Policy Simulator 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