Safeguard
Cloud Security

Scanning Terraform and CloudFormation before you ever run apply

tfsec merged into Trivy in 2024 and OPA hit CNCF graduated status in 2021 — here's how to scan Terraform and CloudFormation before deploy, with a working Conftest policy.

Safeguard Research Team
Research
6 min read

Every public S3 bucket, every security group open to 0.0.0.0/0, every unencrypted RDS instance in production started as a few lines of HCL or YAML that nobody stopped to question before terraform apply or a CloudFormation stack update ran. That's the argument for shifting misconfiguration scanning left, onto the plan file, before infrastructure exists to be exploited. The tooling landscape here shifted meaningfully in the last few years: Open Policy Agent reached CNCF graduated status — the foundation's highest maturity tier — on January 29, 2021, cementing Rego as the default policy language for infrastructure guardrails. tfsec, the Terraform-focused scanner acquired by Aqua Security in July 2021, was folded into Trivy starting with Aqua's February 2023 announcement, and by 2024 all new misconfiguration rule development had moved into Trivy's config subcommand, with the standalone tfsec repo left on maintenance mode. Meanwhile Checkov (from Bridgecrew, acquired by Palo Alto Networks in 2021) and Terrascan (maintained by Tenable after its Accurics acquisition) both remain active, multi-format scanners. This post walks through what each tool actually catches, how policy-as-code with OPA and Conftest fits into a pre-apply pipeline, and where a unified findings pipeline like Safeguard's fits into that picture.

What actually goes wrong in unreviewed Terraform and CloudFormation?

The recurring failure modes are boringly consistent: security groups or NACLs with ingress rules scoped to 0.0.0.0/0 on sensitive ports, S3 buckets without block_public_access settings or default encryption, RDS and EBS volumes provisioned without encryption-at-rest, IAM policies using "Action": "*" or "Resource": "*" far beyond what a role needs, and CloudTrail or VPC flow logging left disabled entirely. None of these require a code-execution bug — they're valid, syntactically correct infrastructure that simply grants more access than intended. Because Terraform and CloudFormation both compile down to a declarative plan before anything runs, every one of these patterns is visible in the source file or plan JSON without provisioning a single resource. That's what makes IaC scanning different from application SAST: the "code" being analyzed is already a structured, machine-readable description of the exact resources and permissions about to exist, so a scanner doesn't need taint tracking to know a bucket will be public — it just needs to read the acl or public_access_block attribute.

Which scanners cover Terraform and CloudFormation today, and how do they differ?

Trivy's trivy config command is now the direct successor to standalone tfsec — Aqua's 2023 "tfsec joining the Trivy family" announcement made this explicit, and by 2024 tfsec's own repository stopped receiving new rules, only dependency bumps. Trivy scans Terraform, CloudFormation, Kubernetes manifests, and Dockerfiles with the same built-in rule set and outputs SARIF or JSON for CI. Checkov, built by Bridgecrew and now under Palo Alto Networks, covers the widest format list — Terraform, CloudFormation, ARM templates, Kubernetes, and Dockerfiles — with checks written in Python or YAML, and it's commonly chosen when a team needs one scanner across a genuinely mixed IaC estate. Terrascan, maintained by Tenable, targets Terraform, Kubernetes, Helm, and CloudFormation, and — notably for this piece — uses OPA and Rego internally rather than a proprietary rule engine, so its policy packs are portable to other Rego tooling. HashiCorp's Sentinel is the fourth option, but it's a paid-tier feature embedded in Terraform Cloud and Enterprise, not a standalone open-source CLI, so it fits organizations already committed to HCP-native pipelines rather than a generic pre-commit or CI scan.

How does policy-as-code with OPA and Conftest actually work?

Conftest, an OPA-based CLI originally from the Instrumenta project, tests structured configuration — JSON, YAML, or Terraform's own plan output — against Rego policy files, and it's designed to run as a pre-apply or pre-merge CI gate rather than a one-off audit. The workflow is: run terraform plan -out=tfplan, convert it with terraform show -json tfplan > plan.json, then run Conftest against that JSON with your Rego rules. Because OPA is a general-purpose policy engine (also used for Kubernetes admission control and API authorization), the same Rego skills and even some of the same policy patterns transfer across your IaC gate, your cluster admission controller, and your service-to-service authz — which is a large part of why OPA's CNCF graduation in 2021 mattered: it signaled one policy language worth standardizing on, not three different DSLs per tool.

Here's a minimal Conftest policy that denies a plan containing a security group ingress rule open to the internet:

package main

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_security_group_rule"
  resource.change.after.type == "ingress"
  resource.change.after.cidr_blocks[_] == "0.0.0.0/0"
  msg := sprintf("security group rule %v allows ingress from 0.0.0.0/0", [resource.address])
}

Run it with conftest test plan.json -p policy/, and a non-zero exit code fails the CI job before apply ever executes — the same pattern used to gate on unencrypted S3 buckets, missing versioning blocks, or overly broad IAM Resource: "*" statements by writing one deny rule per condition.

Should you scan the source HCL/YAML or the rendered plan?

Both, but they catch different things, so relying on only one leaves gaps. Scanning static source files (raw .tf or CloudFormation .yaml/.json templates) with Trivy, Checkov, or Terrascan is fast, needs no cloud credentials, and runs well as a pre-commit hook — but it can miss values that only resolve at plan time, like a variable default that gets overridden per-environment or a count/for_each expansion that only exists after evaluation. Scanning the rendered terraform plan -out JSON, as Conftest is built to do, sees the actual resource attributes Terraform is about to create or modify, including computed values — closer to ground truth, but it requires a real backend and valid credentials to generate, so it can't run in every lightweight pre-commit context. The practical pattern most teams converge on: fast source-level scanning locally and in pre-commit, plan-level policy-as-code gating in CI immediately before the apply step, and both feeding the same findings backlog so a fix in one layer doesn't get silently reintroduced by the other.

How does this fit into a broader unified findings pipeline?

Treating IaC misconfigurations as a fifth silo next to SAST, SCA, secrets, and container scanning defeats the point of catching them early — a public S3 bucket found by an IaC scanner and a hardcoded credential found by a secrets scanner are both "this build should not ship," and triage should treat them that way. Safeguard's unified findings model already carries an iac finding type — infrastructure-as-code misconfigurations — alongside SCA, secrets, and container results in the same tenant-scoped view, so a Terraform-sourced finding and a dependency CVE land in one queue instead of two separate tools' dashboards. As with Safeguard's SAST and DAST engines, IaC detection depth is expanding over time rather than static, so teams should still pair it with an open-source scanner like Trivy or Checkov and a Conftest policy gate in CI for the misconfiguration classes that matter most to their environment, rather than assuming coverage is exhaustive out of the box. The point of a pre-apply gate — whichever engine enforces it — is the same: a misconfigured bucket or security group should fail a pull request, not get discovered by whoever's watching CloudTrail after the fact.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.