Policy, security, and cost gates¶
Every plan, and every drift plan, runs three checks against the Terraform plan. Findings appear in the PR run comment and in drift issues, under "Additional output". That panel is status-aware: collapsed with a check mark when every gate is clean, and auto-expanded with a warning when a gate denies, warns, errors, or the run fails, so a finding is never left hidden behind a click.
What runs¶
| Gate | Tool | Checks | Config |
|---|---|---|---|
| Policy | conftest | policy/*.rego against the plan JSON |
$TFPR_ENGINE_DIR/scripts/conftest-gate.sh |
| Security | checkov | built-in checks against the plan JSON | $TFPR_ENGINE_DIR/scripts/checkov-gate.sh |
| Cost | infracost | monthly cost of the plan | $TFPR_ENGINE_DIR/scripts/infracost-gate.sh |
The gate scripts live in the engine, not in the customer repo. The post_plan
hooks in projects.yml run them from $TFPR_ENGINE_DIR, the engine checkout the
reusable workflow provides (or the workspace in vendored mode).
conftest and checkov are baked into the self-hosted runner image, so there
TFPR_EXTRA_TOOLS only adds infracost. On a GitHub-hosted posture, set
TFPR_EXTRA_TOOLS to conftest checkov infracost to install all three per run.
infracost is a static binary in either case.
Report-only by default¶
All three gates start advisory, so they surface findings without blocking a merge:
- conftest runs
warnrules only. Warnings do not gate. The reference policy surfaces any resource that will be destroyed. - checkov runs with soft-fail, so it reports but never fails the build.
- infracost is informational. The hook always exits zero and never gates.
This lets you turn the gates on against an existing tenant without a wall of findings blocking every PR on day one.
Enforcing a gate¶
Enforce deliberately, after reviewing the report-only findings.
- conftest. Add a
denyrule topolicy/. A deny fails the gate and blocks the PR. The starterexamples/policy/tags.regoshows the two load-bearing plan-JSON patterns:change.actionsis an array (test membership, never index zero), andchange.afteris null on a destroy (guard it, or the deny fails open). - checkov. Drop the soft-fail from the checkov hook in
projects.yml, or scope it withCHECKOV_HARD_FAIL_ON(check IDs) once findings are triaged. - infracost. Cost stays informational. Use it to review the monthly delta a PR introduces.
Writing a policy¶
conftest policies are Rego in package main. A warn rule surfaces a finding
without gating; a deny rule blocks. Run against terraform show -json, so you
match on the plan structure:
package main
import rego.v1
deny contains msg if {
resource := input.resource_changes[_]
resource.type == "azurerm_storage_account"
"create" in resource.change.actions
not resource.change.after.tags.owner
msg := sprintf("%s must have an owner tag", [resource.address])
}
Cost gate data residency¶
infracost sends per-resource region and SKU to its hosted pricing API to look up prices. The Terraform code itself does not leave. This is an accepted trade-off: the self-hosted pricing API that would avoid the lookup is behind a paid Infracost plan.
Use the legacy infracost line (the static-key model). Get a static API key from the
Infracost dashboard, set it as the INFRACOST_API_KEY secret, and add infracost
to TFPR_EXTRA_TOOLS. Until the key is set, the gate notes it and skips.