PRACTITIONER GUIDE | CLOUD SECURITY
Practitioner GuideUpdated 12 min read

Terraform IaC Security Scanning: How to Find and Fix Misconfigurations Before They Reach Production Using tfsec, Checkov, and Trivy

70%
of cloud security incidents involve misconfigured resources that could have been detected by IaC scanning before deployment
100x
cheaper to fix a security misconfiguration in code review than after it reaches production and is exploited
tfsec
checks 300+ rules across AWS, Azure, and GCP Terraform providers, covering storage, networking, compute, IAM, and logging misconfigurations
SARIF
output format allows tfsec and Checkov results to integrate directly into GitHub Security tab and other SARIF-compatible platforms

SponsoredRetool

Retool's new app builder is where AI-generated code ships safely

Building apps with AI is easy. Getting them to production safely is another story.

Start building for free today

Terraform code that provisions an S3 bucket with public access, an RDS instance without encryption, or a security group with 0.0.0.0/0 ingress on all ports will create exactly that infrastructure when applied. Security scanning in CI/CD catches these before apply runs. This guide covers how to set up tfsec and Checkov in a GitHub Actions pipeline, the top misconfiguration patterns each tool catches, and how to handle suppression for legitimate exceptions without creating a blanket ignore policy.

Installing and Running tfsec Locally

tfsec is the most widely used Terraform-specific IaC scanner. Install and run:

# Install via brew (macOS)
brew install tfsec

# Or via Docker
docker pull aquasec/tfsec

# Run against a Terraform directory
tfsec ./terraform/ --format lovely

# JSON output for CI processing
tfsec ./terraform/ --format json > tfsec-results.json

# SARIF output for GitHub Security tab
tfsec ./terraform/ --format sarif > tfsec-results.sarif

tfsec evaluates each .tf file in the target directory and reports findings with:

  • Rule ID (e.g., aws-s3-no-public-access)
  • Severity (Critical, High, Medium, Low)
  • File and line number
  • Description and remediation guidance
  • Link to documentation

For a quick first run on an existing codebase, pipe to a severity filter:

tfsec ./terraform/ --minimum-severity HIGH --format lovely

This shows only High and Critical findings, giving you the most impactful starting point without overwhelming the team with hundreds of Medium and Low findings at once.

The Most Commonly Misconfigured Terraform Resources

These resource types produce the highest-frequency high-severity findings across AWS, Azure, and GCP:

AWS S3 Buckets:

# WRONG: Public access not blocked
resource "aws_s3_bucket" "data" {
  bucket = "company-data-bucket"
}
# Missing: aws_s3_bucket_public_access_block, aws_s3_bucket_server_side_encryption_configuration

# CORRECT:
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
}

AWS Security Groups:

# WRONG: All traffic inbound
resource "aws_security_group_rule" "allow_all" {
  type        = "ingress"
  from_port   = 0
  to_port     = 65535
  protocol    = "-1"
  cidr_blocks = ["0.0.0.0/0"] # tfsec: aws-vpc-no-public-ingress-sgr
}

Azure Storage Accounts:

# WRONG: HTTPS not required, minimum TLS not enforced
resource "azurerm_storage_account" "main" {
  name                     = "companystorage"
  resource_group_name      = azurerm_resource_group.main.name
  location                 = azurerm_resource_group.main.location
  account_tier             = "Standard"
  account_replication_type = "GRS"
  # Missing: enable_https_traffic_only, min_tls_version, network_rules
}

GCP Cloud Storage:

# WRONG: Public IAM binding
resource "google_storage_bucket_iam_member" "public" {
  bucket = google_storage_bucket.data.name
  role   = "roles/storage.objectViewer"
  member = "allUsers" # tfsec: google-storage-no-public-access
}
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.

GitHub Actions Integration for PR-Level Scanning

Add scanning to every pull request that modifies .tf files:

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

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

