DEV Community

DAYAN ELVIS JAHUIRA PILCO
DAYAN ELVIS JAHUIRA PILCO

Posted on

Your Infrastructure Has Bugs Too: Scanning Terraform with Checkov (IaC SAST)

TL;DR: Application code isn't the only thing that ships vulnerabilities — your Terraform does too. I wrote an intentionally insecure AWS configuration, scanned it with Checkov (a SAST tool for Infrastructure as Code, listed on the OWASP Source Code Analysis Tools page), went from 35 failed checks to 0, and wired the scan into GitHub Actions. Full code: GitHub repo →

SAST for infrastructure? Yes, that's a thing

In my previous article I used Bandit to find security bugs in Python code. But modern applications are deployed with Infrastructure as Code (Terraform, Pulumi, OpenTofu, CloudFormation) — and a misconfigured S3 bucket has caused more real-world data breaches than most code vulnerabilities.

The good news: since infrastructure is now code, it can be statically analyzed like code. That's exactly what Checkov does: an open-source tool (Python, maintained by Prisma Cloud) with 1,000+ built-in policies that scans Terraform, CloudFormation, Kubernetes manifests, Dockerfiles, and more — no cloud credentials needed, it never touches your AWS account.

The target: a deliberately insecure AWS stack

I wrote a main.tf that concentrates the misconfigurations behind many famous breaches:

# Issue 1: S3 bucket - public, unencrypted, no versioning, no logging
resource "aws_s3_bucket" "data" {
  bucket = "company-customer-data-bucket"
  acl    = "public-read"
}

# Issue 2: security group open to the entire internet, including SSH
resource "aws_security_group" "web" {
  name = "web-sg"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 0
    to_port     = 65535
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# Issue 3: database publicly accessible, unencrypted, hardcoded password
resource "aws_db_instance" "db" {
  identifier          = "app-db"
  engine              = "mysql"
  instance_class      = "db.t3.micro"
  allocated_storage   = 20
  username            = "admin"
  password            = "SuperSecret123!"
  publicly_accessible = true
  storage_encrypted   = false
  skip_final_snapshot = true
}

# Issue 4: EC2 instance without IMDSv2 and with unencrypted root volume
resource "aws_instance" "app" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"

  vpc_security_group_ids = [aws_security_group.web.id]

  root_block_device {
    encrypted = false
  }
}

# Issue 5: IAM policy with full admin wildcard
resource "aws_iam_policy" "admin" {
  name = "app-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = "*"
      Resource = "*"
    }]
  })
}
Enter fullscreen mode Exit fullscreen mode

Five resources. Looks harmless, right? Let's see.

Running Checkov

pip install checkov
checkov -f main.tf
Enter fullscreen mode Exit fullscreen mode

The verdict, in a few seconds and without touching AWS:

Passed checks: 14, Failed checks: 35, Skipped checks: 0
Enter fullscreen mode Exit fullscreen mode

35 failed checks in 70 lines of Terraform. Some highlights:

🔴 The public S3 bucket (the classic breach)

Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public READ access."
    FAILED for resource: aws_s3_bucket.data
Check: CKV2_AWS_6: "Ensure that S3 bucket has a Public Access block"
    FAILED for resource: aws_s3_bucket.data
Check: CKV_AWS_145: "Ensure that S3 buckets are encrypted with KMS by default"
    FAILED for resource: aws_s3_bucket.data
Enter fullscreen mode Exit fullscreen mode

A bucket named company-customer-data-bucket with acl = "public-read" — this exact pattern exposed hundreds of millions of records in real incidents (Capital One, Accenture, the US voter records leak...).

🔴 The god-mode IAM policy

Check: CKV_AWS_62: "Ensure IAM policies that allow full "*-*" administrative
       privileges are not created"
    FAILED for resource: aws_iam_policy.admin
Check: CKV_AWS_286: "Ensure IAM policies does not allow privilege escalation"
    FAILED for resource: aws_iam_policy.admin
Check: CKV_AWS_288: "Ensure IAM policies does not allow data exfiltration"
    FAILED for resource: aws_iam_policy.admin
