DEV Community

Cover image for Applying SAST to Infrastructure as Code (Without TFSec)
IKER ALBERTO SIERRA RUIZ
IKER ALBERTO SIERRA RUIZ

Posted on

Applying SAST to Infrastructure as Code (Without TFSec)

Static analysis isn't just for application source code. Terraform, Pulumi, OpenTofu, and CloudFormation files are code too — and they get misconfigured just as often as a backend service. A public S3 bucket, a security group open to 0.0.0.0/0, or an unencrypted RDS instance are all bugs you can catch before apply ever runs.

TFSec is the tool most people reach for first, but it's not the only option on the OWASP Source Code Analysis Tools list. In this article I'll use Checkov, a free, open-source policy-as-code scanner built by Bridgecrew (now part of Palo Alto Networks), to scan a Terraform project end to end — from a local scan to a GitHub Actions gate that blocks merges on critical misconfigurations.

The same approach works with OpenTofu and Pulumi projects too, since Checkov understands HCL directly and also has native support for Pulumi's rendered plan output and CloudFormation/ARM/Kubernetes manifests.

Why Checkov?

  • 100% open source (Apache 2.0), actively maintained, thousands of built-in policies.
  • Understands Terraform, OpenTofu, CloudFormation, Kubernetes, Helm, Dockerfile, ARM, Serverless Framework, and Pulumi (via cdktf/synthesized plans) — one tool across most of your IaC surface.
  • No account or API key required to run locally or in CI.
  • Supports custom policies written in Python or YAML if the built-in rule set doesn't cover something specific to your org. ## 1. The sample infrastructure

A small AWS setup with a few intentionally introduced misconfigurations — the kind that get merged during a rushed sprint:

# main.tf
provider "aws" {
  region = "us-east-1"
}

resource "aws_s3_bucket" "data" {
  bucket = "company-app-data-bucket"
}

# Vulnerable: bucket has no encryption, no versioning, and is publicly readable
resource "aws_s3_bucket_acl" "data_acl" {
  bucket = aws_s3_bucket.data.id
  acl    = "public-read"
}

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Allow web traffic"

  # Vulnerable: SSH open to the entire internet
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

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

resource "aws_db_instance" "app_db" {
  identifier        = "app-db"
  engine            = "postgres"
  instance_class    = "db.t3.micro"
  allocated_storage = 20

  # Vulnerable: hardcoded credentials
  username = "admin"
  password = "SuperSecret123!"

  # Vulnerable: no encryption at rest, publicly accessible
  storage_encrypted   = false
  publicly_accessible = true
  skip_final_snapshot = true
}

