OPA Policy Validation for Mock Terraform Plan
This document demonstrates how to validate a mock Terraform plan JSON containing multiple resources (EC2, S3, Security Group, and IAM) using Open Policy Agent (OPA). It includes JSON input, Rego policies, and both Python and PowerShell scripts.
1. Directory Structure
C:\OPA_Mock_Project\
│
├── terraform-plan.json # Mock Terraform plan JSON for all resources
└── policy\ # OPA policies
├── s3.rego
├── ec2.rego
├── iam.rego
└── sg.rego
2. Mock Terraform Plan JSON (terraform-plan.json
)
{
"format_version": "0.1",
"terraform_version": "1.13.1",
"resource_changes": {
"aws_s3_bucket.example": {
"type": "aws_s3_bucket",
"name": "example",
"change": {
"actions": ["create"],
"before": null,
"after": {
"bucket": "my-opa-test-bucket-12345",
"acl": "public-read",
"versioning": {"enabled": false},
"server_side_encryption_configuration": null
}
}
},
"aws_instance.example_ec2": {
"type": "aws_instance",
"name": "example_ec2",
"change": {
"actions": ["create"],
"before": null,
"after": {
"ami": "ami-12345678",
"instance_type": "t2.micro",
"ebs_optimized": false,
"associate_public_ip_address": true,
"iam_instance_profile": "my-ec2-role",
"vpc_security_group_ids": ["sg-12345678"],
"ebs_block_device": [
{"device_name": "/dev/sda1", "volume_size": 30, "encrypted": false},
{"device_name": "/dev/sdb", "volume_size": 50, "encrypted": true}
],
"tags": {"Environment": "Dev"}
}
}
},
"aws_security_group.example_sg": {
"type": "aws_security_group",
"name": "example_sg",
"change": {
"actions": ["create"],
"before": null,
"after": {
"ingress": [
{"from_port": 22, "to_port": 22, "protocol": "tcp", "cidr_blocks": ["0.0.0.0/0"]}
]
}
}
},
"aws_iam_role.example_role": {
"type": "aws_iam_role",
"name": "example_role",
"change": {
"actions": ["create"],
"before": null,
"after": {
"assume_role_policy": {
"Version": "2012-10-17",
"Statement": [
{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"}}
]
}
}
}
}
}
}
3. OPA Policies
S3 Policy (policy/s3.rego
)
package terraform.s3
# Disallow public-read ACL
deny contains msg if {
some resource
input.resource_changes[resource].type == "aws_s3_bucket"
input.resource_changes[resource].change.after.acl == "public-read"
msg := sprintf("Bucket %v has public-read ACL", [input.resource_changes[resource].name])
}
# Require versioning enabled
deny contains msg if {
some resource
input.resource_changes[resource].type == "aws_s3_bucket"
not input.resource_changes[resource].change.after.versioning.enabled
msg := sprintf("Bucket %v does not have versioning enabled", [input.resource_changes[resource].name])
}
# Require encryption
deny contains msg if {
some resource
input.resource_changes[resource].type == "aws_s3_bucket"
not input.resource_changes[resource].change.after.server_side_encryption_configuration
msg := sprintf("Bucket %v does not have server-side encryption", [input.resource_changes[resource].name])
}
EC2 Policy (policy/ec2.rego
)
package terraform.ec2
# Disallow t2.micro instances
deny contains msg if {
some resource
input.resource_changes[resource].type == "aws_instance"
input.resource_changes[resource].change.after.instance_type == "t2.micro"
msg := sprintf("Instance %v uses disallowed type t2.micro", [input.resource_changes[resource].name])
}
# Require EBS optimization
deny contains msg if {
some resource
input.resource_changes[resource].type == "aws_instance"
not input.resource_changes[resource].change.after.ebs_optimized
msg := sprintf("Instance %v is not EBS optimized", [input.resource_changes[resource].name])
}
# Disallow public IP
deny contains msg if {
some resource
input.resource_changes[resource].type == "aws_instance"
input.resource_changes[resource].change.after.associate_public_ip_address
msg := sprintf("Instance %v has a public IP assigned", [input.resource_changes[resource].name])
}
# EBS volumes must be encrypted
deny contains msg if {
some resource
input.resource_changes[resource].type == "aws_instance"
volume := input.resource_changes[resource].change.after.ebs_block_device[_]
not volume.encrypted
msg := sprintf("Instance %v has unencrypted volume %v", [input.resource_changes[resource].name, volume.device_name])
}
Security Group Policy (policy/sg.rego
)
package terraform.sg
# Disallow open ingress 0.0.0.0/0
deny contains msg if {
some resource
input.resource_changes[resource].type == "aws_security_group"
ingress := input.resource_changes[resource].change.after.ingress[_]
ingress.cidr_blocks[_] == "0.0.0.0/0"
msg := sprintf("Security Group %v has open ingress to 0.0.0.0/0", [input.resource_changes[resource].name])
}
IAM Policy (policy/iam.rego
)
package terraform.iam
# Require assume role policy
deny contains msg if {
some resource
input.resource_changes[resource].type == "aws_iam_role"
not input.resource_changes[resource].change.after.assume_role_policy
msg := sprintf("IAM Role %v does not have an assume role policy", [input.resource_changes[resource].name])
}
4. Python Script (opa_check.py
)
import subprocess
import json
import os
plan_file = "terraform-plan.json"
policy_dir = "policy"
rego_files = [os.path.join(policy_dir, f) for f in os.listdir(policy_dir) if f.endswith(".rego")]
cmd = ["opa", "eval", "-i", plan_file, "--format", "json", "data"]
for rego in rego_files:
cmd.extend(["-d", rego])
result = subprocess.run(cmd, capture_output=True, text=True)
opa_output = json.loads(result.stdout)
violations = []
for res in opa_output["result"]:
for expr in res["expressions"]:
if expr["value"]:
violations.extend(expr["value"])
if violations:
print("❌ Policy violations found:")
for v in violations:
print("-", v)
else:
print("✅ All policies passed.")
5. PowerShell Script (opa_check.ps1
)
$PlanFile = "C:\OPA_Mock_Project\terraform-plan.json"
$PolicyFolder = "C:\OPA_Mock_Project\policy"
$RegoFiles = Get-ChildItem -Path $PolicyFolder -Filter *.rego | ForEach-Object { $_.FullName }
$OpaCommand = @("opa", "eval", "-i", $PlanFile, "--format", "json", "data")
foreach ($rego in $RegoFiles) { $OpaCommand += @("-d", $rego) }
try {
$OpaOutputRaw = & $OpaCommand
} catch {
Write-Error "Failed to run OPA. Ensure opa.exe is in PATH."
exit 1
}
$OpaOutput = $OpaOutputRaw | ConvertFrom-Json
$Violations = @()
foreach ($res in $OpaOutput.result) {
foreach ($expr in $res.expressions) {
if ($expr.value) { $Violations += $expr.value }
}
}
if ($Violations.Count -gt 0) {
Write-Host "❌ Policy violations found:" -ForegroundColor Red
foreach ($v in $Violations) { Write-Host "- $v" -ForegroundColor Yellow }
exit 1
} else {
Write-Host "✅ All policies passed." -ForegroundColor Green
}
6. Expected Violations for This Mock Plan
- S3 bucket has
public-read
ACL - S3 bucket does not have versioning enabled
- S3 bucket does not have server-side encryption
- EC2 instance uses disallowed type
t2.micro
- EC2 instance is not EBS optimized
- EC2 instance has a public IP assigned
- EC2 instance has unencrypted volume
/dev/sda1
- Security Group has open ingress
0.0.0.0/0
No comments:
Post a Comment