Terraform Professional (002): Policy Rules, Overrides and Risky Changes
Terraform policy governance is the practice of encoding compliance rules as Sentinel or OPA policies in HCP Terraform policy sets, scoping them so organization-wide rules and team or project rules each reach the right workspaces, and using those policies plus workspace settings to catch and contain risky infrastructure changes before they apply. This lesson covers objectives 5d and 5e of the Terraform Infrastructure Engineer Professional (002) exam: establishing core compliance rules alongside team and project policies, and identifying and mitigating risky changes such as deletions, replacements, destroy runs and targeted applies. Secrets handling and pipeline identities are covered in the companion lesson on secrets and CI identities.
On this page10 sections
- Policy as code in HCP Terraform: where the rules live
- Scoping policy sets: core rules versus team and project rules
- Excluding projects, workspaces or tags from a policy set
- Enforcement levels and who can override them
- Policy checks versus policy evaluations
- Writing and organising policy sets: sentinel.hcl and policies.hcl
- Choosing the right data for a rule: tfconfig, tfplan, tfstate, tfrun
- Rolling out a new rule without surprising teams
- Identifying risky changes in plan and run data
- Mitigating risky changes: layered protection and blast radius
- Scope policy sets globally, by project, by workspace or by tag, and use exclusions for approved exemptions without losing coverage of future workspaces
- Choose Sentinel and OPA enforcement levels and explain who can override a failure, how, and for how long
- Distinguish Sentinel policy checks from policy evaluations, including cost data, runtime pinning and the set-level override option
- Structure policy sets with sentinel.hcl policy and module blocks, supply policy set parameters, and write OPA policies.hcl queries over input.plan and input.run
- Pick tfconfig/v2, tfplan/v2, tfstate/v2 or tfrun data for a rule and avoid the unknown-value, reference and null-after traps
- Detect deletions, replacements, destroy runs, targeted runs and auto-apply in plan and run data
- Layer workspace settings, policy, provider deletion protection and workspace splitting to mitigate risky changes
Policy as code in HCP Terraform: where the rules live
Policy as code in HCP Terraform means writing compliance rules as Sentinel or OPA (Rego) policies, grouping them into policy sets that the organization owns, and letting HCP Terraform evaluate them on every run in the workspaces each set covers. A policy set holds policies from one framework only, and the framework cannot be changed after the set is created. (HashiCorp has also introduced a native HCL-based framework, Terraform policy, for newer Terraform versions; the governance model below applies to it too, but this lesson uses Sentinel and OPA.)
Where policies run in the run lifecycle
Policies evaluate after a successful plan and before apply. The order is plan → (cost estimation, for Sentinel policy checks) → policy stage → apply. A run whose plan errors ends in Plan Errored: it never reaches the policy stage and can never apply, so a missing policy result on an errored run does not mean the policies were bypassed.
Why a policy set, and not a rule in the code
Terraform's own guardrails — variable validation, precondition/postcondition, check blocks and lifecycle settings — are part of the configuration. Whoever controls the code controls the rule: a team that copies a module can delete its validation, and a check block only warns. A policy set is managed at organization level by teams holding Manage policies, scoped by the platform team and evaluated on every run regardless of where the configuration came from. That is what makes a rule unbypassable.
| Mechanism | Who controls it | Good for |
|---|---|---|
| Variable validation, preconditions, check blocks | Whoever edits the configuration | Module contracts, early feedback for honest mistakes |
| Policy set (Sentinel / OPA) | Platform team with Manage policies, at organization level | Organization and team compliance rules expressed against plan, config, state or run data |
| Run task | Attached per workspace (workspace admins can manage workspace run tasks) or organization-wide | Calling an external service such as a commercial security scanner; mandatory enforcement makes a failed task stop the run |
Rule of thumb: rules you can express directly over Terraform data belong in a policy set; results that come from a third-party tool arrive through a run task (set to mandatory if a failure must stop the run). A run task attached in a workspace's own settings can be removed by that workspace's admins, so it is the wrong home for a control the workspace owners must not weaken. Run task stages and setup are covered in the CI/CD lesson.
Scoping policy sets: core rules versus team and project rules
Core compliance rules go in a globally scoped policy set; team or project rules go in additional sets scoped to just those workspaces. A policy set can apply globally (every workspace in the organization, including ones created later), to specific projects, to specific workspaces or Stacks, or to workspaces that carry particular tags.
| Scope | Covers | Future workspaces | Use it when |
|---|---|---|---|
| Global | Every workspace in the organization | Yes, automatically | Organization-wide baseline (encryption, required tags) |
| Project | Workspaces currently in the project | Yes, any workspace added to the project | A team that owns a project needs extra rules (for example data-residency rules for an analytics project) |
| Workspace / Stack | Only the named workspaces or Stacks | No — the list goes stale | A rule must follow one specific workspace wherever it moves |
| Workspace tags | Any workspace carrying the tag, in any project | Yes, as soon as a workspace is tagged | The target group cuts across projects (every workspace tagged regulated) |
Sets add up; they never relax each other
Every policy set whose scope includes a workspace is evaluated for its runs, independently. A project-scoped set cannot weaken a global one: if the global set fails a hard-mandatory policy, a project set holding the same rule at advisory level changes nothing — the global failure still stops the run. To exempt something, change the scope of the set that is failing it (see exclusions below).
Scope is resolved from where the workspace is now
Project scope follows current membership. Move a workspace out of a project and that project's sets stop applying on its next run, while global sets keep applying; nothing is carried over and nothing needs detaching. If a rule must follow a workspace regardless of project, scope the set to the workspace (or a tag) instead.
Enforcement levels and who can override them
An enforcement level decides what a policy failure does to the run: advisory only reports it, the middle level stops the run but allows an authorized override, and the strictest level stops the run with no override. Sentinel and OPA use different words:
| Framework | Level | On failure | Override? |
|---|---|---|---|
| Sentinel | advisory | Warning in the run; run continues | Not needed |
| Sentinel | soft-mandatory | Run stops at the policy stage | Yes — a user with Manage policy overrides |
| Sentinel | hard-mandatory | Run stops | No (in a policy-check set); only the set-level overridable option of a policy-evaluation set changes this |
| OPA | advisory (the default when enforcement_level is omitted) | Warning; run continues | Not needed |
| OPA | mandatory | Run stops | Yes — a user with Manage policy overrides |
OPA has only two levels: soft-mandatory and hard-mandatory are Sentinel words, and mandatory is OPA's. An OPA policy with no enforcement_level is advisory, so a policy that clearly fails can still let the run apply with only a warning.
Two different permissions
- Manage policies — create, edit and delete policies and policy sets. It does not include the right to override.
- Manage policy overrides — continue runs after overridable failures in every workspace (it grants read access to workspaces for that purpose) without being able to edit policies. Give it to a change-advisory or governance team.
What an override does
Overriding is per run. On the run's page the user chooses Override and Continue; for a CLI-driven run with a soft-mandatory Sentinel failure, typing override at the apply prompt does the same. Nothing persists: the policy keeps its level, the workspace stays in scope, and the next run is evaluated again. An override never adds an exclusion or downgrades a policy.
When a hard-mandatory policy check fails
There is no override button, no CLI override and no permission that helps. The ways forward are to change the configuration so it complies, or to change the policy (or its level) in the policy set, and then queue a new run. If a run fails policies in several sets, each is judged separately and the non-overridable failure decides: overriding the OPA mandatory failure does not help while a Sentinel hard-mandatory check also fails.
Policy checks versus policy evaluations
HCP Terraform runs Sentinel in one of two modes: policy checks (the original mode) or policy evaluations (chosen by giving a Sentinel set the Agent policy set type). OPA sets always run as policy evaluations. The mode changes what a policy can read and how overrides work:
| Policy checks (Sentinel only) | Policy evaluations (Agent-type Sentinel sets, all OPA sets) | |
|---|---|---|
Cost estimation data (tfrun.cost_estimate) | Available | Not available |
| Sentinel runtime version | Latest supported only | Can be pinned, so upgrades are controlled |
| Set-level option "This policy set can be overridden in the event of mandatory failures" | Not offered | Offered; takes precedence over each policy's level, so even a hard-mandatory failure can be overridden by a user with Manage policy overrides |
| Where they run | HCP Terraform | HCP Terraform, or a self-managed agent pool (Premium, workspace on that pool, an agent accepting policy jobs) |
Two consequences are worth remembering. A budget rule built on tfrun.cost_estimate.delta_monthly_cost sees no cost data in an Agent-type set and must live in a policy-check set. And when a team needs both a pinned runtime and a cost rule, it splits them: the cost policy in a policy-check set, the rest in an Agent-type set with the pinned version. Even with the set-level option on, a failing run still stops at the policy stage first; the override is still per run.
Writing and organising policy sets: sentinel.hcl and policies.hcl
A VCS-connected Sentinel policy set is a repository directory containing sentinel.hcl, which declares every policy and shared module the set uses. A .sentinel file that no policy block declares is ignored, even after HCP Terraform picks up the commit. VCS-connected sets update when the tracked branch changes; there is nothing to re-create or re-attach.
policy "restrict-regions" {
source = "./restrict-regions.sentinel"
enforcement_level = "soft-mandatory"
}
module "helpers" {
source = "./modules/helpers.sentinel"
}
policyblocks — one per policy, withsourceandenforcement_level. Local policy files sit in the same directory assentinel.hcland are referenced by a relative path such as./restrict-regions.sentinel. Remote sources must be HTTP or HTTPS URLs that return the raw policy text, such as the public registry's policy URLs; a URL that returns a web page displaying the file, rather than the file itself, fails, and non-HTTP source schemes are not accepted.moduleblocks — shared helper code (for example "find resources of a type, skip deletes"), local or HTTP(S). A policy uses it withimport "helpers", so every policy that needs it shares one maintained copy. Declaring a helper as apolicywould evaluate it instead of sharing it.- Pre-written policies — HashiCorp and partners publish ready-made Sentinel policy libraries (for example CIS benchmark controls for the major clouds) in the public registry. Reference them by URL from a
policyblock at any enforcement level to get maintained standard checks without writing them.
Parameters: one policy, different values per set
A Sentinel policy file can declare param values with defaults (a param statement inside the .sentinel policy, not a block in sentinel.hcl). HCP Terraform supplies policy set parameters — key/value pairs stored on the policy set — to that set's Sentinel policies, and a supplied value replaces the default. Two project-scoped sets can therefore source the same policy file and give each team its own allowed list, with no hard-coded team lists in the code. Parameters keep secrets (such as an API token the policy needs) out of the repository: a parameter marked sensitive is write-once and is never displayed again. Parameters are for Sentinel only, are managed on the set (not on workspace variable pages), and hold values, not code.
OPA: policies.hcl, query and input
policy "no-public-ssh" {
query = "data.terraform.policies.no_public_ssh.deny"
enforcement_level = "mandatory"
}
Each OPA policy names a Rego rule through query (package path plus rule name). The query must return an array: empty means pass, any element means fail, so a deny rule that collects messages is the usual pattern. Leave out enforcement_level and the policy is advisory. HCP Terraform passes OPA one input document: input.plan is the Terraform JSON plan (resource_changes, planned_values, …) and input.run is run metadata such as input.run.workspace (including auto_apply), input.run.project and input.run.organization. OPA sets do not receive policy set parameters.
Choosing the right data for a rule: tfconfig, tfplan, tfstate, tfrun
Pick the data source that actually contains the fact the rule is about: configuration facts come from tfconfig/v2, planned values and actions from tfplan/v2, current real-world values from tfstate/v2, and run options and workspace metadata from tfrun.
| Import | Contains | Typical rule |
|---|---|---|
tfconfig/v2 | The configuration as written: resources, module_calls (with source and version_constraint), providers, variables. Arguments are a constant_value (a literal) or references (an expression); expressions are not evaluated | Modules must come from the private registry; providers must be pinned |
tfplan/v2 | resource_changes (each with change.actions, before, after, after_unknown) and planned_values | Tags, sizes, encryption on what will be created or updated; detecting deletes |
tfstate/v2 | The prior state | Rules about what exists today |
tfrun | Run and workspace metadata: is_destroy, target_addrs, replace_addrs, speculative, refresh, workspace tags, variables (names, category and sensitivity only — not values), and cost estimate (policy checks only) | Forbid destroy runs or targeted runs in production |
Traps that make correct configurations fail
- References in tfconfig. A module that sets
tags = var.tagshas noconstant_valuefortags, only a reference, so a tfconfig rule looking for a literalownerkey fails every resource the module builds. Value-based rules belong onchange.afterintfplan/v2. - Unknown values in planned_values.
planned_valuesis an estimate of the resulting state and omits values not known until apply. An instance whosesubnet_idreferences a subnet created in the same run looks as if the subnet is missing. Readresource_changesand treat an attribute marked inchange.after_unknownas set. - Deletes have no
after. For a delete,change.afteris null, so a negated lookup such asnot r.change.after.tags["cost-center"]is true and the rule reports the resource being removed.resource_changesalso lists no-op resources. Rules about desired values should first filter to actions that include"create"or"update".
Rolling out a new rule without surprising teams
Introduce a new rule at advisory level across its full scope, use the warnings to find and fix the affected workspaces, then raise the level. Advisory policies are evaluated on every run and their failures appear in run results, but they never stop a run — so the platform team gets an evidence list of who would be blocked without blocking anyone.
- Advisory, at full scope. Trying the rule on one pilot workspace says nothing about how the rest of the estate would be affected.
- Remediate. Owning teams fix their code within an agreed window.
- Enforce with an exception path. Move to
soft-mandatory(Sentinel policy check) ormandatory(OPA) and give a named change board Manage policy overrides, so new violations stop while approved exceptions can continue run by run. - Harden. Raise to
hard-mandatoryonce no exceptions should remain.
Excluding the workspaces that are known to violate a rule removes enforcement exactly where it is needed; starting at hard-mandatory blocks teams before anyone knows the impact.
Identifying risky changes in plan and run data
A risky infrastructure change is one that can destroy data or take down a shared dependency: a delete, a replacement, a destroy run, a partial (targeted) apply, or an unattended auto-apply in production. Policies identify them from the plan's actions and the run's metadata.
Change actions in the JSON plan
| What Terraform plans | change.actions |
|---|---|
| Create | ["create"] |
| Update in place | ["update"] |
| Delete (block removed, or destroy plan) | ["delete"] |
| Replace (default: destroy then create) | ["delete", "create"] |
Replace with create_before_destroy | ["create", "delete"] |
| No change / read | ["no-op"] / ["read"] |
A rule that compares actions is ["delete"] misses both replacement orders. Test membership instead:
deletes = filter tfplan.resource_changes as _, rc {
rc.type is "aws_db_instance" and
rc.change.actions contains "delete"
}
main = rule { length(deletes) is 0 }
That catches removed blocks, destroy plans and every forced replacement, while letting in-place updates, ignore_changes edits and address changes declared with moved blocks through — a moved block re-addresses the existing object rather than deleting it, which is why it is the safe way to rename.
Run metadata
tfrun.is_destroy— true when the run's plan is a destroy plan, whether queued from the UI, the API orterraform destroy. It does not catch ordinary runs that delete objects (removed blocks, forced replacement,-replace); pair it with aresource_changesrule.tfrun.target_addrs— the addresses of a targeted run (null when none). Requiring it to be null or empty forbids partial applies that can leave a stack half-updated.tfrun.replace_addrs,speculative,refresh,refresh_only— other run options; each answers a different question.- In OPA,
input.run.workspace.auto_applytogether withinput.run.project.namelets a mandatory deny rule refuse auto-apply in the production project.
Mitigating risky changes: layered protection and blast radius
No single control stops every way a critical resource can be lost, so production data stores get layers: workspace settings, plan-time policy, provider-side protection and a workspace layout that limits what any one run can touch.
| Control | Stops | Does not stop |
|---|---|---|
| Allow destroy plans off (workspace destruction settings) | Destroy plans from the UI, API or CLI | Ordinary plans that delete: removed blocks, forced replacements |
lifecycle { prevent_destroy = true } | Any plan destroying the object while the block is in the configuration (replacements and destroy runs) | Removal of the resource block or its module call — the setting is deleted with it |
Mandatory policy failing "delete" actions on the resource | Removed blocks, forced replacements and destroy runs, independent of the code | Changes made outside Terraform |
| Provider deletion protection argument (for example a database's deletion protection) | The cloud API refuses the delete whatever Terraform plans | Nothing Terraform can plan — the apply fails at the API |
A mandatory delete-blocking policy and provider deletion protection each cover all three accidents on their own; together they give defence in depth. ignore_changes never prevents deletion, and create_before_destroy still deletes the old object.
Workspace deletion
Deleting a workspace is different from destroying its resources. By default a workspace admin can delete only an unlocked workspace that manages no resources. An organization owner can force delete, which removes the workspace and its state but leaves the cloud resources running, now unmanaged. The safe order is a reviewed destroy run first, then deletion.
Limit the blast radius
A run can change only what is in its workspace's state. Splitting one large configuration into network, data and application workspaces (sharing values such as subnet IDs through outputs) means a routine application change cannot plan a network or database change at all, which needs far less review than catching such changes with policy. Moving code into modules does not help here: modules called from one workspace still share its state.
Tip. Expect production governance scenarios with a stated constraint — cover future workspaces, exempt one group without losing coverage, block but allow approved exceptions, keep one copy of policy code, no surprise blocking — where every option is a real scope, exclusion, enforcement level, permission, import or setting that fails only on that constraint. Items show sentinel.hcl, policies.hcl, Sentinel or Rego snippets and ask why a run continued or failed, or which condition to use. Near-twins appear side by side: soft-mandatory versus hard-mandatory versus OPA mandatory, Manage policies versus Manage policy overrides, policy checks versus policy evaluations, project versus workspace versus tag scope, resource_changes versus planned_values, the two replacement action orders, and tfrun.is_destroy versus delete actions in the plan. Harder items combine two facts, such as two failing sets with different override rules, or which protections each cover removed blocks, replacements and destroy runs.
- Organization baselines go in a globally scoped policy set; team rules go in extra sets scoped to a project, workspace or tag.
- Every policy set in scope is evaluated independently, so a team's own set can never relax a global rule; exemptions are exclusions on the set that fails.
- Exclude the unit that matches the exempt group: a workspace, a project, or a tag that cuts across projects.
- Sentinel levels are advisory, soft-mandatory and hard-mandatory; OPA has advisory (the default) and mandatory.
- Manage policy overrides lets a team continue overridable failures run by run; Manage policies is for authoring and does not grant overrides.
- A hard-mandatory policy-check failure has no override: fix the code or the policy and queue a new run.
- Policy evaluations (Agent-type Sentinel, all OPA) allow runtime pinning and a set-level override option but cannot read cost estimates.
- Policies run after a successful plan and before apply; an errored plan never reaches them and never applies.
- Test change.actions contains "delete" to catch deletes and both replacement orders; tfrun.is_destroy only sees destroy plans.
- prevent_destroy disappears with its resource block; pair a mandatory delete policy with provider deletion protection, and split workspaces to shrink the blast radius.
Frequently asked questions
What is the difference between Sentinel soft-mandatory and hard-mandatory?
Both stop an HCP Terraform run when the Sentinel policy fails. A soft-mandatory failure can be overridden for that one run by a user with the Manage policy overrides permission, using Override and Continue or typing override at the CLI apply prompt. A hard-mandatory failure in a policy-check set cannot be overridden; the configuration or the policy must change and a new run must be queued.
Can a project-level policy set weaken a global policy set in HCP Terraform?
No. HCP Terraform evaluates every policy set whose scope includes the workspace, independently, and any mandatory failure stops the run. To exempt a project, workspace or tagged group from a global rule, add an exclusion to the global policy set and, if needed, scope a narrower replacement rule to the exempted area.
What enforcement levels does OPA support in HCP Terraform?
OPA policies in HCP Terraform use advisory or mandatory, set with enforcement_level in policies.hcl. If enforcement_level is omitted the policy is advisory, so a failure only warns. A mandatory failure stops the run, and a user with Manage policy overrides can override it for that run.
Why can't my Sentinel policy read cost estimation data?
Cost estimation data is available only to Sentinel policies that run as policy checks. If the policy set uses the Agent policy set type, its policies run as policy evaluations, which cannot access cost estimates. Move the cost rule into a policy-check set and keep other policies in the Agent-type set if they need a pinned Sentinel runtime.
How do I detect a resource replacement in a Terraform policy?
In the JSON plan, a replacement appears in resource_changes as two actions: ["delete", "create"] by default, or ["create", "delete"] when create_before_destroy is set. Testing that change.actions contains "delete" catches both replacement orders as well as plain deletions, while letting in-place updates and moved-block renames through.
Does turning off Allow destroy plans protect a production database?
Only partly. With Allow destroy plans turned off, HCP Terraform refuses destroy plans from the UI, API or CLI, but ordinary plans can still delete the database if its resource block is removed or an argument forces replacement. A mandatory policy that fails delete actions on the database, plus the provider's deletion protection argument, covers those cases.
What happens when an organization owner force-deletes an HCP Terraform workspace?
Force delete removes the workspace and its state but does not touch the cloud resources, which keep running without Terraform managing them. By default workspace admins can delete only unlocked workspaces that manage no resources. To remove infrastructure safely, run a reviewed destroy first and delete the workspace afterwards.
How can one Sentinel policy enforce different values for different teams?
Declare a param in the Sentinel policy and set its value as a policy set parameter on each team's policy set. Both sets can source the same policy file, and each supplies its own list, which replaces the param default. Parameters marked sensitive are write-once and never shown again; parameters apply to Sentinel policies only, not OPA.
Source
This lesson covers the "Terraform security and governance" domain of the official Terraform Pro 002 exam guide. Vendors revise their guides — check the source for the current version.
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.