resource "aws_iam_policy" "broad_access" {
  name = "broad-access-policy"

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

Five issues live here: a public S3 bucket with no encryption or versioning, SSH exposed to the world, a hardcoded database password, an unencrypted publicly-accessible RDS instance, and an IAM policy with wildcard permissions.

2. Installing and running Checkov locally

pip install checkov

# Scan a directory
checkov -d .

# Scan a single file
checkov -f main.tf
Enter fullscreen mode Exit fullscreen mode

Sample output:

Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public READ access"
    FAILED for resource: aws_s3_bucket_acl.data_acl
    File: /main.tf:9-12

Check: CKV_AWS_21: "Ensure the S3 bucket has versioning enabled"
    FAILED for resource: aws_s3_bucket.data
    File: /main.tf:6-8

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.web
    File: /main.tf:14-27

Check: CKV_AWS_16: "Ensure RDS instances have storage encrypted"
    FAILED for resource: aws_db_instance.app_db
    File: /main.tf:34-47

Check: CKV_AWS_17: "Ensure RDS instances are not publicly accessible"
    FAILED for resource: aws_db_instance.app_db
    File: /main.tf:34-47

Check: CKV_SECRET_6: "Base64 High Entropy String"
    FAILED for resource: aws_db_instance.app_db.password
    File: /main.tf:41

Check: CKV_AWS_1: "Ensure IAM policies that allow full '*-*' administrative privileges are not created"
    FAILED for resource: aws_iam_policy.broad_access
    File: /main.tf:50-62

Passed checks: 12, Failed checks: 7, Skipped checks: 0
Enter fullscreen mode Exit fullscreen mode

Checkov ships with over 1,000 built-in policies for AWS, Azure, and GCP, so a small file like this often surfaces more than what you deliberately planted.

3. Tuning it: a config file for reproducible scans

Create a .checkov.yaml at the repo root so local runs and CI runs always agree:

# .checkov.yaml
directory:
  - .
framework:
  - terraform
skip-check:
  - CKV_AWS_8   # skip if you intentionally don't use launch config encryption in dev sandboxes
soft-fail-on:
  - MEDIUM
  - LOW
hard-fail-on:
  - CRITICAL
  - HIGH
output: cli
compact: true
Enter fullscreen mode Exit fullscreen mode

Run it with:

checkov -d . --config-file .checkov.yaml
Enter fullscreen mode Exit fullscreen mode

hard-fail-on / soft-fail-on is the key mechanism here: critical and high findings break the build, medium/low ones get reported without blocking — the same "gate on what matters" principle you'd apply to any SAST tool.

4. Machine-readable output for pipelines

checkov -d . --config-file .checkov.yaml -o json --output-file-path checkov-report.json
checkov -d . --config-file .checkov.yaml -o sarif --output-file-path checkov-report.sarif
Enter fullscreen mode Exit fullscreen mode

The SARIF output is worth calling out: GitHub natively understands SARIF and can render findings directly in the Security > Code scanning alerts tab and inline on pull requests, without needing a third-party dashboard.

5. Automating it with GitHub Actions

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

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

jobs:
  checkov-scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Run Checkov
        id: checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: .
          framework: terraform
          config_file: .checkov.yaml
          output_format: cli,sarif
          output_file_path: console,checkov-report.sarif
          soft_fail: false

      - name: Upload SARIF to GitHub Security tab
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: checkov-report.sarif
Enter fullscreen mode Exit fullscreen mode

This runs on every push and pull request, fails the job when a critical/high finding hits (per the config file's hard-fail-on), and publishes results as native GitHub code-scanning alerts so reviewers see them inline on the diff — no external service required.

6. Fixing the findings

The remediated version of the same file, which passes with zero critical/high findings:

provider "aws" {
  region = "us-east-1"
}

resource "aws_s3_bucket" "data" {
  bucket = "company-app-data-bucket"
}

resource "aws_s3_bucket_versioning" "data_versioning" {
  bucket = aws_s3_bucket.data.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "data_encryption" {
  bucket = aws_s3_bucket.data.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "aws_s3_bucket_public_access_block" "data_block" {
  bucket                  = aws_s3_bucket.data.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Allow web traffic"

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  # SSH restricted to a known bastion / VPN CIDR instead of the whole internet
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/16"]
  }
}

resource "aws_db_instance" "app_db" {
  identifier        = "app-db"
  engine            = "postgres"
  instance_class    = "db.t3.micro"
  allocated_storage = 20

  username                    = "admin"
  manage_master_user_password = true # credentials managed by AWS Secrets Manager

  storage_encrypted   = true
  publicly_accessible = false
  skip_final_snapshot = true
}

resource "aws_iam_policy" "scoped_access" {
  name = "scoped-s3-read-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["s3:GetObject", "s3:ListBucket"]
        Resource = [aws_s3_bucket.data.arn, "${aws_s3_bucket.data.arn}/*"]
      }
    ]
  })
}
Enter fullscreen mode Exit fullscreen mode

Running checkov -d . --config-file .checkov.yaml again returns zero critical or high findings.

Takeaways

  • IaC misconfigurations are just as scannable as application code, and TFSec isn't the only tool for the job — the OWASP list points to several free alternatives, including Checkov, Terrascan, and KICS.
  • Checkov's multi-framework support (Terraform, OpenTofu, Pulumi, CloudFormation, Kubernetes) makes it a reasonable default if your org has more than one IaC tool in play.
  • SARIF output plus GitHub's native code-scanning integration gets you inline PR feedback without standing up a separate dashboard.
  • Gate on severity (hard-fail-on: CRITICAL, HIGH), report everything else — this keeps the pipeline useful instead of noisy. ## Demo repository

The vulnerable Terraform config, the fixed version, .checkov.yaml, and the GitHub Actions workflow above are all in this repo, ready to fork and run:

👉 https://github.com/<your-username>/sast-checkov-iac-demo

Fork it, open a pull request, and watch the Security tab populate with findings automatically.


If this was useful, drop a comment with which IaC tool you'd like to see covered next — Pulumi with native TypeScript policies, or OpenTofu, are both on my list.

Top comments (0)