Safeguard
Infrastructure Security

How to scan Terraform for misconfigurations with Checkov

A hands-on guide to running Checkov against Terraform, triaging findings, writing custom policies, and blocking IaC misconfigurations before they merge.

Karan Patel
Cloud Security Engineer
9 min read

IaC security scanning with Checkov is one of the fastest ways to catch Terraform misconfigurations before they ever reach production. Public S3 buckets, permissive security groups, unencrypted volumes, and overly broad IAM policies are all the kind of drift that turns into a breach headline, and most of them are trivial to catch in a pull request if you have the right scanner wired in. This guide walks through a hands-on Checkov terraform scan tutorial: installing the tool, running your first scan, reading the output, suppressing noise responsibly, and wiring the whole thing into CI so misconfigurations get blocked before merge instead of found during an audit. By the end, you'll have a repeatable IaC misconfiguration detection workflow you can drop into any Terraform repo, plus a sense of how a platform like Safeguard extends this into an organization-wide control.

Why IaC Security Scanning with Checkov Matters for Terraform

Terraform makes infrastructure changes fast and repeatable, which is exactly why a single bad module can replicate a misconfiguration across dozens of environments in minutes. A hardcoded 0.0.0.0/0 ingress rule or a storage bucket without encryption doesn't just affect one resource — it becomes the default pattern every engineer copies into the next module.

Checkov is an open-source static analysis tool built specifically for infrastructure-as-code. It parses Terraform (along with CloudFormation, Kubernetes manifests, Helm charts, ARM templates, and more) into a graph representation and checks it against hundreds of built-in policies mapped to frameworks like CIS Benchmarks, NIST, PCI-DSS, and SOC 2. Because it works statically against your .tf files, it catches problems before terraform apply ever touches a cloud account — which is a much cheaper place to fix a misconfiguration than after it's live.

Step 1: Install Checkov

Checkov is a Python package and installs cleanly with pip, Homebrew, or as a Docker image. Pip is the most common path for local development and CI runners:

pip install checkov

Verify the install and check the version:

checkov --version

If you'd rather avoid polluting your local Python environment, the Docker image works identically and is often the better choice for CI:

docker pull bridgecrew/checkov
docker run --rm -v "$(pwd)":/tf bridgecrew/checkov -d /tf

For teams standardizing tool versions across a monorepo, pinning the Checkov version in a requirements-dev.txt or your CI image avoids "it worked on my machine" drift when new checks ship upstream.

Step 2: Run Your First Checkov Terraform Scan

Navigate to the root of your Terraform configuration and point Checkov at the directory:

checkov -d . --framework terraform

Checkov will recursively scan every .tf file, build a resource graph, and evaluate each resource against its policy set. Output looks like this for a passed and failed check:

Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public READ access"
	PASSED for resource: aws_s3_bucket.logs

Check: CKV_AWS_23: "Ensure every security group rule has a description"
	FAILED for resource: aws_security_group.web
	File: /modules/network/main.tf:14-22
	Guide: https://docs.bridgecrew.io/docs/networking_4

Each finding includes the specific resource, the exact file and line range, and a link to remediation guidance — enough context to fix the issue without leaving your editor. For a single Terraform module, this is a five-minute scan; for a large monorepo it's still typically under a minute since the analysis is static, not a live cloud API scan.

If you only care about a specific check ID or want to scope the scan to one directory, both are supported:

checkov -d ./modules/network --check CKV_AWS_23

Step 3: Read and Prioritize the Findings

A fresh Checkov terraform scan tutorial run against an existing codebase will often surface fifty or more findings on the first pass — don't try to fix everything at once. Triage by severity and blast radius first:

  • Public exposure — open security groups, public S3/GCS buckets, public database instances. Fix these first; they're the ones attackers scan for automatically.
  • Missing encryption — unencrypted EBS volumes, RDS instances, or S3 buckets without SSE. High severity, usually a one-line fix.
  • IAM over-permissioning — wildcard actions or resources in policies. These take longer to fix correctly since you need to know the actual required permissions.
  • Logging and monitoring gaps — missing CloudTrail, VPC flow logs, or access logging. Lower urgency but important for detection and audit trails.

Export results as JSON or SARIF if you want to feed them into a dashboard or ticketing system instead of reading raw terminal output:

checkov -d . --output json --output-file-path results.json
checkov -d . --output sarif --output-file-path results.sarif

SARIF is worth calling out specifically since GitHub, GitLab, and most code-scanning dashboards ingest it natively, which turns your Checkov output into inline PR annotations rather than a wall of terminal text someone has to go look up.

Step 4: Suppress False Positives Deliberately

Not every finding applies to your environment. A public-facing load balancer in front of a static marketing site isn't the same risk as a public database. Checkov supports inline suppression with an explicit reason, which keeps the suppression auditable instead of silent:

resource "aws_s3_bucket" "public_assets" {
  #checkov:skip=CKV_AWS_20:This bucket intentionally serves public static assets via CloudFront
  bucket = "company-public-assets"
}

You can also suppress at the run level with a .checkov.yaml config file checked into the repo, which is the better option for team-wide policy decisions rather than one-off exceptions:

skip-check:
  - CKV_AWS_8   # Not applicable: instances are ephemeral, launched via ASG with encrypted AMI
compact: true
soft-fail: false

Treat every skip as a decision that needs a reviewer's sign-off in the PR — an unexplained skip-check list is exactly how misconfigurations quietly become permanent. This is also where a documented Terraform security best practices standard pays off: agree once, as a team, on which categories of findings are always blocking versus context-dependent, and write it down so triage doesn't get re-litigated in every PR.

Step 5: Enforce Terraform Security Best Practices with Custom Policies

Built-in checks cover the common cloud misconfigurations, but every organization has internal standards that no public ruleset knows about — required tags, approved AMI sources, naming conventions tied to cost allocation. Checkov lets you write custom policies in Python or a simplified YAML format:

metadata:
  id: "CKV2_CUSTOM_1"
  name: "Ensure all EC2 instances have a CostCenter tag"
  category: "GENERAL_SECURITY"
scope:
  provider: "aws"
definition:
  cond_type: "attribute"
  resource_types:
    - "aws_instance"
  attribute: "tags.CostCenter"
  operator: "exists"

Point Checkov at the directory containing your custom policies alongside the built-in set:

checkov -d . --external-checks-dir ./custom-policies

This is the step that turns Checkov from "a generic scanner" into an enforcement layer for your organization's actual Terraform security best practices — the accumulated lessons from past incidents, compliance requirements, and architecture reviews, codified as something a machine checks on every PR instead of something that lives in a wiki nobody reads.

Step 6: Wire Checkov into CI/CD

A scan that only runs on a developer's laptop only catches misconfigurations that developer happens to run it against. The real value of IaC misconfiguration detection comes from making it a required, automated gate. A minimal GitHub Actions job:

name: checkov-scan
on: [pull_request]
jobs:
  checkov:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: .
          framework: terraform
          output_format: sarif
          output_file_path: results.sarif
      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: results.sarif

Start with soft-fail: true (or the action's equivalent) so the team gets visibility without blocking every open PR on day one, then flip specific high-severity checks to hard-fail once the existing findings are triaged and the backlog is under control. Trying to hard-fail everything on day one is the single most common reason a Checkov rollout gets disabled by a frustrated team a week later.

Troubleshooting and Verification

Checkov reports zero resources scanned. This usually means it's being pointed at a directory without .tf files, or a .terraform cache directory is confusing the parser. Run with --compact off and check the file list, or scope the -d flag directly to the module root.

A check fails but the Terraform is already correct. Confirm you're not scanning a stale plan file or an outdated module version. Also check whether the resource uses a variable or module output Checkov can't statically resolve — in those cases, --var-file pointing at your terraform.tfvars can help Checkov resolve values it would otherwise treat as unknown.

Custom policies aren't being picked up. Verify the --external-checks-dir path is correct relative to where the command runs (CI working directories are a common culprit), and that the YAML or Python file follows Checkov's expected schema — a malformed custom check fails silently rather than throwing a hard error in some versions.

CI passes locally but fails in the pipeline. Compare Checkov versions between local and CI. New releases regularly add checks, so a version pinned in CI but not locally (or vice versa) will produce different results. Pin the version in both places.

To verify your setup end to end, intentionally introduce a known-bad resource (an open security group is a good, safe test case), confirm Checkov flags it locally, push the branch, and confirm the same finding shows up as a CI failure or PR annotation. If all three match, your detection pipeline is wired correctly.

How Safeguard Helps

Checkov is an excellent foundation for IaC security scanning, but a single open-source scanner in one repo's CI pipeline only tells you about that repo. Safeguard extends the same underlying idea — static, policy-driven detection of infrastructure misconfigurations — across your entire software supply chain, correlating Terraform findings with the artifacts, containers, and pipelines those resources actually deploy.

Instead of triaging Checkov output repo by repo, Safeguard aggregates IaC misconfiguration detection results across every codebase your organization owns, maps findings to the services and teams that own them, and tracks remediation over time so a fixed issue doesn't quietly reappear in the next module someone copies. Custom policy exceptions get the audit trail compliance teams need for SOC 2 and similar frameworks, instead of living as unreviewed #checkov:skip comments scattered across dozens of files. And because Safeguard sits at the supply chain layer, it can tie a Terraform misconfiguration back to the commit, the pull request, and the engineer who introduced it — turning a scanner finding into an actionable, ownable fix rather than another item in a spreadsheet nobody closes.

If your team is ready to move from ad hoc Checkov runs to an organization-wide IaC security scanning Checkov program with enforced Terraform security best practices, Safeguard can plug directly into the CI workflow you already have.

Never miss an update

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