72%
of cloud environments had at least one publicly exposed storage bucket (Wiz 2024)
3,000+
built-in security policies in Checkov covering Terraform, CloudFormation, ARM, and more
3 tools
covered: Checkov, tfsec, and Trivy -- all free and open source

Manual code review catches architecture decisions; automated IaC scanning catches resource-level misconfigurations that are easy to overlook in a wall of HCL. This checklist covers tool setup, essential scan commands for Checkov, tfsec, and Trivy, and side-by-side bad-vs-fixed HCL examples for the four most commonly exploited misconfiguration categories: S3 buckets, security groups, IAM policies, and RDS instances. Includes GitHub Actions and pre-commit integration templates.

Why IaC Scanning Catches What Manual Review Misses

Infrastructure as Code reviews in pull requests catch architecture problems, but resource-level security misconfigs -- an S3 bucket missing block-public-access, a security group with 0.0.0.0/0 on SSH, an RDS instance without encryption -- are easy to miss in a wall of HCL. Automated IaC scanners check hundreds of CIS Benchmark and cloud provider best-practice rules in seconds. A 2024 Wiz cloud security report found that 72% of cloud environments had at least one publicly exposed storage bucket; most had been created by Terraform without encryption or access controls.

Tool Setup: Install All Three Scanners

# Checkov
pip install checkov
# or: brew install checkov

# tfsec
brew install tfsec
# or: go install github.com/aquasecurity/tfsec/cmd/tfsec@latest

# Trivy (covers Terraform + containers + SBOMs)
brew install trivy
# or: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh

# Verify installs
checkov --version
tfsec --version
trivy --version

For CI environments, use pinned Docker images:

docker pull bridgecrew/checkov:3.2.0
docker pull aquasec/tfsec:v1.28.10
docker pull aquasec/trivy:0.52.0
Free daily briefing

Briefings like this, every morning before 9am.

Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.

Quick Scan: Run All Three Against a Terraform Directory

# Checkov: full scan, show only FAILED checks
checkov -d . --quiet --compact

# tfsec: all severities, show code snippets
tfsec . --include-passed=false

# Trivy IaC scan
trivy config .

Checkov with SARIF output for GitHub Advanced Security:

checkov -d . --output sarif --output-file results.sarif

tfsec with JSON output for custom reporting:

tfsec . --format json > tfsec-results.json

Checkov scan a single file:

checkov -f main.tf

Checkov scan with specific framework only:

checkov -d . --framework terraform

S3 Bucket Misconfigs: Bad HCL vs. Fixed HCL

BAD -- public ACL, no encryption, logging disabled:

resource "aws_s3_bucket" "data" {
  bucket = "company-data-bucket"
  acl    = "public-read"  # CKV_AWS_20, CKV2_AWS_65
  # Missing: server_side_encryption_configuration
  # Missing: logging
  # Missing: versioning
}

FIXED:

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

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

resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
  bucket = aws_s3_bucket.data.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
  }
}

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

resource "aws_s3_bucket_logging" "data" {
  bucket        = aws_s3_bucket.data.id
  target_bucket = aws_s3_bucket.logs.id
  target_prefix = "data-access/"
}

Checkov checks that catch this: CKV_AWS_20 (public ACL), CKV_AWS_19 (encryption), CKV_AWS_21 (versioning), CKV_AWS_18 (logging), CKV2_AWS_6 (block public access)

Security Group Misconfigs: Catch Wide-Open Ingress

BAD -- SSH and RDP open to the world:

resource "aws_security_group" "bastion" {
  name = "bastion-sg"

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

  ingress {
    from_port   = 3389
    to_port     = 3389
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]  # CKV_AWS_25
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]  # CKV_AWS_25
  }
}

FIXED -- restrict SSH to corporate egress IP range:

resource "aws_security_group" "bastion" {
  name = "bastion-sg"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.corporate_ip_range]  # e.g. ["203.0.113.0/24"]
  }

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

tfsec command to scan only security group rules:

tfsec . --include-checks aws-ec2-no-public-ingress-sgr,aws-ec2-no-public-ip-subnet

IAM Policy Misconfigs: Wildcard Actions and Resources

BAD -- wildcard action and resource:

