tfsec: Static Security Analysis for Terraform Code
tfsec is a static security scanner purpose-built for Terraform. Where Checkov supports many IaC formats, tfsec focuses entirely on Terraform and goes deeper: it resolves variable references, evaluates module calls, and understands Terraform's type system. The result is fewer false positives on complex configurations.
Originally built by Aqua Security and now part of the broader Trivy ecosystem, tfsec is widely used in Terraform-heavy organizations as a fast, developer-friendly security gate.
Installation
# Homebrew (macOS)
brew install tfsec
<span class="hljs-comment"># Direct binary download (Linux/macOS/Windows)
curl -s https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh <span class="hljs-pipe">| bash
<span class="hljs-comment"># Go install
go install github.com/aquasecurity/tfsec/cmd/tfsec@latest
<span class="hljs-comment"># Docker
docker pull aquasec/tfsecVerify:
tfsec --versionBasic Usage
Run tfsec against a Terraform directory:
# Scan current directory
tfsec .
<span class="hljs-comment"># Scan a specific directory
tfsec ./terraform/prod
<span class="hljs-comment"># Output as JSON (for CI processing)
tfsec . --format json > security-results.json
<span class="hljs-comment"># Output as SARIF (for GitHub code scanning)
tfsec . --format sarif --out results.sarifSample output:
Result #1 HIGH Bucket does not have encryption enabled
────────────────────────────────────────
main.tf:12-18
aws_s3_bucket.data_bucket
────────────────────────────────────────
12 | resource "aws_s3_bucket" "data_bucket" {
13 | bucket = "my-data-bucket"
14 | acl = "private"
15 |
16 | tags = {
17 | Name = "Data Bucket"
18 | }
19 | }
────────────────────────────────────────
ID aws-s3-enable-bucket-encryption
Impact Bucket contents may be readable if access is compromised
Resolution Enable AES-256 or CMK encryption for the bucket
More Info https://aquasecurity.github.io/tfsec/latest/checks/aws/s3/enable-bucket-encryption/Severity Levels
tfsec classifies findings as:
- CRITICAL — immediate exploitation possible (e.g., open security group to 0.0.0.0/0 on port 22)
- HIGH — significant security risk (missing encryption, disabled logging)
- MEDIUM — moderate risk (weak TLS versions, overly permissive IAM)
- LOW — best-practice deviation (missing tags, verbose logging not enabled)
Filter by severity:
# Only show HIGH and CRITICAL
tfsec . --minimum-severity HIGH
<span class="hljs-comment"># Show only CRITICAL in CI
tfsec . --minimum-severity CRITICALVariable Resolution
tfsec's strongest technical feature: it resolves Terraform variable references and evaluates the check against the actual value.
# variables.tf
variable "enable_encryption" {
type = bool
default = false
}
# main.tf
resource "aws_s3_bucket_server_side_encryption_configuration" "example" {
bucket = aws_s3_bucket.example.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = var.enable_encryption ? "AES256" : "none"
}
}
}With default = false, tfsec resolves the ternary and reports the encryption failure. With default = true, it passes. Checkov typically treats the variable reference as unknown.
You can provide variable values for scanning:
tfsec . --tf-vars-file terraform.tfvarsIgnoring Rules
Suppress a check for a specific resource with an inline comment:
resource "aws_s3_bucket" "public_assets" {
bucket = "my-public-static-assets"
#tfsec:ignore:aws-s3-no-public-buckets
#tfsec:ignore:aws-s3-block-public-acls
tags = {
Visibility = "public"
Purpose = "CDN origin"
}
}Ignore with an expiry date (check auto-reinstates after the date):
#tfsec:ignore:aws-s3-enable-bucket-encryption:exp:2026-12-31Ignore for an entire directory using .tfsec/config.yml:
# .tfsec/config.yml
exclude:
- aws-s3-enable-bucket-logging # Internal buckets don't need access logs
- aws-cloudwatch-log-group-customer-key # Using AWS-managed keys for CloudWatch
# Minimum severity to report
minimum_severity: MEDIUMCustom Checks
Write custom checks in YAML to enforce organization-specific rules:
# .tfsec/checks/require_team_tag.yaml
checks:
- code: CUS001
description: AWS resources must have a Team tag
impact: Untagged resources can't be attributed to a cost center
resolution: Add a Team tag to the resource
requiredTypes:
- resource
requiredLabels:
- aws_instance
- aws_s3_bucket
- aws_db_instance
- aws_lambda_function
- aws_ecs_service
severity: MEDIUM
matchSpec:
name: tags
action: isPresent
errorMessage: Resource is missing required 'Team' tag
relatedLinks:
- https://docs.internal/standards/taggingMore complex custom check with attribute value assertion:
# .tfsec/checks/approved_regions.yaml
checks:
- code: CUS002
description: Resources must be in approved regions
impact: Data sovereignty and compliance requirements
resolution: Use only us-east-1, eu-west-1, or ap-southeast-1
requiredTypes:
- resource
requiredLabels:
- aws_db_instance
- aws_s3_bucket
severity: HIGH
matchSpec:
name: region
action: in
value:
- us-east-1
- eu-west-1
- ap-southeast-1
errorMessage: Resource region is not in the approved listRun with custom checks:
tfsec . --custom-check-dir ./.tfsec/checksModule Scanning
tfsec scans called modules, including remote modules. To scan a root module that references child modules:
# Include downloaded modules (requires terraform init first)
terraform init
tfsec . --include-ignored-modulesFor internal module registries, tfsec supports scanning module sources directly:
tfsec ./modules/vpc
tfsec ./modules/rdsOutput Formats
tfsec supports multiple output formats for different consumers:
# Terminal (default) — human readable
tfsec .
<span class="hljs-comment"># JSON — for custom processing
tfsec . --format json <span class="hljs-pipe">| jq <span class="hljs-string">'.results[] | select(.severity == "HIGH") <span class="hljs-pipe">| .description'
<span class="hljs-comment"># SARIF — GitHub code scanning integration
tfsec . --format sarif --out tfsec.sarif
<span class="hljs-comment"># JUnit XML — Jenkins / test report integration
tfsec . --format junit-xml --out tfsec-junit.xml
<span class="hljs-comment"># CSV — spreadsheet import
tfsec . --format csv --out tfsec.csv
<span class="hljs-comment"># Markdown — PR comment
tfsec . --format markdown --out tfsec-report.mdCI Pipeline Integration
GitHub Actions
# .github/workflows/tfsec.yml
name: tfsec Security Scan
on:
pull_request:
paths:
- 'terraform/**'
jobs:
tfsec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tfsec
uses: aquasecurity/tfsec-action@v1.0.0
with:
working_directory: terraform/
format: sarif
sarif_file: tfsec.sarif
minimum_severity: MEDIUM
- name: Upload SARIF to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: tfsec.sarif
- name: PR Comment on Failure
if: failure()
uses: aquasecurity/tfsec-pr-commenter-action@v1.2.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
working_directory: terraform/GitLab CI
tfsec:
stage: security
image: aquasec/tfsec:latest
script:
- tfsec ./terraform --format sarif --out tfsec.sarif --minimum-severity MEDIUM
artifacts:
when: always
reports:
sast: tfsec.sarif
rules:
- changes:
- terraform/**/*tfsec vs Checkov: When to Use Which
Both tools scan Terraform for security issues, but they have different strengths:
| Feature | tfsec | Checkov |
|---|---|---|
| Terraform focus | Purpose-built | Multi-format (Terraform, K8s, CloudFormation, Helm, ARM) |
| Variable resolution | Deep (evaluates ternaries, locals) | Limited |
| Custom checks | YAML (simpler) | YAML + Python (more powerful) |
| False positive rate | Lower (for Terraform) | Higher on complex configs |
| Built-in check count | ~400 | 1,000+ |
| Kubernetes checks | No | Yes |
| Output formats | CLI, JSON, SARIF, JUnit, CSV, Markdown | CLI, JSON, SARIF, JUnit, CycloneDX |
| CI integrations | GitHub, GitLab, Bitbucket | Same + Prisma Cloud integration |
| Community | Active (Aqua/Trivy project) | Active (Bridgecrew/Prisma) |
Use tfsec when: Your primary IaC language is Terraform and you want fewer false positives. tfsec's variable resolution is its key differentiator.
Use Checkov when: You scan multiple IaC formats in the same pipeline, or you need the breadth of 1,000+ checks, or you need Python-level custom check logic.
Use both when: Running in a pipeline that needs comprehensive coverage — tfsec for Terraform-specific depth, Checkov for breadth and cross-format consistency.
Trivy Integration
tfsec is now part of the Trivy ecosystem. Trivy's config scanner includes tfsec's checks:
# Scan Terraform with Trivy (includes tfsec checks)
trivy config ./terraform
<span class="hljs-comment"># Trivy also scans Dockerfile, Kubernetes, Helm in the same run
trivy config .If you're already using Trivy for container scanning, using trivy config instead of separate tfsec and Checkov runs reduces tooling complexity.
Practical Checklist for Adopting tfsec
- Start with advisory mode. Run
tfsec . --soft-failin CI to see findings without blocking. Triage what's real vs. acceptable. - Add
#tfsec:ignorefor deliberate exceptions. Every ignore should include a comment explaining why. - Enable blocking at HIGH+. Once ignores are in place, set
--minimum-severity HIGHand make it a blocking gate. - Add SARIF upload. GitHub Code Scanning surfaces findings as PR annotations without reading CI logs.
- Write custom checks for conventions. Start with tagging requirements — they're simple and high-value.
- Scan modules independently. Don't just scan the root — scan each reusable module in its own job.
Summary
tfsec adds a static security gate to your Terraform workflow:
- Fast scan (seconds for most projects), no cloud credentials needed
- Deep variable resolution reduces false positives
- Inline
#tfsec:ignorecomments for deliberate exceptions - YAML custom checks for organization-specific rules
- SARIF output integrates with GitHub Code Scanning for PR annotations
- Part of the Trivy ecosystem — can consolidate with container scanning
Run tfsec on every PR against Terraform files. Combined with Checkov's broader coverage or Pulumi CrossGuard's enforcement, it forms the policy layer of your IaC testing pyramid.