Terraform Security Scanning Checklist: Checkov, tfsec, and Trivy IaC Commands
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
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
Free resources
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.
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.
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.

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.
The Mythos Brief is free.
AI that finds 27-year-old zero-days. What it means for your security program.
