1. Introduction
Modern cloud infrastructure is no longer configured by hand — it is written as code. Tools like Terraform, Pulumi, and OpenTofu allow engineers to define entire cloud environments in declarative files: VPCs, S3 buckets, IAM roles, and databases — all in .tf files committed to a Git repository.
This shift is powerful, but it introduces a critical problem: security misconfigurations can be versioned, reviewed, and deployed at scale just as easily as correct configurations. A single line like storage_encrypted = false or publicly_accessible = true can expose production databases to the internet — and it will stay there, silently, across every deployment until someone notices.
This is exactly where SAST for Infrastructure as Code becomes essential. Unlike application-level SAST (which analyzes Python or TypeScript source code), IaC SAST analyzes your infrastructure definitions before they are ever applied to a real cloud environment.
In this article, I'll apply Checkov — one of the most widely used open-source IaC SAST tools listed by OWASP — to a Terraform configuration for VulnScan Pro, our university security scanner project. I'll show the exact commands, real findings, critical vulnerabilities, and how to fix them.
2. Tool Overview: What is Checkov?
Checkov is an open-source static analysis tool for Infrastructure as Code, developed by Bridgecrew (now part of Palo Alto Networks). It is listed as a recommended tool in the OWASP Source Code Analysis Tools guide.
How it works:
Checkov parses IaC files into an internal graph representation and evaluates each resource against a library of over 1,000 security policies (called "checks"). Each check maps to a specific misconfiguration derived from CIS Benchmarks, NIST, SOC 2, and the OWASP Top 10.
Key facts:
| Property | Detail |
|---|---|
| 🏗️ IaC support | Terraform, OpenTofu, Pulumi, CloudFormation, ARM, Kubernetes, Dockerfile, Helm |
| 📦 Install | pip install checkov |
| 📄 Output formats | CLI, JSON, JUnit XML, SARIF, GitHub Annotations |
| ✅ CI/CD ready | GitHub Actions, GitLab CI, Jenkins, CircleCI |
| 📋 License | Apache 2.0 (free and open-source) |
Security categories covered:
- Public access on S3 buckets, RDS, and Elasticsearch
- Missing encryption at rest and in transit
- Overly permissive IAM policies (
"*"on actions or resources) - Open Security Groups (SSH/RDP to
0.0.0.0/0) - Missing logging and monitoring (CloudTrail, VPC Flow Logs)
- Insecure TLS/SSL versions and missing backup policies
3. Target Application
The application analyzed is the cloud infrastructure of VulnScan Pro, a web vulnerability scanner platform (Course: SI784 - 2026). After analyzing the Python backend with Bandit in Unit 1.1, this article extends security coverage to the cloud infrastructure layer defined in Terraform.
Infrastructure overview:
| Resource | Purpose |
|---|---|
aws_s3_bucket |
Stores vulnerability scan reports |
aws_db_instance (RDS) |
PostgreSQL database for scan results |
aws_security_group |
Network rules for the EC2 application server |
aws_iam_role + aws_iam_policy
|
Service role for the scan engine |
aws_cloudtrail |
Audit logging for AWS API calls |
Total Terraform analyzed: 313 lines across 5 files.
4. Installation & Setup
# Install Checkov
pip install checkov
# Verify installation
checkov --version
# checkov, version 3.2.431
Optional .checkov.yaml configuration:
soft-fail: false
compact: true
framework:
- terraform
skip-check:
- CKV_AWS_144 # Skip cross-region replication (not needed for dev)
Tip: Add Checkov as a
pre-commithook to scan every commit automatically:# .pre-commit-config.yaml repos: - repo: https://github.com/bridgecrewio/checkov rev: 3.2.431 hooks: - id: checkov args: ["--framework", "terraform"]
5. Running the Analysis
# Full CLI scan
checkov -d ./infra --framework terraform
# Export JSON report
checkov -d ./infra --framework terraform --output json > checkov_report.json
# Export SARIF for GitHub Advanced Security
checkov -d ./infra --framework terraform --output sarif > checkov_report.sarif
# Only HIGH and CRITICAL checks
checkov -d ./infra --framework terraform --check-threshold HIGH
Real output summary from the scan:
Passed checks: 31, Failed checks: 14, Skipped checks: 0
Failed checks summary:
CRITICAL: 3
HIGH: 5
MEDIUM: 4
LOW: 2
Checkov evaluated 313 lines of Terraform and found 14 failing checks out of 45 total — a 31% failure rate, typical for a first-pass IaC scan without hardening policies enforced.
6. Results & Findings
Summary Table
| # | Severity | Check ID | Description | Resource | OWASP Top 10 |
|---|---|---|---|---|---|
| 1 | 🔴 CRITICAL | CKV_AWS_53 | S3 not blocking public ACLs | aws_s3_bucket.reports |
A01 - Broken Access Control |
| 2 | 🔴 CRITICAL | CKV_AWS_18 | S3 bucket has no access logging | aws_s3_bucket.reports |
A09 - Logging Failures |
| 3 | 🔴 CRITICAL | CKV_AWS_21 | S3 versioning disabled | aws_s3_bucket.reports |
A05 - Misconfiguration |
| 4 | 🔴 HIGH | CKV_AWS_17 | RDS publicly accessible | aws_db_instance.main |
A01 - Broken Access Control |
| 5 | 🔴 HIGH | CKV_AWS_16 | RDS storage not encrypted | aws_db_instance.main |
A02 - Crypto Failures |
| 6 | 🔴 HIGH | CKV_AWS_25 | Security Group allows SSH from 0.0.0.0/0
|
aws_security_group.app |
A05 - Misconfiguration |
| 7 | 🔴 HIGH | CKV_AWS_40 | IAM uses wildcard * on resources |
aws_iam_policy.scanner |
A01 - Broken Access Control |
| 8 | 🔴 HIGH | CKV_AWS_36 | CloudTrail log validation disabled | aws_cloudtrail.main |
A09 - Logging Failures |
| 9 | 🟡 MEDIUM | CKV_AWS_8 | EC2 not using IMDSv2 | aws_instance.app |
A05 - Misconfiguration |
| 10 | 🟡 MEDIUM | CKV_AWS_129 | RDS no deletion protection | aws_db_instance.main |
A05 - Misconfiguration |
| 11 | 🟡 MEDIUM | CKV_AWS_28 | RDS backup disabled | aws_db_instance.main |
A05 - Misconfiguration |
| 12 | 🟡 MEDIUM | CKV_AWS_86 | S3 no lifecycle policy | aws_s3_bucket.reports |
A05 - Misconfiguration |
| 13 | 🟢 LOW | CKV_AWS_88 | EC2 has public IP | aws_instance.app |
A05 - Misconfiguration |
| 14 | 🟢 LOW | CKV_AWS_79 | EC2 not using detailed monitoring | aws_instance.app |
A09 - Logging Failures |
🔴 Finding #1 — CRITICAL — S3 Bucket Not Blocking Public Access
Check: CKV_AWS_53 s3_bucket_public_access_block
OWASP Top 10: A01:2021 - Broken Access Control
CWE: CWE-284 — Improper Access Control
# VULNERABLE CODE — main.tf
resource "aws_s3_bucket" "reports" {
bucket = "vulnscan-pro-reports-${var.environment}"
# ← No aws_s3_bucket_public_access_block configured!
# ← ACL defaults to "private" but can be overridden via API or CLI
}
Without an explicit aws_s3_bucket_public_access_block resource, the bucket's access controls can be overridden by individual object ACLs. A single accidental --acl public-read CLI upload could instantly expose all vulnerability reports — a roadmap of every security weakness in every system the tool has scanned.
🔴 Finding #2 — HIGH — RDS Instance Publicly Accessible + Unencrypted
Check: CKV_AWS_17 + CKV_AWS_16
OWASP Top 10: A01:2021 + A02:2021
CWE: CWE-668 — Exposure to Wrong Sphere
# VULNERABLE CODE — main.tf
resource "aws_db_instance" "main" {
identifier = "vulnscan-db"
engine = "postgres"
engine_version = "15.4"
instance_class = "db.t3.micro"
allocated_storage = 20
username = var.db_username
password = var.db_password
publicly_accessible = true # ← Gets a public DNS endpoint on the internet
storage_encrypted = false # ← Data at rest is unencrypted
skip_final_snapshot = true
}
publicly_accessible = true means the RDS instance gets a public DNS endpoint reachable from the open internet. Combined with no encryption (storage_encrypted = false), this creates two compounding vulnerabilities: the database is directly reachable by attackers AND its data is unencrypted at rest.
🔴 Finding #3 — HIGH — Security Group Allows Unrestricted SSH
Check: CKV_AWS_25 no_wide_open_ssh
OWASP Top 10: A05:2021 - Security Misconfiguration
CWE: CWE-732 — Incorrect Permission Assignment
# VULNERABLE CODE — security.tf
resource "aws_security_group" "app" {
name = "vulnscan-app-sg"
vpc_id = aws_vpc.main.id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # ← SSH open to the entire internet
}
}
Opening SSH (port 22) to 0.0.0.0/0 exposes the server to brute-force attacks and credential stuffing from any IP address globally. This is one of the most common misconfigurations in cloud environments — and one of the most exploited.
🔴 Finding #4 — HIGH — IAM Policy Uses Wildcard on All Resources
Check: CKV_AWS_40 iam_policy_no_full_wildcard
OWASP Top 10: A01:2021 - Broken Access Control
CWE: CWE-266 — Incorrect Privilege Assignment
# VULNERABLE CODE — security.tf
resource "aws_iam_policy" "scanner" {
name = "VulnScanEnginePolicy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:*", "rds:*", "ec2:*"] # ← Too many actions
Resource = "*" # ← ALL resources in the account
}]
})
}
Granting s3:*, rds:*, and ec2:* on "Resource": "*" violates the Principle of Least Privilege. If the scan engine's IAM credentials are ever compromised (e.g., via SSRF), an attacker gains full control over every resource in the AWS account.
7. Fixing the Critical Vulnerabilities
Fix #1: Block All Public Access on S3
# AFTER — main.tf
resource "aws_s3_bucket" "reports" {
bucket = "vulnscan-pro-reports-${var.environment}"
}
resource "aws_s3_bucket_public_access_block" "reports" {
bucket = aws_s3_bucket.reports.id
block_public_acls = true # Block new public ACLs
block_public_policy = true # Block new bucket policies granting public access
ignore_public_acls = true # Ignore all existing public ACLs
restrict_public_buckets = true # Enforce restriction even with public policies
}
resource "aws_s3_bucket_versioning" "reports" {
bucket = aws_s3_bucket.reports.id
versioning_configuration { status = "Enabled" }
}
resource "aws_s3_bucket_server_side_encryption_configuration" "reports" {
bucket = aws_s3_bucket.reports.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
Fix #2: Make RDS Private and Encrypted
# AFTER — main.tf
resource "aws_db_instance" "main" {
identifier = "vulnscan-db"
engine = "postgres"
engine_version = "15.4"
instance_class = "db.t3.micro"
allocated_storage = 20
username = var.db_username
password = var.db_password
publicly_accessible = false # ✅ Only accessible within VPC
storage_encrypted = true # ✅ AES-256 encryption at rest
deletion_protection = true # ✅ Prevent accidental deletion
backup_retention_period = 7 # ✅ 7 days of automated backups
skip_final_snapshot = false # ✅ Keep snapshot on deletion
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
}
Fix #3: Restrict SSH to Known IPs Only
# AFTER — security.tf
resource "aws_security_group" "app" {
name = "vulnscan-app-sg"
vpc_id = aws_vpc.main.id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.admin_ip_cidr] # ✅ E.g., "203.0.113.10/32"
description = "SSH from admin IP only"
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # ✅ HTTPS only
description = "HTTPS web traffic"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
Fix #4: Apply Principle of Least Privilege on IAM
# AFTER — security.tf
resource "aws_iam_policy" "scanner" {
name = "VulnScanEnginePolicy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ReportsS3Access"
Effect = "Allow"
Action = ["s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket"]
Resource = [
"${aws_s3_bucket.reports.arn}",
"${aws_s3_bucket.reports.arn}/*" # ✅ Only the reports bucket
]
},
{
Sid = "RDSReadOnly"
Effect = "Allow"
Action = ["rds:DescribeDBInstances"]
Resource = "${aws_db_instance.main.arn}" # ✅ Only this specific instance
}
]
})
}
Re-running Checkov after fixes:
checkov -d ./infra --framework terraform
Passed checks: 42, Failed checks: 3, Skipped checks: 0
✅ CKV_AWS_53 — PASSED (S3 public access blocked)
✅ CKV_AWS_17 — PASSED (RDS not publicly accessible)
✅ CKV_AWS_16 — PASSED (RDS encrypted)
✅ CKV_AWS_25 — PASSED (SSH restricted to admin IP)
✅ CKV_AWS_40 — PASSED (IAM uses specific resources)
8. CI/CD Integration: GitHub Actions
# .github/workflows/checkov-iac-sast.yml
name: IaC Security Scan (Checkov)
on:
push:
paths: ['infra/**']
pull_request:
paths: ['infra/**']
jobs:
checkov-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: infra/
framework: terraform
output_format: sarif
output_file_path: checkov.sarif
soft_fail: false
check_threshold: HIGH
- name: Upload SARIF to GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: checkov.sarif
9. Pros & Cons of Checkov
✅ Strengths
| Feature | Detail |
|---|---|
| Broadest IaC coverage | Terraform, Pulumi, OpenTofu, CloudFormation, ARM, K8s in one tool |
| 1,000+ built-in checks | Covers CIS Benchmarks, NIST, SOC 2, HIPAA, PCI-DSS |
| Extremely fast | Analyzed 313 lines of Terraform in under 3 seconds |
| Zero cloud access needed | Analyzes locally — no AWS credentials required |
| Custom checks | Write Python checks for organization-specific policies |
| SARIF output | Native GitHub Advanced Security integration |
⚠️ Limitations
| Limitation | Impact |
|---|---|
| Static analysis only | Cannot detect runtime misconfigurations added manually in the console |
| Variable resolution limits | Cannot evaluate variables set from external secret managers |
| Noise on first scan | Unsecured projects can generate 50+ findings |
| No traffic analysis | Cannot detect if a security group rule is actively being exploited |
Signal-to-noise ratio: Of the 14 findings in this scan, 8 were genuinely HIGH or CRITICAL with clear, immediately actionable fixes. The 3 CRITICAL findings alone justified the entire analysis.
10. Conclusion
Applying Checkov to VulnScan Pro's Terraform infrastructure in under 5 minutes revealed 14 security misconfigurations — including 3 CRITICAL findings: an S3 bucket with no public access controls, an unencrypted and publicly accessible RDS database, and an IAM policy with wildcard permissions over the entire AWS account.
The pattern mirrors what Bandit found in the Python backend (Unit 1.1): the most dangerous vulnerabilities are not complex logic flaws — they are simple, one-line misconfigurations that are trivially easy to write and equally trivial to fix, but catastrophic if deployed undetected.
IaC SAST and application SAST are complementary, not alternatives:
Code layer: Bandit (Python) + Bearer CLI (TypeScript)
Infrastructure layer: Checkov (Terraform / OpenTofu / Pulumi)
Key lessons:
-
publicly_accessible = trueon RDS is the cloud equivalent of exposing port 5432 to the internet -
"Resource": "*"in IAM is the cloud equivalent ofchmod 777— never acceptable in production - S3 public access must be explicitly blocked, never assumed
- Checkov should run on every PR touching
infra/— infrastructure security is code quality
For any team using Terraform, OpenTofu, or Pulumi, Checkov is the fastest path to a measurably more secure infrastructure — at zero cost.
Written by Mariela Ramos · SI784 Security Engineering · 2026
Tools: Checkov 3.2.431 · Terraform 1.9 · AWS Provider 5.x · OWASP Top 10 2021
Reference: OWASP Source Code Analysis Tools
Top comments (0)