tfsec
.mdVerified 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 --versionScanning#
tfsec . # scan the current directory recursively
tfsec /path/to/terraform
tfsec --minimum-severity HIGH . # CRITICAL, HIGH, MEDIUM, or LOWCI 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 foundNote
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/modulesInline 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 failuresSample 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-insA 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#
| tfsec | Checkov | trivy config | |
|---|---|---|---|
| IaC coverage | Terraform only | Terraform, CloudFormation, K8s, Helm, Dockerfile, Ansible, ARM/Bicep, more | Terraform, CloudFormation, K8s, Helm, Dockerfile, Azure ARM |
| Maintenance status | Maintenance mode (checks frozen, folded into Trivy) | Actively developed | Actively developed |
| Default CI behavior | Fails on any finding | Fails on any finding | Reports only (needs --exit-code) |
| Custom check language | JSON/YAML + optional Rego | Python or YAML | Rego |
| Also does vuln/secret/SBOM scanning | No | Partial (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 HIGHis a common starting point) - Add inline
#tfsec:ignorecomments 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)
fiTip
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.sarifA sample of frequently-hit built-in checks#
| Check ID | Catches |
|---|---|
aws-s3-enable-bucket-encryption | An S3 bucket with no server-side encryption configured |
aws-s3-enable-versioning | An S3 bucket with versioning disabled — no protection against accidental overwrite/delete |
aws-s3-enable-logging | An S3 bucket with no access logging — no audit trail if it's ever accessed unexpectedly |
aws-ec2-no-public-ingress-sgr | A security group rule allowing ingress from 0.0.0.0/0 |
aws-iam-no-policy-wildcards | An IAM policy granting Action: "*" or Resource: "*" — overly broad by default |
azure-storage-default-action-deny | An Azure Storage account with a network ACL default action of Allow instead of Deny |
google-storage-bucket-encryption-customer-key | A 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:ignorecomments 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#
| Flag | What it does |
|---|---|
-m, --minimum-severity | Only report at/above this severity: CRITICAL, HIGH, MEDIUM, LOW |
-e, --exclude | Comma-separated rule IDs to skip for this run |
-E, --exclude-ignores | Comma-separated rule IDs to exclude from ignore-rule processing — forces them to always report even if inline-ignored |
--exclude-path | Folder path to skip; repeatable, evaluated in specification order |
--exclude-downloaded-modules | Skip findings inside anything pulled into .terraform/modules |
--force-all-dirs | Scan every directory below the target, even ones with no .tf files detected |
-s, --soft-fail | Run checks but always exit 0 |
-G, --disable-grouping | Show every individual result instead of grouping similar findings together |
--include-passed | Show passing checks too, not just failures |
--include-ignored | Show checks suppressed by an inline ignore, instead of hiding them entirely |
--no-ignores | Ignore every inline #tfsec:ignore comment for this run — useful for a periodic full-strength audit |
--no-module-downloads | Don't download remote modules — scan only what's already vendored locally |
--migrate-ignores | Rewrite old-style ignore codes to the current ID structure |
--code-theme | light or dark — cosmetic, for the annotated code snippets in terminal output |
--run-statistics | Print a summary statistics table of findings by severity/rule |
-w, --workspace | Which Terraform workspace's ignore limits apply, when a project uses multiple workspaces |
--rego-only | Run only custom Rego-based checks, skip the built-in Go checks entirely |
--print-rego-input | Dump 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 stepBecause 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.