DEV Community

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

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 = "*"
      }
    ]
  })
}

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

Running Checkov

pip install checkov
checkov -f main.tf

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

Passed checks: 14, Failed checks: 35, Skipped checks: 0

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

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

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

The result:

Passed checks: 79, Failed checks: 0, Skipped checks: 5

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

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
  • 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
  • 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

Comments

No comments yet. Start the discussion.