resource "aws_iam_policy" "app_policy" {
  name = "app-full-access"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = "*"       # CKV_AWS_40
      Resource = "*"       # CKV_AWS_40
    }]
  })
}

FIXED -- scoped to specific S3 bucket and DynamoDB table:

resource "aws_iam_policy" "app_policy" {
  name = "app-s3-dynamo-access"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:PutObject",
          "s3:DeleteObject"
        ]
        Resource = "arn:aws:s3:::${var.app_bucket}/*"
      },
      {
        Effect = "Allow"
        Action = [
          "dynamodb:GetItem",
          "dynamodb:PutItem",
          "dynamodb:Query"
        ]
        Resource = aws_dynamodb_table.app.arn
      }
    ]
  })
}

Checkov checks: CKV_AWS_40 (wildcard in IAM), CKV_AWS_274 (no wildcard resource for sensitive actions)

RDS Misconfigs: Encryption, Public Access, and Backups

BAD -- publicly accessible, no encryption:

resource "aws_db_instance" "app" {
  identifier        = "app-db"
  engine            = "postgres"
  instance_class    = "db.t3.micro"
  publicly_accessible = true   # CKV_AWS_17
  storage_encrypted   = false  # CKV_AWS_16
  backup_retention_period = 0  # CKV_AWS_133
  deletion_protection = false  # CKV_AWS_293
  # Missing: multi_az
  # Missing: performance_insights_enabled
}

FIXED:

resource "aws_db_instance" "app" {
  identifier              = "app-db"
  engine                  = "postgres"
  instance_class          = "db.t3.micro"
  publicly_accessible     = false
  storage_encrypted       = true
  kms_key_id              = aws_kms_key.rds.arn
  backup_retention_period = 7
  deletion_protection     = true
  multi_az                = true
  performance_insights_enabled = true

  db_subnet_group_name   = aws_db_subnet_group.private.name
  vpc_security_group_ids = [aws_security_group.rds.id]
}

Scan for RDS issues only:

checkov -d . --check CKV_AWS_16,CKV_AWS_17,CKV_AWS_133,CKV_AWS_293,CKV_AWS_157

GitHub Actions Integration: Block Bad Terraform on PR

# .github/workflows/terraform-security.yml
name: Terraform Security Scan

on:
  pull_request:
    paths:
      - '**.tf'
      - '**.tfvars'

jobs:
  checkov:
    runs-on: ubuntu-latest
    permissions:
      security-events: write  # for SARIF upload
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Run Checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: .
          framework: terraform
          output_format: cli,sarif
          output_file_name: results.sarif
          soft_fail: false  # fail the job on any HIGH/CRITICAL

      - name: Upload SARIF to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: results.sarif

  tfsec:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run tfsec
        uses: aquasecurity/tfsec-action@v1.0.0
        with:
          soft_fail: false

Mark both jobs as required status checks in branch protection settings. This blocks merges when new security issues are introduced.

Pre-commit Hook: Catch Issues Before Push

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/bridgecrewio/checkov
    rev: 3.2.0
    hooks:
      - id: checkov
        args: ['--framework', 'terraform', '--quiet']

  - repo: https://github.com/aquasecurity/tfsec
    rev: v1.28.10
    hooks:
      - id: tfsec
# Install hooks
pip install pre-commit
pre-commit install

# Run manually against all files
pre-commit run --all-files

This gives developers immediate feedback before code ever reaches a PR, reducing back-and-forth with security teams.

Suppressing False Positives Consistently

Inline suppression (use sparingly, require justification in comment):

resource "aws_s3_bucket" "public-assets" {
  bucket = "company-public-cdn"
  # checkov:skip=CKV_AWS_20:Public CDN bucket intentionally public, reviewed by security team 2026-04-01
}

tfsec inline:

ingress {
  from_port   = 443
  to_port     = 443
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]  #tfsec:ignore:aws-ec2-no-public-ingress-sgr: HTTPS ingress is intentional for public-facing ALB
}

Checkov config file for project-wide suppressions:

# .checkov.yaml
skip-check:
  - CKV_AWS_144  # S3 cross-region replication not required for this env
  - CKV_AWS_111  # S3 lifecycle not required for temp bucket

