Skip to content

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 warn rules 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 deny rule to policy/. A deny fails the gate and blocks the PR. The starter examples/policy/tags.rego shows the two load-bearing plan-JSON patterns: change.actions is an array (test membership, never index zero), and change.after is 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 with CHECKOV_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.