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
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
}
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
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
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
}
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
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
}
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)