Policy Enforcement Cheat Sheet: Terraform & OpenTofu
This cheat sheet provides a quick start guide to implementing policy enforcement for your Terraform and OpenTofu projects using Open Policy Agent (OPA) and Conftest.
1. Why Policy as Code (PaC)?
- Consistency: Ensure uniform deployments.
- Security: Prevent misconfigurations and vulnerabilities.
- Compliance: Adhere to internal and external standards.
- Cost Control: Avoid budget overruns by restricting expensive resources.
2. Core Tools & Setup
- Terraform/OpenTofu: Your IaC tool.
- Open Policy Agent (OPA): The policy engine.
- Installation: Refer to OPA official documentation.
- Conftest: Tool to test Terraform plans against OPA policies.
- Installation: Refer to Conftest official documentation.
- (Optional) Infracost: For cost estimation policies.
- Installation: Refer to Infracost official documentation.
3. Writing Your First Rego Policy
Rego policies define what is allowed or denied. Store them in a dedicated directory (e.g., ./policies
).
Basic Structure (.rego
file):
package <your_policy_namespace> # e.g., terraform.aws.s3
import input.resource_changes # Accesses the list of resource changes from the plan JSON
# 'deny' rules generate messages if conditions are met (violations)
deny[msg] {
# Condition 1
# Condition 2
# ...
msg := sprintf("Violation message for resource %s: %s", [resource.address, "reason"])
}
# 'warn' rules (if supported by your test runner setup) generate warnings
warn[msg] {
# Conditions for warning
msg := sprintf("Warning for resource %s: %s", [resource.address, "advice"])
}
Example: Ensure AWS S3 Buckets are Private (./policies/aws/s3_acl.rego
)
package terraform.aws.s3_acl
import input.resource_changes
# Deny if an S3 bucket is being created or updated with a public ACL
deny[msg] {
resource := resource_changes[_] # Iterate over each resource change
resource.type == "aws_s3_bucket"
resource.mode == "managed" # Ensure it's a resource Terraform manages
# Apply to 'create' or 'update' actions
action := resource.change.actions[_]
actions_to_check := {"create", "update"}
actions_to_check[action]
acl := object.get(resource.change.after, "acl", "") # Safely get ACL attribute
public_acls := {"public-read", "public-read-write", "authenticated-read"}
public_acls[acl] # True if the acl is one of the defined public ACLs
msg := sprintf("S3 bucket '%s' must not have a public ACL. Found: '%s'.", [resource.address, acl])
}
4. Generating Terraform Plan as JSON
OPA and Conftest evaluate the JSON representation of your Terraform plan.
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > plan.json
5. Testing with Conftest
Run Conftest from your project root to test plan.json
against policies in your ./policies
directory.
conftest test plan.json --policy ./policies --all-namespaces
--policy ./policies
: Path to your Rego policy files.--all-namespaces
: Tells Conftest to find policies regardless of theirpackage
declaration if your query is generic (e.g., just looking fordeny
orwarn
). Alternatively, specify a namespace with-n <namespace>
.
Interpreting Output: Conftest will list failing policies and their messages. Modify Terraform code or policies until checks pass.
6. Common Resource Policy Patterns (Rego Snippets)
Access resource attributes via resource.change.after.<attribute>
.
Mandatory Tags:
mandatory_tag_keys := {"Owner", "Environment"}
current_tags := object.get(resource.change.after, "tags", {})
missing_key := mandatory_tag_keys[_]
not current_tags[missing_key]
msg := sprintf("Resource '%s' is missing mandatory tag: '%s'.", [resource.address, missing_key])
Region/Location Restrictions (e.g., Azure):
azure_allowed_locations := {"uksouth", "westeurope"}
location := resource.change.after.location
not azure_allowed_locations[location]
msg := sprintf("Disallowed Azure location '%s' for resource '%s'.", [location, resource.address])
Resource Type/Size Restrictions (e.g., AWS EC2):
allowed_instance_types := {"t3.micro", "t3.small"}
instance_type := resource.change.after.instance_type
not allowed_instance_types[instance_type]
msg := sprintf("Disallowed EC2 instance type '%s' for resource '%s'.", [instance_type, resource.address])
Naming Conventions:
# Deny if resource name doesn't start with "prod-"
name := resource.change.after.name
not startswith(name, "prod-")
msg := sprintf("Resource '%s' name ('%s') must start with 'prod-'.", [resource.address, name])
7. Cost Policies (Infracost + OPA)
- Generate Infracost JSON:
infracost breakdown --path . --format json --out-file infracost.json
(For PRs, useinfracost diff --path . --compare-to infracost-base.json --format json --out-file infracost-diff.json
) - Test with Conftest:
conftest test infracost.json --policy ./policies -N infracost
(Orconftest test infracost-diff.json ...
)
Write Rego Policy (e.g., ./policies/common/cost.rego
):
package infracost # Policies for Infracost data must be in this package
# Deny if total monthly cost difference exceeds $100
deny[out] {
max_total_monthly_cost_diff := 100.00
actual_diff := to_number(input.diffTotalMonthlyCost) # Input is Infracost JSON
actual_diff > max_total_monthly_cost_diff
msg := sprintf("Total monthly cost difference ($%.2f) exceeds limit of $%.2f.", [actual_diff, max_total_monthly_cost_diff])
out := {"msg": msg, "failed": true} # Infracost expects this output structure
}
8. CI/CD Integration (Conceptual Workflow)
Integrate these checks into your CI/CD pipeline (e.g., GitHub Actions, GitLab CI):
- Checkout code.
- Setup Terraform, Conftest, (Infracost if used).
- Run
terraform init
. - Run
terraform validate
. - Run
terraform plan -out=tfplan.binary
. - Run
terraform show -json tfplan.binary > plan.json
. - (If using Infracost) Run
infracost breakdown ... > infracost.json
. - Run
conftest test plan.json ...
(andconftest test infracost.json ...
if applicable). - Fail pipeline if Conftest reports violations.
9. When to Consider Scalr
If your needs evolve to require:
- Centralized policy management and versioning.
- Automated pre-plan and post-plan checks integrated into a platform.
- Role-Based Access Control (RBAC) for IaC operations.
- Comprehensive audit trails.
- Integrated cost estimation and policy enforcement. ...then explore a platform like Scalr.
10. Key Commands & Resources
- OPA CLI:
opa eval --data policy_file.rego --input input.json "data.your_policy_namespace"
opa test ./policies/
opa run
(REPL)
- Conftest:
conftest test <input_file.json> --policy <policy_dir> [--all-namespaces | -n <namespace>]
- Documentation:
- Open Policy Agent:
openpolicyagent.org/docs
- Conftest:
conftest.dev
- Infracost:
infracost.io/docs
- Scalr:
docs.scalr.com
- Terraform:
developer.hashicorp.com/terraform/docs
- OpenTofu:
opentofu.org/docs
- Open Policy Agent: