Tuesday, 26 August 2025

Centralize OPA policy

OPA Policy Validation for Multi-Resource Terraform Plan (Mock)

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