Verified11 commandsAI-assisted

tfsec

.md

Verified against tfsec v1.28.14, flags verified via `tfsec --help` run locally, 2026-08-29 · official docs

What it is and where it fits 🎯#

tfsec is Terraform-specific static analysis for security misconfigurations — it reads .tf source directly and needs no plan/apply, so it can run in a pre-commit hook or on every PR with zero cloud credentials or state access required.

Important

tfsec is in maintenance mode. Aqua Security merged its entire check library into Trivy (trivy config) and directs engineering effort there now — tfsec still works and receives security fixes, but new checks land in Trivy first. For a brand-new setup, evaluate trivy config (see the Trivy cheat sheet's companion IaC page) before reaching for tfsec. This page documents tfsec because it's still widely deployed in existing pipelines that haven't migrated yet, and understanding it is genuinely useful for reading those pipelines' configs.

Installation#

curl -s https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash
# installs to /usr/local/bin by default — needs sudo, or download the release binary directly and place it
# on your own PATH (e.g. ~/.local/bin) if you don't have sudo:
curl -sL -o tfsec https://github.com/aquasecurity/tfsec/releases/latest/download/tfsec-linux-amd64
chmod +x tfsec && mv tfsec ~/.local/bin/

go install github.com/aquasecurity/tfsec/cmd/tfsec@latest    # if you have a Go toolchain

tfsec --version

Scanning#

tfsec .                                       # scan the current directory recursively
tfsec /path/to/terraform
tfsec --minimum-severity HIGH .               # CRITICAL, HIGH, MEDIUM, or LOW

CI gating — the inverted default worth remembering#

tfsec . --soft-fail                            # run checks but always exit 0 — reporting-only mode
tfsec .                                        # default: non-zero exit if any (non-ignored) issue is found

Note

tfsec fails the build by default on any finding — --soft-fail is the opt-out. That's the opposite default from Trivy and Grype (both default to exit 0, requiring an explicit --exit-code/--fail-on flag to gate). Mixing tfsec into a pipeline alongside those tools without noticing this asymmetry is a common source of "wait, why did only this one step fail" confusion.

Excluding checks and paths#

tfsec . --exclude aws-s3-enable-bucket-encryption,aws-s3-enable-versioning
tfsec . --exclude-path .terraform --exclude-path modules/vendored
tfsec . --exclude-downloaded-modules            # skip anything pulled into .terraform/modules

Inline suppression, directly above the offending resource block:

#tfsec:ignore:aws-s3-enable-bucket-encryption
resource "aws_s3_bucket" "example" {
  # ...
}

Output formats#

tfsec . --format json --out results.json
tfsec . --format sarif --out results.sarif      # for GitHub code scanning
tfsec . --format junit --out results.xml
tfsec . --concise-output                         # summary only, no code snippets
tfsec . --include-passed                          # show passing checks too, not just failures

Sample lovely (default) output shape — illustrative, actual check IDs depend on what's in the scanned Terraform:

result #1 CRITICAL S3 bucket does not have logging enabled. ──────────────────────────────────────────────────────────────── main.tf:3-8 ──────────────────────────────────────────────────────────────── 3 resource "aws_s3_bucket" "example" { 4 bucket = "my-bucket" 5 } ──────────────────────────────────────────────────────────────── ID aws-s3-enable-logging Impact There is no way to determine the access or usage of this bucket Resolution Add a logging block to the resource ──────────────────────────────────────────────────────────────── 24 passed, 1 potential problem(s) detected.

Custom checks#

tfsec . --custom-check-dir ./custom-checks       # load org-specific Rego/JSON checks alongside built-ins

A custom check is a small JSON/YAML document describing a Rego-backed rule:

{
  "checks": [
    {
      "code": "CUS001",
      "description": "Ensure all resources are tagged with an owner",
      "requiredTypes": ["resource"],
      "requiredLabels": ["aws_instance", "aws_s3_bucket"],
      "severity": "MEDIUM",
      "matchSpec": {
        "name": "tags",
        "action": "notContains",
        "value": "owner"
      },
      "errorMessage": "Resource is missing a required 'owner' tag"
    }
  ]
}

Tip

Org-specific tagging/naming-convention checks are one of the highest-value custom checks a platform team can add — generic public rulesets naturally can't know your org's internal tagging policy, but a missing owner or cost-center tag is exactly the kind of thing that turns into a FinOps headache (see this series' FinOps content) months after the resource ships, when nobody remembers who created it.

tfsec vs. the other IaC scanners in this series#

tfsecCheckovtrivy config
IaC coverageTerraform onlyTerraform, CloudFormation, K8s, Helm, Dockerfile, Ansible, ARM/Bicep, moreTerraform, CloudFormation, K8s, Helm, Dockerfile, Azure ARM
Maintenance statusMaintenance mode (checks frozen, folded into Trivy)Actively developedActively developed
Default CI behaviorFails on any findingFails on any findingReports only (needs --exit-code)
Custom check languageJSON/YAML + optional RegoPython or YAMLRego
Also does vuln/secret/SBOM scanningNoPartial (sca_package/sca_image/secrets frameworks)Yes, in the same binary

Adoption checklist for a new Terraform repo#

  • Run tfsec . once, unfiltered, to see the real baseline finding count before deciding on a rollout plan
  • Decide severity threshold for the initial hard gate (--minimum-severity HIGH is a common starting point)
  • Add inline #tfsec:ignore comments only for genuinely reviewed, accepted exceptions — not as a way to silence noise quickly
  • Wire the CI step in with SARIF output so findings show up as inline PR annotations, not just a pass/fail badge
  • Revisit the severity threshold quarterly as the backlog of lower-severity findings gets addressed

Real-world scenario: gating new Terraform without a wall of legacy findings#

A large existing Terraform codebase adopting tfsec for the first time often has too many pre-existing findings to fix before the first PR merges. --soft-fail on the whole repo defeats the purpose; a more targeted approach scopes the hard gate to what actually changed:

# CI step: only fail on findings in files this PR actually touched
git diff --name-only origin/main...HEAD -- '*.tf' > changed.txt
if [ -s changed.txt ]; then
  tfsec $(cat changed.txt | xargs dirname | sort -u)
fi

Tip

This diff-scoped pattern (report on everything, hard-fail only on what changed) is the same shape Semgrep's --baseline-commit and Checkov's --baseline solve more natively — tfsec doesn't have a first-class equivalent flag, so scoping by changed files is the practical workaround.

Real-world scenario: GitHub Actions CI recipe#

# .github/workflows/tfsec.yml
name: tfsec
on: [pull_request]
jobs:
  tfsec:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aquasecurity/tfsec-sarif-action@v0.1.4
        with:
          sarif_file: tfsec.sarif
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: tfsec.sarif

A sample of frequently-hit built-in checks#

Check IDCatches
aws-s3-enable-bucket-encryptionAn S3 bucket with no server-side encryption configured
aws-s3-enable-versioningAn S3 bucket with versioning disabled — no protection against accidental overwrite/delete
aws-s3-enable-loggingAn S3 bucket with no access logging — no audit trail if it's ever accessed unexpectedly
aws-ec2-no-public-ingress-sgrA security group rule allowing ingress from 0.0.0.0/0
aws-iam-no-policy-wildcardsAn IAM policy granting Action: "*" or Resource: "*" — overly broad by default
azure-storage-default-action-denyAn Azure Storage account with a network ACL default action of Allow instead of Deny
google-storage-bucket-encryption-customer-keyA GCS bucket not using a customer-managed encryption key

tfsec's --help doesn't expose a flag to list every check from the CLI itself — check the online checks catalog linked from the official docs for the full, current list. The built-in catalog grows and check IDs occasionally get renamed across releases, so treat the table above as a taste of what tfsec catches, not an exhaustive reference.

Common pitfalls#

  • Assuming the same "reports, doesn't gate" default as Trivy/Grype — see the NOTE above; tfsec is the opposite.
  • Not knowing tfsec is in maintenance mode — planning new custom-check investment against tfsec's Rego format when that investment would carry forward more safely against Trivy's actively developed check bundle.
  • Inline #tfsec:ignore comments accumulating with no review process — like any suppression mechanism, worth periodically auditing (grep -rn 'tfsec:ignore') rather than letting them silently pile up.

Exit codes#

0 clean, or --soft-fail set · non-zero when findings exist and --soft-fail is not set.

Full flag reference#

FlagWhat it does
-m, --minimum-severityOnly report at/above this severity: CRITICAL, HIGH, MEDIUM, LOW
-e, --excludeComma-separated rule IDs to skip for this run
-E, --exclude-ignoresComma-separated rule IDs to exclude from ignore-rule processing — forces them to always report even if inline-ignored
--exclude-pathFolder path to skip; repeatable, evaluated in specification order
--exclude-downloaded-modulesSkip findings inside anything pulled into .terraform/modules
--force-all-dirsScan every directory below the target, even ones with no .tf files detected
-s, --soft-failRun checks but always exit 0
-G, --disable-groupingShow every individual result instead of grouping similar findings together
--include-passedShow passing checks too, not just failures
--include-ignoredShow checks suppressed by an inline ignore, instead of hiding them entirely
--no-ignoresIgnore every inline #tfsec:ignore comment for this run — useful for a periodic full-strength audit
--no-module-downloadsDon't download remote modules — scan only what's already vendored locally
--migrate-ignoresRewrite old-style ignore codes to the current ID structure
--code-themelight or dark — cosmetic, for the annotated code snippets in terminal output
--run-statisticsPrint a summary statistics table of findings by severity/rule
-w, --workspaceWhich Terraform workspace's ignore limits apply, when a project uses multiple workspaces
--rego-onlyRun only custom Rego-based checks, skip the built-in Go checks entirely
--print-rego-inputDump the JSON representation fed into Rego policies — useful when debugging a custom Rego check

Troubleshooting#

"tfsec found nothing, but I know this file has an issue" — check whether the resource lives inside a downloaded module (.terraform/modules/); --exclude-downloaded-modules is off by default in most setups, but a project-level config might have it on. Also confirm the file has a .tf extension tfsec actually scans recursively from the given directory — tfsec . from the wrong working directory silently scans nothing.

"A check keeps firing even though I added #tfsec:ignore" — the comment must be directly above the resource block, not the argument line; a misplaced ignore comment is a silent no-op, not an error.

"Results look different between my laptop and CI" — confirm both are running the exact same tfsec version (tfsec --version); check bundle content and defaults have changed release to release even during the maintenance-mode period, so a floating "latest" in CI vs. a pinned local install is a common source of drift.

Real-world scenario: migrating an existing tfsec CI gate to Trivy#

Given tfsec's maintenance-mode status, a team with tfsec already wired into CI has a natural migration path that doesn't require a big-bang cutover:

# Run both in parallel for a sprint, comparing findings
tfsec . --format json --out tfsec-results.json
trivy config . --format json --output trivy-results.json
# Once trivy's findings are confirmed to be a superset (or an acceptable equivalent), drop the tfsec step

Because Trivy's Terraform checks are literally sourced from tfsec's own check library, this comparison usually converges quickly — most differences come down to check-ID naming rather than genuinely different coverage, but it's worth confirming on a security-critical repo before removing the older gate entirely.

When to reach for something else#

trivy config now covers the same Terraform check library tfsec does, actively maintained, plus other IaC formats in the same run — see the Trivy IaC/SBOM cheat-sheet page. Checkov remains a genuinely distinct option for teams whose IaC footprint includes Ansible, Bicep, or Serverless Framework, which neither tfsec nor Trivy's config scanner cover as of this writing.