tfsec: Static Security Analysis for Terraform Code

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/tfsec

Verify:

tfsec --version

Basic 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.sarif

Sample 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 CRITICAL

Variable 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.tfvars

Ignoring 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-31

Ignore 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: MEDIUM

Custom 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/tagging

More 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 list

Run with custom checks:

tfsec . --custom-check-dir ./.tfsec/checks

Module 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-modules

For internal module registries, tfsec supports scanning module sources directly:

tfsec ./modules/vpc
tfsec ./modules/rds

Output 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.md

CI 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

  1. Start with advisory mode. Run tfsec . --soft-fail in CI to see findings without blocking. Triage what's real vs. acceptable.
  2. Add #tfsec:ignore for deliberate exceptions. Every ignore should include a comment explaining why.
  3. Enable blocking at HIGH+. Once ignores are in place, set --minimum-severity HIGH and make it a blocking gate.
  4. Add SARIF upload. GitHub Code Scanning surfaces findings as PR annotations without reading CI logs.
  5. Write custom checks for conventions. Start with tagging requirements — they're simple and high-value.
  6. 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:ignore comments 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.

Read more