permissions:
  security-events: write  # required for SARIF upload
  contents: read

jobs:
  tfsec:
    name: tfsec
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Run tfsec
        uses: aquasecurity/tfsec-sarif-action@v0.1.4
        with:
          sarif_file: tfsec.sarif
          minimum_severity: HIGH

      - name: Upload SARIF file
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: tfsec.sarif

  checkov:
    name: Checkov
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Run Checkov
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: terraform/
          framework: terraform
          output_format: sarif
          output_file_path: checkov.sarif
          soft_fail: true  # set to false to block PRs on high findings

      - name: Upload Checkov SARIF
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: checkov.sarif

Set soft_fail: false once the existing finding backlog is resolved. With soft_fail: false, any High or Critical finding blocks the PR merge. Upload SARIF to the GitHub Security tab so findings are tracked as code scanning alerts alongside SAST results.

Suppressing False Positives Without Creating Security Gaps

Some findings are intentional. A public S3 bucket hosting a static website, a security group allowing 443 from the world for a load balancer, or a non-encrypted bucket for cost optimization in a staging environment. Suppression must be documented and scoped.

For tfsec, use inline ignore comments:

resource "aws_s3_bucket" "website" {
  bucket = "company-public-website"
  # tfsec:ignore:aws-s3-no-public-access -- Public website bucket, approved exception TICKET-4521
}

For Checkov, use checkov:skip comments:

resource "aws_security_group_rule" "alb_https" {
  type        = "ingress"
  from_port   = 443
  to_port     = 443
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
  # checkov:skip=CKV_AWS_25: ALB HTTPS inbound -- public-facing load balancer, approved
}

Policies for suppression:

  • Require a ticket reference in every suppression comment
  • Review all suppressions quarterly in a PR
  • Never suppress at a module level or directory level (file-level .tfsec-ignore files) without CISO approval
  • Track suppression count as a metric: if it grows faster than the team's review cadence, the tool is being misused

For custom organization rules that the public scanners do not cover (e.g., required tags on all resources, specific naming conventions), tfsec supports custom rule YAML files placed in a .tfsec/ directory in the repo.

Trivy for End-to-End Terraform and Secrets Scanning

Trivy is a broader security scanner that covers IaC, container images, and secrets in a single tool. For Terraform, Trivy replicates most tfsec and Checkov checks and adds secrets detection (hardcoded API keys, passwords, AWS access keys in Terraform variable files).

# Scan Terraform directory for misconfigurations and secrets
trivy fs ./terraform/ --scanners misconfig,secret --format table

# JSON output
trivy fs ./terraform/ --scanners misconfig,secret --format json > trivy-results.json

Trivy is particularly useful for detecting hardcoded secrets in .tfvars files and in resource arguments that should reference variable inputs:

# WRONG: Hardcoded credential in Terraform resource
resource "aws_db_instance" "main" {
  password = "hardcoded_password_123"  # Trivy: secret detection
  # Should be: password = var.db_password or a Secrets Manager reference
}

For secrets in Terraform state files (a separate problem): Terraform state can contain sensitive values in plaintext. Always use remote state with encryption at rest (S3 + SSE, Terraform Cloud, or Azure Backend with customer-managed keys). Add state file locations to your .gitignore and run Trivy on your repository to ensure no .tfstate files are committed.

The bottom line

Every terraform apply without a prior security scan is a production deployment with no code review for the security properties that matter most. Add tfsec or Checkov to your CI/CD pipeline at the PR level, upload results as SARIF to the Security tab, set a policy that High and Critical findings block merge, and require ticket references for every suppression. The scanning takes 30-60 seconds per run and prevents the kind of S3 bucket public access exposure that shows up in breach post-mortems.

Frequently asked questions

Which is better: tfsec, Checkov, or Trivy for Terraform scanning?

