Static Security Scanning
A configuration can pass terraform validate and tflint and still ship a publicly readable S3 bucket, an unencrypted database, or a security group open to 0.0.0.0/0. Those are not syntax errors — they are insecure defaults, and the only way to catch them before apply is a scanner that knows what “good” looks like. Static security scanners read your HCL (or the JSON plan) and match it against a library of security and compliance policies. This page covers the three you will actually reach for: Checkov, tfsec, and Trivy. All three work with Terraform and OpenTofu.
What the scanners catch
These tools encode hundreds of rules drawn from CIS benchmarks, cloud provider best practices, and compliance frameworks (PCI-DSS, HIPAA, SOC 2). Each rule maps to a concrete misconfiguration: missing encryption, overly permissive IAM, public network exposure, disabled logging. Crucially, they understand resource defaults — if encryption is off unless you explicitly enable it, the scanner flags the resource even though your HCL never mentions encryption at all.
Consider this configuration. It is syntactically valid and would apply cleanly:
resource "aws_s3_bucket" "logs" {
bucket = "devcraftly-app-logs"
}
resource "aws_security_group" "web" {
name = "web-sg"
description = "Web tier"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
Two serious problems hide here: the bucket has no encryption, versioning, or public-access block, and SSH is open to the entire internet. Run Checkov against the directory:
checkov --directory . --quiet --compact
Output:
_ _
___| |__ ___ ___| | _______ __
/ __| '_ \ / _ \/ __| |/ / _ \ \ / /
| (__| | | | __/ (__| < (_) \ V /
\___|_| |_|\___|\___|_|\_\___/ \_/
terraform scan results:
Passed checks: 2, Failed checks: 4, Skipped checks: 0
Check: CKV_AWS_18: "Ensure the S3 bucket has access logging enabled"
FAILED for resource: aws_s3_bucket.logs
File: /main.tf:1-3
Check: CKV_AWS_19: "Ensure all data stored in the S3 bucket is securely encrypted at rest"
FAILED for resource: aws_s3_bucket.logs
File: /main.tf:1-3
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:5-15
Check: CKV_AWS_21: "Ensure all data stored in the S3 bucket have versioning enabled"
FAILED for resource: aws_s3_bucket.logs
File: /main.tf:1-3
Each finding cites the rule ID, a human description, the resource, and the exact line range — enough to fix it without guesswork.
Choosing a tool
The three overlap heavily. The practical differences are about scope and where each is heading.
| Tool | Maintainer | Scope | Notes |
|---|---|---|---|
| Checkov | Prisma Cloud / Bridgecrew | Terraform, CloudFormation, K8s, Helm, Dockerfile, ARM | Largest policy library; supports custom Python and YAML policies |
| tfsec | Aqua Security | Terraform only | Fast; now in maintenance mode, folded into Trivy |
| Trivy | Aqua Security | IaC, containers, filesystems, SBOM, secrets | The successor to tfsec; one binary for misconfig + vuln + secret scanning |
For greenfield work, Trivy is the forward-looking default — trivy config . runs the former tfsec engine plus a unified scanner. Checkov remains the richest choice when you need broad coverage across many file types or custom organizational policies.
trivy config --severity HIGH,CRITICAL .
Output:
main.tf (terraform)
===================
Tests: 6 (SUCCESSES: 4, FAILURES: 2)
Failures: 2 (HIGH: 1, CRITICAL: 1)
CRITICAL: Security group rule allows ingress from public internet.
AVD-AWS-0107 — Public ingress to port 22 (SSH)
main.tf:8-13
HIGH: Bucket does not have encryption enabled.
AVD-AWS-0088
main.tf:1-3
Suppressing false positives
Not every finding is actionable — a public-facing load balancer bucket may legitimately need broad access. Suppress individual rules inline with a comment so the exemption lives next to the code and survives review. Checkov reads checkov:skip comments:
resource "aws_s3_bucket" "public_assets" {
#checkov:skip=CKV_AWS_20:Static website assets are intentionally public
bucket = "devcraftly-public-assets"
}
Trivy uses the same idea with a trivy:ignore annotation, and supports a central .trivyignore file for blanket exemptions. Always include a justification after the colon — a skip without a reason is an unreviewed risk.
Suppress narrowly, by rule ID and resource, and never disable a whole rule class globally to silence one finding. A blanket
--skip-checkin CI hides every future instance of that misconfiguration, not just the one you reviewed.
Custom policy packs
The built-in checks enforce industry baselines; your organization will have rules of its own — mandatory tags, an approved AMI list, a banned region. Checkov supports custom policies in YAML for simple attribute matches and Python for complex logic. This YAML policy fails any resource missing a CostCenter tag:
metadata:
id: "CKV_DEVCRAFTLY_1"
name: "Every resource must carry a CostCenter tag"
category: "GENERAL_SECURITY"
definition:
cond_type: "attribute"
resource_types:
- "aws_s3_bucket"
- "aws_instance"
attribute: "tags.CostCenter"
operator: "exists"
Point Checkov at the directory holding your policies with --external-checks-dir, and they run alongside the built-ins.
CI integration
Scanners are single binaries and exit non-zero on a failed check, so they gate merges naturally. Emit SARIF so findings appear as inline annotations in the pull-request diff and the GitHub Security tab.
name: security-scan
on: [pull_request]
jobs:
checkov:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bridgecrewio/checkov-action@v12
with:
directory: .
output_format: sarif
output_file_path: results.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: results.sarif
Security scanning sits one rung above linting on the quality ladder — after validate and tflint, before plan-based policy as code. Run it on raw HCL for fast feedback, and optionally against the JSON plan (checkov -f plan.json) to catch misconfigurations that only appear after variables and modules resolve.
Best Practices
- Pin the scanner version in CI so a new rule release never breaks a pipeline you did not review.
- Scan the JSON plan, not just the HCL, when modules or variables hide the final resource shape.
- Suppress findings inline by specific rule ID with a written justification — never disable a rule globally to silence one case.
- Standardize on Trivy for new projects (it absorbs tfsec); keep Checkov where you need broad file-type or custom-policy coverage.
- Codify organization-specific rules (tags, regions, AMIs) as custom policies rather than relying on manual review.
- Emit SARIF in CI to surface findings as inline pull-request annotations and feed the Security tab.
- Run the scan early enough that an insecure default blocks the merge, not the deploy.