DEV Community

Catching Cloud Misconfigurations Before They Ship: SAST for Terraform with Checkov

Why Infrastructure as Code needs SAST too

Static Application Security Testing (SAST) is normally associated with application source code — Java, Python, JavaScript. But Infrastructure as Code (IaC) files like Terraform are code too, and they define something arguably more dangerous to get wrong: the actual cloud resources an organization runs on. A single misconfigured .tf file can leave an S3 bucket public, disable encryption on a database, or open a security group to the entire internet — and none of that shows up as a runtime bug, because it's not a bug, it's exactly what was declared.

OWASP's Source Code Analysis Tools list (https://owasp.org/www-community/Source_Code_Analysis_Tools) is normally cited for application-level static analyzers, but the same "shift left" principle applies directly to IaC: catch the misconfiguration in the pull request, not in production. This article walks through Checkov, an open-source SAST tool built specifically for IaC, deliberately leaving TFSec out of the picture.

What is Checkov?

Checkov is an open-source static analysis tool created by Bridgecrew (now part of Prisma Cloud). It scans Terraform, Terraform plan output, CloudFormation, Kubernetes manifests, Helm charts, Dockerfiles, Bicep, ARM templates, and OpenTofu, looking for security and compliance misconfigurations using graph-based scanning rather than plain text matching.

Official repository: https://github.com/bridgecrewio/checkov

Setting up a target to scan

Rather than writing a toy example from scratch, I used TerraGoat, an intentionally vulnerable Terraform project maintained by the same Bridgecrew team behind Checkov. It exists specifically so people can practice IaC scanning against real, varied misconfigurations instead of a single contrived file.

git clone https://github.com/bridgecrewio/terragoat.git
cd terragoat
Enter fullscreen mode Exit fullscreen mode

A representative snippet from the AWS module looks like this:

resource "aws_s3_bucket" "data" {
  bucket = "terragoat-${var.environment}-customer-data"
  acl    = "public-read"
}

resource "aws_db_instance" "customer_db" {
  identifier        = "terragoat-${var.environment}-db"
  engine            = "postgres"
  instance_class    = "db.t3.micro"
  allocated_storage = 20
  storage_encrypted = false
  publicly_accessible = true
}
Enter fullscreen mode Exit fullscreen mode

Both resources look fine syntactically. Both are serious security problems: a public S3 bucket with customer data, and a publicly accessible, unencrypted RDS instance.

Installing and running Checkov

python3 -m venv venv
source venv/bin/activate
pip install checkov

checkov -d ./aws
Enter fullscreen mode Exit fullscreen mode

Checkov reports each failed check with an ID, a description, and a link to remediation guidance:

Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public access"
    FAILED for resource: aws_s3_bucket.data
    File: /aws/s3.tf:1-4

Check: CKV_AWS_16: "Ensure that RDS instances have encryption at rest enabled"
    FAILED for resource: aws_db_instance.customer_db
    File: /aws/rds.tf:1-8

Check: CKV_AWS_17: "Ensure all data stored in RDS is not publicly accessible"
    FAILED for resource: aws_db_instance.customer_db
    File: /aws/rds.tf:1-8
Enter fullscreen mode Exit fullscreen mode

Fixing the findings

resource "aws_s3_bucket" "data" {
  bucket = "terragoat-${var.environment}-customer-data"
  acl    = "private"
}

resource "aws_db_instance" "customer_db" {
  identifier           = "terragoat-${var.environment}-db"
  engine               = "postgres"
  instance_class       = "db.t3.micro"
  allocated_storage    = 20
  storage_encrypted    = true
  publicly_accessible  = false
}
Enter fullscreen mode Exit fullscreen mode

Running checkov -d ./aws again shows those specific checks now passing, while the rest of TerraGoat's intentional vulnerabilities remain — which is exactly the point of a vulnerable-by-design target: it lets you validate the tool actually catches what it claims to, one fix at a time.

Automating it with GitHub Actions

Manually running Checkov defeats the purpose of "shifting left." The real value comes from wiring it into CI so every pull request gets scanned automatically:

# .github/workflows/checkov.yml
name: Checkov IaC Scan

on:
  push:
    paths:
      - '**/*.tf'
  pull_request:
    paths:
      - '**/*.tf'

jobs:
  checkov_scan:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Run Checkov
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: .
          framework: terraform
          soft_fail: true
          output_format: sarif
          output_file_path: results.sarif

      - name: Upload SARIF to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: results.sarif
Enter fullscreen mode Exit fullscreen mode

soft_fail: true lets the pipeline continue even if issues are found — useful while a team is still triaging a legacy codebase. Once findings are under control, flipping it to false turns Checkov into an actual merge gate. The SARIF output means every finding shows up directly as an annotation in GitHub's Security tab, right next to the code that caused it.

Suppressing accepted risks

Not every finding is actionable — sometimes a public bucket is intentional (a static website, a public documentation folder). Checkov supports inline suppression instead of forcing you to disable an entire rule globally:

resource "aws_s3_bucket" "public_docs" {
  bucket = "my-public-documentation"
  acl    = "public-read" #checkov:skip=CKV_AWS_20:This bucket intentionally serves public docs
}
Enter fullscreen mode Exit fullscreen mode

This keeps the suppression next to the resource it applies to, so the next person reading the file understands why the rule was skipped, instead of just seeing a disabled check somewhere in a config file.

Where Checkov stops

Checkov is a static analyzer: it understands the declared configuration, not runtime behavior. It won't catch a logic bug in a module's variable interpolation, and it can't tell you if a security group that looks fine on paper ends up overly permissive once combined with other resources at apply time. It's one layer of defense — a strong one for catching known misconfiguration patterns before deployment, not a replacement for code review or runtime monitoring.

Conclusion

Applying SAST to Terraform isn't fundamentally different from applying it to application code: find the tool, wire it into the pipeline, and treat its findings as a gate rather than a suggestion. Checkov's graph-based scanning, 1,000+ built-in policies, and native SARIF output make it a solid entry point for teams that want to catch cloud misconfigurations in the pull request instead of in an incident report.

Top comments (0)