They overlap significantly on core rule coverage. tfsec is Terraform-specific and fast, with good HCL parsing. Checkov covers more frameworks (Terraform, Kubernetes, Dockerfiles, Ansible) and has a larger rule library including compliance framework mappings (CIS, SOC 2, PCI). Trivy adds secrets detection on top of misconfiguration scanning. Many teams run two tools in parallel: tfsec for fast PR feedback and Checkov for compliance-mapped reporting. Running all three is not unreasonable in high-compliance environments and takes under 2 minutes in CI.

Can IaC scanning detect runtime misconfigurations that are applied outside of Terraform?

No. IaC scanners analyze the Terraform code; they cannot see what exists in cloud accounts that was deployed manually, through another tool, or that drifted from the Terraform state after apply. For runtime misconfiguration detection, use a CSPM tool (Defender for Cloud, Wiz, Prisma Cloud, AWS Security Hub). IaC scanning and CSPM are complementary: IaC scanning prevents new misconfigurations from being deployed; CSPM detects existing misconfigurations regardless of how they were created.

How do I handle Terraform modules from the public registry that have findings?

When tfsec or Checkov scans a root module that calls a public registry module, findings in the module code are typically surfaced with the module source path. You cannot modify the module source, but you can: (1) pass variables to the module that enable secure configuration, (2) fork the module and apply fixes in your fork, (3) suppress the finding with a documented justification, or (4) switch to a different module with better security defaults. Check the module's GitHub issues first -- many popular modules have accepted PRs for security fixes that are not yet in a released version.

Should Terraform security scanning block the CI pipeline or just report?

Start with report-only mode (soft_fail: true) for the first 4-6 weeks to let the team understand the finding volume and resolve the existing backlog. Then switch Critical findings to blocking, then High findings. Medium and Low findings should remain advisory for most teams. The goal is to make the path of least resistance a secure one: if every PR fails due to 50 findings, engineers will configure blanket suppressions to unblock themselves, defeating the purpose.

How do I track and manage exceptions to IaC security rules at enterprise scale?

Use inline suppression comments (tfsec, Checkov, and Trivy all support suppression directives with a reason field) for one-time exceptions. Enforce a policy that every suppression must include a reason string and a Jira or issue tracker reference. In your CI pipeline, run a script that counts active suppressions per repository and outputs a suppression inventory as a build artifact. Route suppressions beyond a defined threshold for mandatory security review. Set suppression expiry by convention: if the reason references a ticket, the ticket closure should trigger suppression removal. Review the suppression inventory quarterly as part of your IaC security posture assessment.

How do I handle Terraform state file security?

Terraform state files contain plaintext values for all resource attributes, including secrets, passwords, and private keys that were passed to resources during provisioning. Never store state locally on developer machines or in version control. Use remote state backends with encryption at rest and access controls: Terraform Cloud, AWS S3 with server-side encryption and a DynamoDB lock table, Azure Blob Storage with encryption, or GCS with a customer-managed key. Restrict state file access via IAM so only the CI/CD role and designated operators can read or write state. Enable state locking to prevent concurrent applies. For particularly sensitive state (containing database credentials or certificate private keys), consider using external secret references rather than passing secrets directly to resource arguments.

Sources & references

  1. Aqua Security: tfsec documentation
  2. Bridgecrew: Checkov documentation
  3. Aqua Security: Trivy documentation
  4. HashiCorp: Terraform security best practices

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, daily, 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 every morning.

Black Hat Giveaway

Win a $2,495 Black Hat pass.

Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.

Joins Decryption Digest daily briefing. Unsubscribe anytime.

Giveaway: Black Hat USA 2026 Full-Access Pass ($2,495 value)

Details →
Daily Briefing

Subscribe to enter the giveaway

Every subscriber is automatically entered. You also get daily threat intel every morning: zero-days, ransomware, and nation-state campaigns. Free. No spam.

Already subscribed? You're already entered.

Giveaway

Win a $2,495 Black Hat USA 2026 pass.