Enter fullscreen mode Exit fullscreen mode

Action = "*" on Resource = "*" earned eight failed checks by itself: privilege escalation, credentials exposure, data exfiltration... one wildcard, every attack path.

🔴 SSH open to the world + public database

Checkov also flagged the security group allowing 0.0.0.0/0 on port 22 (CKV_AWS_24), the RDS instance with publicly_accessible = true (CKV_AWS_17), no storage encryption (CKV_AWS_16), and the hardcoded database password sitting in version control.

Fixing it

The remediated main_fixed.tf applies standard hardening:

Misconfiguration Fix
Public S3 bucket aws_s3_bucket_public_access_block + private ACL
Unencrypted bucket KMS key with rotation + SSE configuration
No versioning/logging aws_s3_bucket_versioning + access logging to a second bucket
SSH open to 0.0.0.0/0 Ingress restricted to a trusted CIDR variable, HTTPS only
Hardcoded DB password variable "db_password" { sensitive = true } via TF_VAR_db_password
Public, unencrypted RDS publicly_accessible = false, storage_encrypted = true, multi-AZ
EC2 metadata v1 metadata_options { http_tokens = "required" } (IMDSv2)
Admin wildcard IAM Least privilege: s3:GetObject on one bucket ARN only

Two checks didn't apply to this workload (cross-region replication, S3 event notifications), so instead of silencing the scanner I documented the decision inline — Checkov's equivalent of Bandit's # nosec:

resource "aws_s3_bucket" "data" {
  #checkov:skip=CKV_AWS_144:Single-region deployment, replication not required
  #checkov:skip=CKV2_AWS_62:No downstream consumers need S3 event notifications
  bucket = "company-customer-data-bucket"
}
Enter fullscreen mode Exit fullscreen mode

The result:

Passed checks: 79, Failed checks: 0, Skipped checks: 5
Enter fullscreen mode Exit fullscreen mode

From 35 failed checks to 0, with every skip justified and auditable. ✅

Automating with GitHub Actions

Same principle as any SAST tool: a manual scan is a snapshot, a CI scan is a security gate. This workflow runs on every push and PR, and fails the build if the hardened configuration regresses:

name: Checkov IaC Scan

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  checkov:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Install Checkov
        run: pip install checkov
      - name: Scan vulnerable config (findings expected, does not block)
        run: checkov -f main.tf --compact || true
      - name: Scan fixed config (security gate)
        run: checkov -f main_fixed.tf --compact
      - name: Generate JSON report
        if: always()
        run: checkov -f main_fixed.tf -o json > checkov-report.json || true
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: checkov-report
          path: checkov-report.json
Enter fullscreen mode Exit fullscreen mode

Now a pull request that opens port 22 to the internet gets a red ❌ before any human even reviews it.

Strengths and limitations

What impressed me about Checkov: enormous policy coverage out of the box, scans finish in seconds with zero cloud credentials, checks map to real breach patterns, and the graph-based engine understands connections between resources (that's what the CKV2_* checks do — for example, it knows my bucket lacks a Public Access Block resource, not just a bad attribute).

Limitations, in line with what OWASP says about SAST generally: it analyzes declared state, not what actually runs in your account (drift is invisible to it); it can't know your business context, so expect policies that don't apply to your workload — that's what documented skips are for; and it won't catch logic flaws in how your services use the infrastructure. Combine it with cloud posture management (CSPM) and runtime monitoring for full coverage.

Conclusion

The same lesson as application SAST, but the stakes are arguably higher: nobody exploits your off-by-one error as fast as a scanner finds your public S3 bucket. Checkov made 35 concrete problems visible in seconds, taught me the hardening pattern for each one, and now guards my infrastructure on every commit — for free.

Full demo code + workflow: github.com/Dayan-18/checkov-demo

Do you scan your IaC in CI? Which tool? Tell me in the comments! 👇


References: OWASP Source Code Analysis Tools · Checkov documentation · Terraform AWS provider docs

Top comments (0)