In this episode we'll discuss Policy as Code (PaC) for OpenTofu using the Open Policy Agent (OPA) and the Rego language. We'll extract the plan output to JSON with tofu show, then test compliance rules such as forbidding port 22 from being opened to the public and forbidding the creation of storage without encryption.

In the previous episode 15 we covered Secret Management & State Hardening, from client-side state encryption to HashiCorp Vault and AWS Secrets Manager integration. However, securing data alone isn't enough. The big question: how do we make sure every infrastructure change stays compliant with company policy before it's actually applied to the cloud?
Manually reviewing every plan on every Pull Request won't scale as teams and resource counts keep growing. In this episode 16, we'll discuss Policy as Code (PaC) using Open Policy Agent (OPA) with the Rego language. We'll turn an OpenTofu plan into JSON with the tofu show -json plan.tfplan command, then test compliance rules like forbidding port 22 from being opened to the public and forbidding unencrypted storage.
Policy as Code is the practice of writing compliance rules as code that can be version-controlled, reviewed via Pull Requests, and tested automatically — exactly like ordinary application code. Instead of checking policy through PDF documents or manual checklists, policy is written in a declarative language that machines evaluate.
For OpenTofu, the object under test is the plan file. Think of a plan as a complete résumé of all changes to come: which resources are created, modified, or destroyed. If we can extract that résumé in a structured format, we can check every line automatically before applying.
There are three common times to validate policy:
The third point is the most ideal. OPA evaluates the plan JSON and answers one simple question: may this change proceed, or must it be rejected? It's like an airport security officer checking baggage before the plane takes off, not after it lands.
By default, tofu plan only displays output in the terminal. To inspect the plan with OPA, we first save it to a binary file:
tofu plan -out=plan.tfplan
tofu show -json plan.tfplan > plan.jsonThe tofu plan -out=plan.tfplan command saves the complete plan result to a binary file, then tofu show -json plan.tfplan > plan.json converts it into a machine-readable JSON document. That plan.json file becomes the input for OPA.
The plan JSON structure follows the state file format. The most important part for policy is resource_changes, an array listing the resources that change along with their actions:
{
"resource_changes": [
{
"address": "aws_security_group_rule.ssh",
"change": {
"actions": ["create"],
"after": {
"from_port": 22,
"cidr_blocks": ["0.0.0.0/0"]
}
}
}
]
}Note the three keys we'll use in the Rego rules: address (resource identity), actions (create, update, or delete), and after (attribute values after the change). Our rules will filter this array and report every violation.
Rego is OPA's policy language. We start with core rules: deny every security group rule opening port 22 to all IPs, and deny unencrypted storage.
package infra.security
import rego.v1
default allow := false
allow if {
count(deny) == 0
}
deny contains msg if {
some res in input.resource_changes
res.type == "aws_security_group_rule"
res.change.after.from_port == 22
res.change.after.cidr_blocks[_] == "0.0.0.0/0"
msg := sprintf("resource %s opens port 22 to the public", [res.address])
}
deny contains msg if {
some res in input.resource_changes
res.type == "aws_ebs_volume"
res.change.after.encrypted != true
msg := sprintf("resource %s is not encrypted", [res.address])
}The first rule checks whether any new aws_security_group_rule opens port 22 to 0.0.0.0/0. The second rejects an aws_ebs_volume whose encrypted attribute isn't true. If the deny collection is empty, allow is true and the change is allowed to proceed.
Tip
Rules can be expanded endlessly: forbid an aws_s3_bucket without server_side_encryption_configuration, forbid an IAM policy with wildcard actions, or require an aws_db_instance without publicly_accessible. Each one is just another deny contains msg if block.
With the policy and plan JSON in hand, we evaluate whether any violations exist:
opa eval --data policy/security.rego --input plan.json "data.infra.security.deny"If the rules produce a violation, the output shows a message like resource aws_security_group_rule.ssh opens port 22 to the public. To use it as a pipeline gate, we need a clear exit code — OPA provides the --fail-defined flag, which returns exit code 1 if the requested rule evaluates to defined:
opa eval --data policy/security.rego --input plan.json \
--fail-defined "data.infra.security.deny"Exit code 1 means a violation exists, so the pipeline can stop immediately. Also tidy up the Rego code with opa fmt for consistency, just like tofu fmt does for HCL.
We already built the GitHub Actions pipeline in episode 13. The OPA gate just gets inserted as one step before apply:
tofu plan -out=plan.tfplan
tofu show -json plan.tfplan > plan.json
opa eval --data policy/ --input plan.json \
--fail-defined "data.infra.security.deny"
tofu apply plan.tfplanThe flow becomes: the PR is reviewed by humans, a plan is produced, OPA checks compliance automatically, and only if all rules pass are the changes applied. Policy that once relied on memory and individual awareness is now an automatic defense line working on every change.
Warning
Plan JSON doesn't contain values marked sensitive, so there's no leak risk through pipeline artifacts. But make sure plan.json isn't committed to the repository — add it to .gitignore, because its contents can be large and are an internal infrastructure snapshot.
In episode 16, we discussed Policy as Code with OPA comprehensively. We learned how to save a plan to a file, extract it to JSON, write Rego rules, evaluate them with OPA, and integrate it as a compliance gate in the pipeline.
Key takeaways:
tofu plan -out=plan.tfplan saves the plan, tofu show -json plan.tfplan > plan.json turns it into JSON.resource_changes section contains addresses, actions, and post-change attributes.deny contains msg if rule for every policy violation.--fail-defined flag turns OPA's evaluation result into an exit code for pipelines.tofu plan and tofu apply.With a policy gate running automatically, your infrastructure isn't just possible to create — it's only allowed to be created when it meets company standards. In the next episode, episode 17, we'll combine all OpenTofu capabilities to provision multi-cloud infrastructure spanning AWS, GCP, and Kubernetes at once. See you there!