Plan Policies
A plan policy checks the plan of a tracked or proposed run on the stacks it is attached to, once the plan has succeeded and before anything is applied. It can deny a plan, which fails the run, or warn about it, which leaves the decision to whoever approves the run. Policies are written in Rego and evaluated by Zenfra; there is nothing to install.
Can this run still proceed?
Open the run and look at the Policy panel.
- Denied by policy, run Failed: no. The run page reads Blocked by policy, and View findings jumps to the reasons. Nothing on this run can be approved or re-run. Read the findings and check which policies apply on the stack's Policies tab; change the configuration, or ask an Admin to change the policy, then start a new run.
- Policy not evaluated, run Awaiting Approval: not yet. The evaluation did not complete, and the panel says why. Fix the cause, then use Re-evaluate (Write or Admin). Approve only once the verdict is Policy passed or Policy warnings.
- Policy warnings: yes. The findings are advisory; read them before you approve.
- Policy passed: yes.
Re-evaluate does not fix everything. It evaluates the plan and the reports that were saved with the run against the policies attached and enabled now. It does not run the plan or any hook again, it cannot recreate a report that was never saved, and it never makes a Failed run approvable. When the plan or a report is missing, start a new run.
Editing, disabling or detaching a policy changes nothing on a run that was already evaluated, until that run is re-evaluated. A new run always uses the policies as they are when it is evaluated.
What a policy is
A policy is a Rego v1 module in package zenfra with at least one of two rules:
deny contains msg if { … }: each message denies the plan;warn contains msg if { … }: each message is an advisory finding.
package zenfra
deny contains msg if {
some rc in input.terraform.resource_changes
rc.type == "aws_db_instance"
"delete" in rc.change.actions
msg := sprintf("%s would be deleted", [rc.address])
}
warn contains msg if {
input.zenfra.run.delta.total > 20
msg := sprintf("this plan changes %d resources", [input.zenfra.run.delta.total])
}
A replacement counts as a delete here, because its actions include delete. Helper rules and functions are allowed. deny and warn must be declared exactly in the form above, and each message must be a string. The document a policy reads, input, is described in Policy Input: the run, the stack and its labels, the commit, the Terraform or OpenTofu plan with sensitive values replaced, and reports your hooks hand over.
A policy is a pure function of its input. Builtins that reach outside it are not available: http.send, net.lookup_ip_addr, opa.runtime, time.now_ns, rand.intn, uuid.rfc4122, io.jwt.encode_sign, io.jwt.encode_sign_raw, io.jwt.decode_verify, json.verify_schema and json.match_schema. Every other builtin of OPA 1.21.1 is.
The source compiles when you save. A policy that does not compile is refused with the line and column of each problem.
Where policies are managed
Policies belong to the organization. Open Organization → Policies in the sidebar, or Policies in the organization settings. The list shows each policy's name, slug, whether it is enabled, and what it is attached to.
- Create policy opens the editor with Start from a template already open; Use template fills the source, and the name and description unless you wrote your own. Templates in the editor opens the picker again. Then Create policy saves it. The slug is made from the name and cannot change later.
- Edit in a policy's row menu opens it; Save compiles and stores the new source. The change applies from the next evaluation of every attached stack.
- The switch in the Enabled column turns a policy off or on. A disabled policy stays attached and is not evaluated.
- Delete is available only once the policy is detached from everything. Runs already evaluated keep their verdict.
- Dry-run in the editor evaluates the draft against a run without saving anything; see Dry-run.
| Role | Policies |
|---|---|
| Read | View policies, their attachments, a stack's policies, a run's verdict and its policy input, and the templates |
| Write | Re-evaluate a run; approve, discard and cancel runs as before |
| Admin | Create, edit, enable, disable, attach, detach and delete policies; dry-run a draft |
Every change to a policy, every attachment and detachment, every dry-run and re-evaluation, and every approval refused by a policy is recorded in the audit log.
Attachments and the stack's effective set
Attachments on a policy's page lists where it applies; Attach adds a Stack or a Space, and Detach removes one.
- A stack attachment covers that stack.
- A space attachment covers every stack in the space and in every space beneath it, whatever the spaces' bundle inheritance setting says.
A stack's effective policies are the enabled policies attached to it, to its space, or to any space above it, each counted once. The stack's Policies tab lists them, with where each comes from: This stack, Space <name>, or Inherited from space <name>. A policy that applies through a space is changed by an Admin on Organization → Policies, not from the stack. When none apply, the tab reads "No policies apply to this stack."
When a run is evaluated
A tracked or proposed run is evaluated once, when its plan has finished successfully and its plan hooks have run, before it leaves Planning. Evaluation is part of the run: there is no separate step to start.
- A run whose plan failed or was cancelled is not evaluated.
- A destroy run is never evaluated.
- A run on a stack with no effective policy records a pass without evaluating anything, and continues as it would without policies.
The verdict decides what happens next:
| Run | Policy passed | Policy warnings | Policy not evaluated | Denied by policy |
|---|---|---|---|---|
| Tracked, with changes | Awaiting Approval | Awaiting Approval, findings shown | Awaiting Approval; cannot be approved until a re-evaluation passes | Failed |
| Tracked, no changes | Finished | Finished, findings shown | Finished, the error recorded | Failed |
| Proposed | Finished | Finished, findings shown | Finished, the error recorded | Failed |
A deny fails the run with policy_failed and releases the stack's lock, so the next run can start. Policy not evaluated means the evaluation did not complete; Failures you may see lists the causes. If the time budget runs out after a deny was already found, the verdict is still a deny.
The run page and approval
The Policy panel on the run page shows the verdict, the number of findings by severity (high for a deny, low for a warning), and the findings: Severity, Resource, Message and Rule, where the rule is the policy's slug. Denies are listed first. "Checked
When an evaluation was interrupted, the panel says "Evaluation did not complete" with the reason; on a deny it adds that "The denial above stands; other policies may not have run."
Confirm appears on a run awaiting approval once the verdict allows approval, and opens Approve & Apply. A denied run is Failed, so it has no Confirm. While the evaluation has not completed, did not complete, or is being redone, the run's approval row says why in place of Confirm and links to the Policy panel. An Approve & Apply dialog that is already open follows the verdict and says why while approval is blocked. If an approval request is refused because the evaluation is incomplete, the dialog shows Not yet approvable; if it is refused because a policy denied the plan, it shows Denied by policy and the findings.
Discard still works on a run you will not approve.
Re-evaluation
Re-evaluate in the Policy panel, behind a Re-evaluate policies confirmation, evaluates a run again. It is offered to Write and Admin users on a run that has not ended and is either awaiting approval or has the verdict Policy not evaluated.
- It reads the plan and the hook reports saved with the run, and the policies attached and enabled now.
- It does not run the plan or any hook again.
- The only status it changes is Awaiting Approval to Failed, when the policies now deny the plan; the stack's lock is then released. A run that has ended keeps its status; its new verdict is informational.
- It is refused while the run is still being planned ("Wait for planning to finish, then try again."), and when something else moved the run first, such as another re-evaluation or an approval ("Something else moved this run first; showing its latest state.").
Templates
Start from a template offers five policies to start from. A template makes an independent copy: editing the copy is expected, and a later change to the catalog never changes a policy made from it. Zenfra does not record which template a policy came from. To check whether a policy's source is still identical to a template, compare the policy's source_sha256 with the template's.
| Template | What it does | What to edit |
|---|---|---|
Do not delete stateful resources (no-delete-stateful, the default) |
Denies deleting or replacing a resource of a listed type: aws_db_instance, aws_rds_cluster, aws_dynamodb_table, aws_efs_file_system, aws_s3_bucket, google_sql_database_instance, azurerm_mssql_database, azurerm_postgresql_flexible_server |
stateful_types |
Require owner and env tags (require-tags) |
Denies a taggable AWS resource whose owner or env tag is missing, blank or not a string; warns when a tag is not known until apply, or is sensitive |
taggable_types, required_tags |
Change budget (change-budget) |
Denies a proposed run, and warns on a tracked run, that creates, updates or deletes more than 25 managed resources | threshold |
Deny scanner failed checks (scanner-failed-checks) |
Denies each failed check in a Checkov report, written as {"reports": [...]} with Checkov's JSON output as the entries, and a report that is not in that shape |
report |
Require a scanner report (scanner-report-required) |
Denies when the scanner report did not arrive intact | report |
Attach the two scanner templates together: one denies findings, the other denies a missing report. A hook can supply the report; see Custom inputs.
Limits
| Limit | Value |
|---|---|
| Policy source | 256 KiB |
| Attachments per policy | 200 |
| Effective policies per stack | 50 |
| Combined source of a stack's effective policies | 1 MiB |
| Time to evaluate a run | 15 seconds |
| Findings stored per run | 500; the totals still count every finding |
| Length of a finding's message | 1024 bytes, after the automatic redaction of values that look like secrets |
Attaching is not refused when a stack goes over the policy or source limit. Instead, every run of that stack whose plan would be evaluated gets Policy not evaluated, until an Admin detaches or disables policies, or shortens their source, so the stack is back within both limits. The limits on what a policy reads are in Policy Input.
Failures you may see
| Code or message | Where | Meaning |
|---|---|---|
policy_failed |
Run Failed | A policy denied the plan. Read the findings; start a new run once the configuration or the policy is changed. An approval sent while a re-evaluation denies the plan is refused with the same code. |
approval_gates_pending |
Approval refused | The evaluation has not completed, did not complete, or is being redone. Wait, or fix the cause and Re-evaluate. |
policy does not compile |
Saving a policy | The source has errors; each comes with its line and column. A deny or warn that is not declared as deny contains msg if { … } is reported at that rule; a policy with neither rule is reported at its package line. |
policy_attached |
Deleting a policy | Detach it from every stack and space first. |
too_many_attachments, already_attached |
Attaching | The policy has 200 attachments already, or is already attached there. |
run_not_evaluable |
Re-evaluating | The run is still queued or being planned. |
revision_conflict |
Re-evaluating | Something else changed the run first; reload it. |
| Policy not evaluated | Run page | The evaluation did not complete. The panel names the reason: the saved plan or a hook report could not be read or failed verification, the input or the plan was over its size limit, the stack is over its policy limits, the 15-second budget ran out, the evaluator was busy, or a policy failed while running (for example, a message that is not a string). Fix the cause, then Re-evaluate. |
API equivalents
| Operation | Method and path | Minimum role |
|---|---|---|
| List policies, get one | GET /api/v1/policies, GET /api/v1/policies/{id} |
Read |
| A stack's effective policies | GET /api/v1/stacks/{stack_id}/policies |
Read |
| The templates | GET /api/v1/policy-templates, GET /api/v1/policy-templates/{slug} |
Read |
| Re-evaluate a run | POST /api/v1/runs/{run_id}/policies/evaluate |
Write |
| Create, update (including enable and disable), delete | POST /api/v1/policies, PUT /api/v1/policies/{id}, DELETE /api/v1/policies/{id} |
Admin |
| Attach, detach | POST /api/v1/policies/{id}/attachments, DELETE /api/v1/policies/{id}/attachments/{kind}/{target_id} |
Admin |
A run's verdict and findings are part of the run (GET /api/v1/runs/{run_id}). An approval refused by a policy answers 409 with the code. The policy input and dry-run endpoints are on Policy Input.