Document every suppression in your security exception log with: check ID, resource, business justification, approver, and expiry date.

The bottom line

Shift IaC security left. Run Checkov as a pre-commit hook so developers fix misconfigurations before they reach a PR. Run it again in CI to block merges. Track suppression exceptions in a dedicated log with expiry dates. Every finding you catch in code review is a misconfiguration that never reaches production.

Frequently asked questions

What is the difference between Checkov, tfsec, and Trivy for Terraform scanning?

Checkov (Bridgecrew/Prisma) has the largest built-in policy library (3,000+) covering Terraform, CloudFormation, ARM, Kubernetes, and Dockerfile. It outputs SARIF for GitHub Advanced Security integration. tfsec is narrower but faster and integrates cleanly with pre-commit hooks. Trivy IaC scanning covers Terraform plus container images and SBOMs in one tool, useful if you want a single scanner across the stack. All three are free and open source.

How do I integrate Terraform security scanning into a GitHub Actions pipeline?

Add a job that runs after terraform plan: uses: bridgecrewio/checkov-action@master with inputs dir: . output_format: sarif output_file_name: results.sarif. Then add an upload-sarif step to surface findings in GitHub Security tab. For tfsec: run tfsec . --format sarif > tfsec.sarif as a run step. Block merges by setting the job as a required status check.

Can these scanners detect hardcoded secrets in Terraform files?

Yes. Checkov check CKV_SECRET_* and tfsec's sensitive-value detection both flag hardcoded passwords, tokens, and API keys in variable default values or resource arguments. However, they miss secrets embedded in user_data scripts as base64. Use Trufflehog or detect-secrets as a complementary pre-commit hook for comprehensive secret scanning.

How do I suppress a false positive in Checkov?

Add an inline comment: # checkov:skip=CKV_AWS_18:Reason why this is a false positive. For tfsec use tfsec:ignore:AVD-AWS-0089 on the line above the argument. Both tools also support .checkov.yaml and .tfsec/config.yml config files for project-wide suppressions with mandatory justification fields.

What Terraform misconfigurations are most commonly exploited in the wild?

The top exploited configs are: S3 buckets with public ACLs or no block-public-access settings, security groups with 0.0.0.0/0 ingress on port 22/3389, IAM policies using wildcard actions or resources, RDS instances without encryption or with public accessibility enabled, and Lambda functions with overly permissive execution roles.

Does Terraform Cloud or Terraform Enterprise have built-in security scanning?

Terraform Cloud has a Sentinel policy framework that can enforce security rules before apply. HashiCorp also integrates with Checkov via a run task. However, Sentinel is a paid feature (Plus plan and above). For free alternatives, run Checkov in a GitHub Actions pre-plan job triggered on pull requests.

How do I scan Terraform modules I pull from the registry?

Run terraform init to download modules, then point Checkov at the .terraform/modules directory: checkov -d .terraform/modules --external-modules-download-path .terraform. Alternatively, pin modules to a specific version tag and scan the module source repository separately in your supply chain security workflow.

Sources & references

  1. Checkov Documentation
  2. Wiz State of Cloud Security 2024
  3. CIS AWS Foundations Benchmark
  4. tfsec
  5. MITRE ATT&CK
  6. AWS Security Hub Documentation

Free resources

25
Free download

Critical CVE Reference Card 2025–2026

25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.

No spam. Unsubscribe anytime.

Free download

Ransomware Incident Response Playbook

Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.

No spam. Unsubscribe anytime.

Free newsletter

Get threat intel before your inbox does.

50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, weekly, no spam.

Unsubscribe anytime. We never sell your data.

Eric Bang
Author

Founder & Cybersecurity Evangelist, Decryption Digest

Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals weekly.

Free Brief

The Mythos Brief is free.

AI that finds 27-year-old zero-days. What it means for your security program.

Joins Decryption Digest. Unsubscribe anytime.

Daily Briefing

Get briefings like this every morning

Actionable threat intelligence for working practitioners. Free. No spam. Trusted by 50,000+ SOC analysts, CISOs, and security engineers.

Unsubscribe anytime.

Mythos Brief

Anthropic's AI finds zero-days your scanners miss.