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/
Select a user/role to simulate
Choose the service (EC2, S3, RDS, etc.)
Select actions to test (RunInstances, CreateBucket, etc.)
Add resource ARNs for resource-level conditions
Run simulation — see Allow/Deny results per action
#
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
}
}
#
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}'
#
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 = "*"
}]
})
}
#
# ✅ 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 #
Result Meaning Action allowed Policy explicitly grants access ✅ Good implicitDeny No policy grants access Add permission to policy explicitDeny An SCP or boundary denies access Check SCPs, permission boundaries
#
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
Related Articles #
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.