Policy-as-code & scanning
tfsec, Checkov, OPA/Sentinel gates.
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.
$ checkov -d .Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public access"FAILED for resource: aws_s3_bucket.dataFile: /main.tf:12-18Passed 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.
package terraformdeny[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 -