Why custom policies
Checkov and Trivy ship generic checks. But every organisation has its own rules: "every resource must have an Owner tag", "only region ap-southeast-1 is allowed", "images must come from registry.example.com". Policy as code writes those rules as code: reviewed, versioned, tested and run automatically like application code.
Open Policy Agent and Rego
OPA (a CNCF graduated project) is a general-purpose policy engine: it takes JSON input, evaluates it against policies written in Rego, and returns a decision. The same engine serves Terraform, Kubernetes (Gatekeeper), API authorisation and CI.
# policy/tags.rego
package main
import rego.v1
deny contains msg if {
some rc in input.resource_changes
rc.change.actions[_] == "create"
not rc.change.after.tags.Owner
msg := sprintf("%s is missing the Owner tag", [rc.address])
}
Read it as: for each resource about to be created in the plan, if it has no tags.Owner, add a message to the deny set. An empty deny set means pass.
Conftest
Conftest uses OPA to test configuration files: JSON, YAML, HCL, Dockerfiles, INI and more. By default it looks for policies in the policy/ folder and rules named deny, violation and warn.
terraform show -json tfplan > tfplan.json
conftest test tfplan.json # uses the default policy/
conftest test -p policy/ k8s/deployment.yaml
conftest test --all-namespaces Dockerfile
conftest verify -p policy/ # run the policy unit tests
FAIL - tfplan.json - main - aws_s3_bucket.logs is missing the Owner tag
2 tests, 1 passed, 0 warnings, 1 failure
flowchart LR TF["Terraform plan JSON"] --> CT["conftest test"] K8["Kubernetes YAML"] --> CT POL["policy/*.rego<br/>reviewed like code"] --> CT CT -- "empty deny" --> PASS["Pass"] CT -- "deny found" --> FAIL["Fail the pipeline"]
Reusing it at runtime
The same Rego policies can run in OPA Gatekeeper as a Kubernetes admission controller, so rules in CI and in the cluster agree. Alternatives include Kyverno (YAML policies, next module), HashiCorp Sentinel in Terraform Cloud, Azure Policy and AWS SCPs.
Good practice
- Write tests for policies (
*_test.rego) and runconftest verifyin CI. - Start in
warnmode, measure violations, then switch todeny. - Make error messages name the resource and the fix.
Traps: writing policies against the .tf code structure instead of the plan, so modules and variables slip past; policies without tests, where a small change makes them always pass.