Policy-as-code & scanning

tfsec, Checkov, OPA/Sentinel gates.

Advanced14 min · lesson 13 of 15

A plan can be perfectly valid Terraform and still create a public S3 bucket, an open security group, or an unencrypted database. Policy-as-code and scanning are the guardrails that catch those before apply — automated checks that fail a plan violating your rules, so security is enforced by the pipeline rather than by hoping a reviewer notices. There are two overlapping families: static scanners and policy engines.

Static scanners: fast, opinionated

Checkov, Trivy (which absorbed tfsec), Terrascan, and KICS scan your HCL (and often the plan) against large built-in libraries of misconfiguration checks — public exposure, missing encryption, over-broad IAM, absent logging. They need no rules to start (hundreds ship in the box), run in seconds, and slot straight into CI. Run one on every change and fail the build on the severities you would actually block, exactly like image scanning.

terminal
$ checkov -d .
Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public access"
FAILED for resource: aws_s3_bucket.data
File: /main.tf:12-18
Passed checks: 41, Failed checks: 1, Skipped checks: 0
$ trivy config . # tfsec-style checks, now part of Trivy

Policy engines: your rules, enforced

Where scanners bring a fixed library, policy engines let you write your own rules. OPA with Conftest evaluates Rego policies against the plan (exported as JSON); HashiCorp Sentinel is the equivalent in Terraform Cloud/Enterprise. This is how you encode organization-specific mandates — every resource must carry a cost-center tag, only approved instance types, no security group open to 0.0.0.0/0 — as code that gates the pipeline.

policy/deny_public.rego
package terraform
deny[msg] {
r := input.resource_changes[_]
r.type == "aws_security_group_rule"
r.change.after.cidr_blocks[_] == "0.0.0.0/0"
msg := sprintf("%s allows 0.0.0.0/0", [r.address])
}
# terraform plan -out=tf.plan && terraform show -json tf.plan | conftest test -
Scan the plan, and give suppressions an expiry
Scanning HCL statically is good; evaluating the JSON plan is better, because it sees computed values and the actual change set. And when you must suppress a finding (a real exception), require a justification and an expiry date on the suppression — permanent silent skips are how scanners quietly stop meaning anything. A suppression with no expiry is a hole nobody will ever revisit.