Skip to content

Policy Engine

The policy engine enables custom policy enforcement using OPA (Open Policy Agent) and Rego.

Overview

The policy engine allows you to define and enforce organizational policies on your Terraform configurations using the powerful Rego policy language.

Usage

# Run policy checks
terratidy policy

# With custom policy directory
terratidy policy --policy-dir ./policies

# Show policy input (for debugging)
terratidy policy --show-input

Configuration

engines:
  policy:
    enabled: true
    config:
      policy_dirs:
        - ./policies
        - ~/.terratidy/policies
      policy_files:
        - ./custom-policy.rego

Writing Policies

Policies are written in Rego (v1 syntax) and evaluated against a JSON representation of your Terraform modules. TerraTidy uses OPA v1.15.0 with Rego v1, which requires the import rego.v1 statement and updated rule syntax.

Basic Policy Structure

package terraform

import rego.v1

# Deny rule - creates an error
deny contains msg if {
    some resource in input.resources
    resource.type == "aws_s3_bucket"
    not resource.versioning
    msg := {
        "msg": sprintf("S3 bucket %s must have versioning enabled", [resource.name]),
        "rule": "s3-versioning-required",
        "severity": "error",
        "file": resource._file
    }
}

# Warn rule - creates a warning
warn contains msg if {
    some resource in input.resources
    resource.type == "aws_instance"
    not resource.tags
    msg := {
        "msg": sprintf("EC2 instance %s should have tags", [resource.name]),
        "rule": "required-tags",
        "severity": "warning",
        "file": resource._file
    }
}

Key Rego v1 Syntax Changes

  • Add import rego.v1 at the top of every policy file
  • Use deny contains msg if { ... } instead of deny[msg] { ... }
  • Use some resource in input.resources instead of resource := input.resources[_]

Input Structure

The policy engine provides the following input structure:

{
  "resources": [...],
  "data": [...],
  "modules": [...],
  "variables": [...],
  "outputs": [...],
  "locals": [...],
  "providers": [...],
  "terraform": {...},
  "_files": [...]
}

Each resource/block includes:

  • type: The resource type (e.g., "aws_instance")
  • name: The resource name
  • _block_type: The HCL block type (resource, data, module, etc.)
  • _file: Source file path where the block is defined
  • _range: Location object with start_line, end_line, start_column, end_column
  • All attributes as key-value pairs (raw expression text)

The _range field is useful for precise error reporting in custom policies:

msg := {
    "msg": "Issue description",
    "rule": "my-rule",
    "severity": "error",
    "file": resource._file,
    "line": resource._range.start_line
}

Built-in Policies

TerraTidy includes several built-in policies:

Policy Description
required-terraform-block Terraform block must exist
required-version required_version must be specified
required-providers Providers must have version constraints
no-public-ssh Security groups cannot allow SSH from 0.0.0.0/0
no-public-s3 S3 buckets cannot have public-read ACL
no-public-rds RDS instances cannot be publicly accessible
required-tags Resources should have tags
module-version External modules should have version constraints

Example Policies

Require Encryption

package terraform

import rego.v1

deny contains msg if {
    some resource in input.resources
    resource.type == "aws_ebs_volume"
    resource.encrypted != "true"
    msg := {
        "msg": sprintf("EBS volume %s must be encrypted", [resource.name]),
        "rule": "ebs-encryption",
        "severity": "error",
        "file": resource._file
    }
}

Naming Convention

package terraform

import rego.v1

deny contains msg if {
    some resource in input.resources
    not regex.match("^[a-z][a-z0-9_]*$", resource.name)
    msg := {
        "msg": sprintf("Resource %s.%s must use snake_case naming", [resource.type, resource.name]),
        "rule": "naming-convention",
        "severity": "warning",
        "file": resource._file
    }
}

Cost Control

package terraform

import rego.v1

expensive_types := {"aws_instance", "aws_db_instance", "aws_elasticache_cluster"}

warn contains msg if {
    some resource in input.resources
    resource.type in expensive_types
    not resource.tags.CostCenter
    msg := {
        "msg": sprintf("%s %s should have a CostCenter tag", [resource.type, resource.name]),
        "rule": "cost-center-tag",
        "severity": "warning",
        "file": resource._file
    }
}

Debugging Policies

Use the --show-input flag to see the JSON input:

terratidy policy --show-input > input.json

Then test your policy with OPA directly:

opa eval --input input.json --data policies/ "data.terraform.deny"