DEV Community

DIEGO FABRIZIO ANDIA NAVARRO
DIEGO FABRIZIO ANDIA NAVARRO

Posted on

Applying a SAST Tool to Infrastructure as Code: Scanning a Terraform Stack with Checkov

Abstract

Application code isn't the only place a SAST tool belongs — infrastructure definitions are source code too, and misconfigurations written into a Terraform file ship to production exactly as reliably as a bug in application logic does. This article applies Checkov, an open-source static analysis tool for Infrastructure as Code, to a Terraform stack that provisions storage, networking, a database, and IAM permissions for an order service. The unmodified stack produced 36 failed checks against 14 passed. After remediating every finding that represented an actual exploitable risk, the same stack passed 57 checks with 6 remaining — and those 6 turned out to be business decisions (replication, lifecycle policies) rather than vulnerabilities, which is itself the more useful finding: not every flagged item deserves the same response.

Why IaC needs its own static analysis, separate from application SAST

A SAST tool for application code looks for dangerous function calls and data flows. A SAST tool for IaC looks for a different category entirely: resource configurations that violate a known-safe default. An aws_s3_bucket with no aws_s3_bucket_public_access_block isn't a code bug in the traditional sense — the Terraform is syntactically perfect and will apply cleanly. The risk is purely in what gets provisioned. That's precisely the gap Checkov, Terrascan, and similar tools are built to close: they evaluate the intended infrastructure state, not the code that produces it.

The real-world example: a Terraform stack for an order service

A small but realistic stack: an S3 bucket for order exports, a security group, an RDS database, an IAM policy, an EBS volume, and a CloudWatch log group.

# terraform-vulnerable/main.tf
resource "aws_s3_bucket" "order_exports" {
  bucket = "store-order-exports"
  acl    = "public-read"
}

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

resource "aws_db_instance" "orders_db" {
  password             = "Sup3rSecret!2024"
  publicly_accessible  = true
  storage_encrypted    = false
}

resource "aws_iam_policy" "app_policy" {
  policy = jsonencode({
    Statement = [{ Effect = "Allow", Action = "*", Resource = "*" }]
  })
}
Enter fullscreen mode Exit fullscreen mode

Nothing here fails terraform validate — it's all valid HCL that will provision successfully. That's exactly why this category of risk needs a dedicated scanner instead of relying on the provider to reject something dangerous.

Running Checkov against it

pip install checkov
checkov -d terraform-vulnerable --compact
Enter fullscreen mode Exit fullscreen mode

Real, unedited output (excerpt):

Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public READ access."
    FAILED for resource: aws_s3_bucket.order_exports

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_sg

Check: CKV_AWS_16: "Ensure all data stored in the RDS is securely encrypted at rest"
    FAILED for resource: aws_db_instance.orders_db

Check: CKV_AWS_17: "Ensure all data stored in RDS is not publicly accessible"
    FAILED for resource: aws_db_instance.orders_db

Check: CKV_AWS_62: "Ensure IAM policies that allow full "*-*" administrative privileges are not created"
    FAILED for resource: aws_iam_policy.app_policy

Check: CKV_AWS_3: "Ensure all data stored in the EBS is securely encrypted"
    FAILED for resource: aws_ebs_volume.app_data

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

Every failed check carries a stable ID (CKV_AWS_*), which is what makes a finding something a team can track, suppress with a documented reason, or treat as a release blocker — instead of a one-off comment in a pull request.

Fixing what's actually a risk

Resource Finding Fix
S3 bucket Public-read ACL, no versioning, no encryption Remove the ACL, add aws_s3_bucket_public_access_block, aws_s3_bucket_versioning, aws_s3_bucket_server_side_encryption_configuration
Security group SSH and Postgres open to 0.0.0.0/0 Scope cidr_blocks to the office VPN and the app subnet specifically
RDS instance Public access, unencrypted storage, hardcoded password publicly_accessible = false, storage_encrypted = true, manage_master_user_password = true (AWS-managed via Secrets Manager)
IAM policy Action: "*" on Resource: "*" Scope to the two S3 actions the service actually needs, on the specific bucket ARN
EBS volume Unencrypted encrypted = true with a customer-managed KMS key
CloudWatch log group No retention period retention_in_days = 365
# terraform-fixed/main.tf
resource "aws_security_group" "app_sg" {
  ingress {
    description = "SSH from the office VPN only"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["203.0.113.0/24"]
  }
}

resource "aws_db_instance" "orders_db" {
  manage_master_user_password = true
  publicly_accessible         = false
  storage_encrypted           = true
  deletion_protection         = true
}

resource "aws_iam_policy" "app_policy" {
  policy = jsonencode({
    Statement = [{
      Effect   = "Allow"
      Action   = ["s3:GetObject", "s3:PutObject"]
      Resource = "${aws_s3_bucket.order_exports.arn}/*"
    }]
  })
}
Enter fullscreen mode Exit fullscreen mode

Re-scanning: from 36 failures to 6

$ checkov -d terraform-fixed --compact
Passed checks: 57, Failed checks: 6, Skipped checks: 0
Enter fullscreen mode Exit fullscreen mode

The remaining 6 are worth looking at individually, because none of them are the same category of risk as the original 36:

CKV2_AWS_62  Ensure S3 buckets should have event notifications enabled
CKV_AWS_144  Ensure that S3 bucket has cross-region replication enabled
CKV2_AWS_61  Ensure that an S3 bucket has a lifecycle configuration
CKV2_AWS_64  Ensure KMS key Policy is defined
CKV2_AWS_30  Ensure Postgres RDS has Query Logging enabled
CKV_AWS_354  Ensure RDS Performance Insights are encrypted using KMS CMKs
Enter fullscreen mode Exit fullscreen mode

Cross-region replication and lifecycle rules are cost and disaster-recovery decisions, not vulnerabilities — applying them blindly to satisfy a scanner would add infrastructure cost without addressing any actual exposure. Query logging and Performance Insights encryption are reasonable hardening that a team might genuinely choose to defer. This is the point a checklist mentality misses: the job of a SAST tool is to surface every deviation from a safe default, and the job of the engineer is still to decide which deviations matter for this specific system.

Wiring it into CI

# .github/workflows/checkov.yml
name: IaC SAST scan (Checkov)

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

jobs:
  checkov:
    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-fixed --compact --soft-fail-on LOW
Enter fullscreen mode Exit fullscreen mode

Running this on every terraform plan means a public S3 bucket or an open security group never reaches apply without someone explicitly seeing — and accepting — the finding first.

Conclusion

The first scan caught real, exploitable misconfiguration: a publicly readable bucket, an internet-facing database with a hardcoded password, an IAM policy with unrestricted * access. Those aren't edge cases — they're exactly the kind of thing that ends up in a breach disclosure. The second scan is the more instructive result: not every one of the remaining 6 findings deserved a fix, and treating a SAST tool's output as a literal to-do list rather than a prioritized list of deviations to evaluate is how teams either burn time hardening low-risk defaults or, worse, learn to ignore the tool altogether. Static analysis for infrastructure is most useful exactly where this example landed — as the thing that catches the dangerous default, while leaving the judgment calls to the team that owns the system.

Top comments (0)