You skim the tfpolicy announcement, see an HCL block that denies an unencrypted EBS volume, and file it mentally next to Sentinel. Another plan checker. Nothing that changes your Monday.
The part worth a second read sits further down the page, in a table of evaluation stages. One of them runs before Terraform downloads anything.
Terraform and OpenTofu
Compliance.tf works with both Terraform and OpenTofu. This article uses "Terraform" for brevity, but the concepts apply equally to OpenTofu users.
What HashiCorp Shipped
On July 16, 2026, HashiCorp announced Terraform policy, with a CLI named tfpolicy, in public beta inside HCP Terraform and Terraform Enterprise. Policies are written in HCL in files ending .policy.hcl, tested locally, then committed to version control and grouped into policy sets.
There are three block types: resource_policy, module_policy, and provider_policy.
There are three evaluation stages:
- setup, before providers and modules are downloaded
- plan, before any change is made
- apply, after provisioning, where computed values are visible
Enforcement levels include advisory, which reports without blocking, and mandatory_overridable, where someone with the right permission can override a failure. You read attributes through attrs., previous state through prior_attrs., and Terraform metadata through meta.. Functions in a core:: namespace can look up provider data sources and cross-resource relationships while the policy runs.
Here is the minimal example from HashiCorp's documentation:
resource_policy "aws_ebs_volume" "ebs_encrypted" { enforce { condition = attrs.encrypted == true error_message = "EBS volume is not encrypted" }}That block is what most coverage quoted. It is also the least surprising thing in the release.
This is a public beta
Everything above comes from HashiCorp's announcement and the Terraform policy docs. Syntax and behavior can still change before general availability. Check the current docs before you write policies against it.
The Stage Is the News, Not the Syntax
Every policy tool a platform team already runs reads a plan. Sentinel evaluates plan JSON. OPA evaluates plan JSON. Checkov and Trivy read Terraform files or a plan file. The input is always the same thing: what Terraform means to build, worked out after every module is fetched and every variable resolved.
The setup stage does not have that input. It runs before the download. And HashiCorp shipped a block type built for it: module_policy.
Give module sources their own block type, run it before Terraform fetches anything, and you have treated the source line as a decision the org makes rather than a dependency each developer picks. Platform teams have argued that for years while hand-rolling allow-lists in CI. Now it ships in the product, and you can check that in HashiCorp's docs instead of taking anyone's word for it.
What a Policy Can See Before the Download
Work out what a policy has to go on at setup stage.
Nothing has been fetched. No resource graph, no attribute to compare against, no computed value. Just a source address and a version constraint:
module "web" { source = "terraform-aws-modules/ec2-instance/aws" version = "6.1.4"}A setup-stage policy can allow that string or deny it. It cannot inspect what the module does, because the module is not there yet.
That is not a gap to work around. It is what the stage is. And it means that at setup, a policy decides a compliance outcome by picking a name. Every control the module enforces, and every control it leaves to the caller, was settled the moment that name passed the allow-list.
Plan-time policy asks whether a configuration is allowed. Setup-time policy asks whether a module is allowed. The second question is only as good as the set of answers you can say yes to.
Two Ways to Get an Encrypted Volume
Take HashiCorp's own example and follow it to the end of the run.
A developer writes Terraform using a general-purpose module that leaves encryption to the caller. Terraform builds the plan. The resource_policy above evaluates attrs.encrypted == true, finds false, and stops the run with "EBS volume is not encrypted". The developer now works out which argument controls encryption, in which module, at which nesting level, sets it, and runs again.
The run ends red, and the fix is homework.
Now point the allow-list somewhere else. Same module, same version, different host:
module "web" { source = "soc2.compliance.tf/terraform-aws-modules/ec2-instance/aws" version = "6.1.4"}That is the whole migration for this module. Compliance.tf republishes terraform-aws-modules with controls added inside, so the inputs and outputs you already use keep working and you pin versions the same way you do now. Do it for one module in one environment and leave the rest alone.
The EC2 instance module enforces attached EBS volume encryption through its own defaults and input validation rather than through a caller remembering an argument. Same requirement, same resource_policy still watching.
That one control does a lot of audit work. It maps to SOC 2 CC6 and CC7, PCI DSS v4.0 Requirement 3.4, HIPAA 45 CFR 164.312(a)(2)(iv), ISO/IEC 27001:2022 A.8.24, GDPR Article 32, and NIST SP 800-53 Rev 5 SC-28, along with other frameworks. A resource_policy checking attrs.encrypted does the same technical job. What it does not carry is the link from that check to the clause your auditor asks about. That link lives in your head, or in a spreadsheet.
The run ends green, and nobody wrote a fix.
Keep writing that resource_policy. It is your independent check that the module did what it claims, and it covers resources built outside your approved modules. The gate is worth having. It is just that what sits behind the gate decides whether the gate ever fires. We made the longer version of this argument in Make Non-Compliant Terraform Impossible.
What Each Layer Answers
Once you separate the stages, it gets easier to see what does what.
| Layer | Question it answers | What it cannot do |
|---|---|---|
module_policy at setup | Is this module source allowed here? | See anything about what the module does |
| The module itself | What gets enforced once it is allowed? | Govern resources written outside it |
resource_policy at plan | Does this configuration violate a rule? | Supply the compliant configuration |
| Policy at apply | What did the computed values turn out to be? | Prevent the change, it already happened |
That last row bites hard with this control. You cannot encrypt an EBS volume in place. You take an encrypted snapshot, build a new volume from it, detach the old one, and attach the new one, which means instance downtime, and for a root volume the instance has to be stopped. Catching it at plan costs a developer an afternoon. Catching it after apply costs a maintenance window. Never creating it costs nothing.
Point the allow-list at modules that already carry controls and you need fewer plan-time rules re-checking things the module could have gotten right on its own. For the plan-stage half of this comparison, see compliance.tf vs OPA and Sentinel.
The obvious alternative is to build the catalog yourself: a private module library, wrappers around terraform-aws-modules, and an allow-list pointing at your own registry. That works, and plenty of teams run it. The cost shows up later, in who keeps the controls current as frameworks change and the upstream modules move. We walk through that trade-off in Why Enterprises Choose Compliance.tf Over terraform-aws-modules.
Where This Does Not Reach
Three limits worth stating plainly.
-
tfpolicy runs in HCP Terraform and Terraform Enterprise. If your runs happen in Spacelift, env0, Scalr, Atlantis, or a plain OpenTofu pipeline, you get none of this. The module you pick is still a compliance decision there. There is just no setup stage enforcing it.
-
Compliance.tf covers the modules in its catalog, not every resource in your plan. Conditional logic, per-workspace exceptions, tagging standards, region limits, and budget rules are what a policy engine is for. Compliance.tf does not replace one, and a team running tfpolicy should keep running it.
-
tfpolicy is in public beta. Anything you build against it now should expect churn.
Getting Started
If you are already writing tfpolicy policies, do this first. Write down the module sources your module_policy would allow today, then open each one and check what it enforces on its own. Whatever that list does not enforce is what your plan-stage rules have to catch for the rest of the project's life.
Then try the other path on a single module. Start a free trial, change one source line, and run terraform plan. You will see the controls the module enforces and the framework clauses each one maps to. The same one-line change reverts it if it does not fit.
If you want more detail first: the module catalog, the controls those modules enforce, and Technical Usage for setup.
Explore framework-specific controls
See how compliance.tf maps controls for SOC 2, HIPAA, PCI DSS, NIST 800-53, and 30+ other frameworks.
Continue the conversation
Discuss this post with the community or share it with your network.
Next Step
