Abstract
Infrastructure as Code (IaC) has turned cloud infrastructure into source code — and, therefore, into a valid target for Static Application Security Testing (SAST). This article demonstrates the complete application of Checkov, an open-source IaC static analysis tool listed in the OWASP Source Code Analysis Tools catalog, to a Terraform project describing a small cloud system (S3, RDS and EC2). The initial scan detected 27 failed security checks — including a publicly readable bucket, a database exposed to the internet with a hardcoded password, and an instance vulnerable to SSRF credential theft — in roughly ten seconds, without an AWS account and without executing terraform. The configuration was then hardened to 0 failed checks (61 passed), with the three non-applicable policies suppressed through inline, documented justifications. Finally, the scan was integrated into a GitHub Actions pipeline that blocks any pull request reintroducing a misconfiguration, illustrating the DevSecOps principle of security as an automated gate. The full demo code and CI workflow are available at https://github.com/GianfrancoArocutipa/checkov-iac-demo.
SAST for infrastructure? Yes.
Static Application Security Testing (SAST) is usually explained with application code: find SQL injection in PHP, XSS in JavaScript. But since Infrastructure as Code became the standard — Terraform, Pulumi, OpenTofu, CloudFormation — your infrastructure is also source code. And it fails in its own ways: a public S3 bucket, a database exposed to the internet, an SSH port open to the world. These misconfigurations are behind a huge share of real cloud breaches, and the beautiful part is that they are visible in the code before anything is deployed. That is exactly what IaC SAST tools do: parse your .tf files and compare every resource against hundreds of security policies — no cloud account needed, no terraform apply, no risk.
Why Checkov?
Checkov (by Prisma Cloud) is open source, ships 1,000+ built-in policies for AWS, Azure and GCP, and supports Terraform, OpenTofu, CloudFormation, Kubernetes, Dockerfiles and more. It installs with a single pip install, needs no configuration to start, and exports SARIF reports that plug directly into GitHub Code Scanning. It also does something many scanners don't: graph-based checks (IDs starting with CKV2_) that reason about relationships between resources — for example, "this bucket has no aws_s3_bucket_public_access_block resource attached to it".
The victim: a workshop system in Terraform
The demo describes the infrastructure of a vehicle-workshop management system: an S3 bucket for diagnostic reports, a PostgreSQL RDS database, and an EC2 app server. Here are the highlights of the vulnerable version:
# S3 bucket for vehicle diagnostic reports
resource "aws_s3_bucket" "reports" {
bucket = "workshop-diagnostic-reports"
acl = "public-read" # anyone on the internet can read customer reports
}
# SSH open to the entire internet
resource "aws_security_group" "app" {
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # brute-force attacks welcome
}
}
# The database
resource "aws_db_instance" "workshop" {
engine = "postgres"
username = "admin"
password = "SuperSecret123!" # hardcoded secret in source control
publicly_accessible = true # database exposed to the internet
storage_encrypted = false # customer data stored in plaintext
}
Every one of those comments is a real incident waiting to happen — and every one of them maps to items in the OWASP Top 10 world (security misconfiguration, cryptographic failures, secrets in code).
Step 1 — Install Checkov
pip install checkov
That's the entire setup. No API keys, no cloud credentials, no Terraform binary.
Step 2 — Scan the vulnerable code
checkov -d terraform/vulnerable --compact
The result, in about ten seconds:
terraform scan results:
Passed checks: 16, Failed checks: 27, Skipped checks: 0
Check: CKV_AWS_24: "Ensure no security groups allow ingress from 0.0.0.0:0 to port 22"
FAILED for resource: aws_security_group.app
File: /main.tf:31-49
Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public READ access."
FAILED for resource: aws_s3_bucket.reports
File: /main.tf:22-25
Check: CKV_AWS_17: "Ensure all data stored in RDS is not publicly accessible"
FAILED for resource: aws_db_instance.workshop
File: /main.tf:55-67
Check: CKV_AWS_16: "Ensure all data stored in the RDS is securely encrypted at rest"
FAILED for resource: aws_db_instance.workshop
File: /main.tf:55-67
Check: CKV_AWS_79: "Ensure Instance Metadata Service Version 1 is not enabled"
FAILED for resource: aws_instance.app
File: /main.tf:73-82
...
27 failed checks on ~80 lines of Terraform. Each finding has a stable ID (CKV_AWS_24), a human-readable policy, the exact resource and the line range. Note the last one: Checkov even catches that the EC2 instance allows IMDSv1 — the metadata service version abused in real SSRF attacks (like the Capital One breach) to steal cloud credentials. That's not a check most humans remember during code review.
Step 3 — Harden the code
The fixed version remediates every class of finding:
# Secrets: injected via variable, never hardcoded
variable "db_password" {
type = string
sensitive = true
}
# S3: private, versioned, encrypted with a rotating KMS key
resource "aws_s3_bucket_public_access_block" "reports" {
bucket = aws_s3_bucket.reports.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# Network: SSH only from the admin network, egress restricted to HTTPS
ingress {
description = "SSH from the admin network only"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.admin_cidr]
}
# EC2: encrypted disk + IMDSv2 enforced (blocks SSRF credential theft)
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
}
Plus: RDS gets storage_encrypted, publicly_accessible = false, deletion protection, Multi-AZ, IAM authentication, CloudWatch logs and forced SSL in transit.
Step 4 — Handle the checks that don't apply (the honest way)
Three checks kept failing even on the hardened code: cross-region replication, S3 event notifications and bucket access logging. They are operational policies that make sense for production at scale, not for this demo. Deleting them from the report would be lying to yourself — instead, Checkov supports inline, documented suppressions placed inside the resource:
resource "aws_s3_bucket" "reports" {
#checkov:skip=CKV_AWS_18:Access logging is centralized at the org level
#checkov:skip=CKV_AWS_144:Cross-region replication not required for a single-region demo
#checkov:skip=CKV2_AWS_62:Event notifications not needed; no downstream consumers
bucket = "workshop-diagnostic-reports"
}
Every exception is now version-controlled, justified and visible in the report as skipped — an auditor can review each decision. The final scan:
terraform scan results:
Passed checks: 61, Failed checks: 0, Skipped checks: 3
From 27 failed to 0 failed, with every exception documented.
Step 5 — Automate it: scan on every push with GitHub Actions
A manual scan is a scan someone will eventually skip. This workflow runs Checkov on every push and pull request, fails the build on any finding, and uploads a SARIF report so results appear in GitHub's Security → Code scanning tab as inline annotations on the exact Terraform lines:
# .github/workflows/sast-checkov.yml
name: SAST — Checkov IaC Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
security-events: write
jobs:
checkov-iac-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install checkov
- run: >
checkov -d terraform
--output cli --output sarif
--output-file-path console,results.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: results.sarif
If a teammate opens a PR that re-exposes the database or opens SSH to the world, the pipeline goes red and the merge is blocked. The insecure infrastructure never gets deployed, because it never even gets merged.
Limitations worth knowing
Checkov analyzes the code, not the running cloud: drift (someone clicking in the AWS console) is invisible to it, and values only known at runtime can produce blind spots. Policy checks can also feel noisy at first — that's what documented skips and severity filtering are for. IaC SAST is one layer: it complements — not replaces — cloud posture management and runtime monitoring.
Conclusion
Ten seconds of static analysis found 27 misconfigurations — public buckets, exposed databases, plaintext storage, IMDSv1 — before a single resource existed in the cloud. We fixed them all, documented the exceptions, and wired the scan into CI so it runs on every commit forever. If your infrastructure lives in Terraform, Pulumi or OpenTofu, there is no cheaper security win than this.
Demo repository (code + CI workflow): https://github.com/GianfrancoArocutipa/checkov-iac-demo(#)
Top comments (0)