TerraformXXI · Testing, Linting, and Static AnalysisProduction Terraform
Static Security Analysis
What you'll learn
- Run tfsec, checkov, or trivy against a Terraform module and interpret the output
- Configure a per-team severity gate that blocks the merge on critical and high findings
- Suppress known-accepted findings with an in-repo exception mechanism, not a `--skip` flag
- Distinguish the categories of findings each tool catches and the gaps they leave
Prerequisites
None — start here.
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13
terraform validate proves the configuration is well-formed.
tflint proves the configuration is consistent with the
provider schema. Neither proves the configuration is safe.
A static security scanner is the gate that catches the
category of error that costs the most: an S3 bucket with
public access disabled at the account level but enabled at
the resource level. A security group with 0.0.0.0/0 on
port 22. An IAM role with Action: "*" and Resource: "*".
These configurations parse. They lint. They plan. They
apply. The cloud accepts them. The auditor does not.
What static security analysis is
A static analyser reads the Terraform configuration and the plan output and matches the values against a ruleset. The ruleset is curated by the tool’s maintainers and lags the cloud by weeks to months. The scan is fast (seconds per module) and produces a structured list of findings.
The three tools that matter in production in 2026:
| Tool | Maintainer | Strengths | Limitations |
|---|---|---|---|
tfsec | Aqua Security (now Trivy) | Tight Terraform focus, friendly output | No longer standalone; folded into Trivy |
trivy | Aqua Security | Multi-tool (containers, IaC, secrets, SBOM) | IaC scan is shallower than dedicated tools |
checkov | Bridgecrew (now Palo Alto) | Deep Terraform ruleset, GraphQL policies, custom policies | Slower than the alternatives |
The right operating model is to run one of them as the gate and accept the gaps. The wrong model is to run all three and treat the merge as a checklist exercise.
Installing the tools
# Severity: CONFIGURATION - installs a binary to /usr/local/bin.
# Trivy (preferred in 2026 - includes the former tfsec rules)
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Checkov
pip install checkov
A canonical CI step:
- name: Run static security analysis
run: |
trivy config --severity CRITICAL,HIGH --exit-code 1 --no-progress .
The --exit-code 1 flag makes the scan block the merge on
any CRITICAL or HIGH finding. The --no-progress flag keeps
the CI log clean.
A first scan
# Severity: READ-ONLY - does not contact the cloud.
trivy config --severity CRITICAL,HIGH modules/network
Output:
modules/network/main.tf (terraform)
Issues:
CRITICAL: Resource 'aws_security_group' allows ingress from 0.0.0.0/0
12: cidr_blocks = ["0.0.0.0/0"]
See https://avd.aquasec.com/.../AVD-AWS-0007
HIGH: Resource 'aws_s3_bucket' has public access enabled
45: acl = "public-read"
See https://avd.aquasec.com/.../AVD-AWS-0001
The output is structured: rule ID, severity, file:line, rationale. CI can parse the JSON output and post a comment to the PR.
# JSON output for CI integration
trivy config --format json --output trivy-report.json modules/network
The right severity gate
The default of the tool is “block on every finding.” That is the wrong default for any codebase with history. The right discipline is a per-team severity table:
CRITICAL - block the merge. No exceptions without a
security review.
HIGH - block the merge. Allowed with a documented
exception (see below).
MEDIUM - warn on the PR. Do not block.
LOW - log only. Never block.
UNKNOWN - log only. Never block.
The CI command matches the table:
# Block on CRITICAL and HIGH; warn on MEDIUM and below.
trivy config --severity CRITICAL,HIGH --exit-code 1 --no-progress .
The CRITICAL gate is non-negotiable. The HIGH gate is
non-negotiable in greenfield. In brownfield, the gate is
introduced with --severity CRITICAL only and widened
later as the team catches up.
Suppressing findings
The team must be able to suppress findings it has accepted
the risk for. The right mechanism is in-repo, inline, and
auditable. The wrong mechanism is --skip on the CLI.
For tfsec and Trivy:
# tfsec:ignore:AVD-AWS-0001
resource "aws_s3_bucket" "public_assets" {
bucket = "public-assets-${var.environment}"
acl = "public-read"
}
For checkov:
resource "aws_s3_bucket" "public_assets" {
bucket = "public-assets-${var.environment}"
acl = "public-read"
# checkov:skip=CKV_AWS_20:Public read is required for the assets CDN
}
The suppression is on the resource that the rule flagged. The reason is mandatory. The rule is named. The next contributor who looks at the resource sees the reason. The next static analysis upgrade sees the suppression and either honours it or flags it as needing review.
A team-wide exception file (.trivyignore or
.checkov.yaml) is the right place for findings that
apply across the codebase:
# .trivyignore
AVD-AWS-0007 # Ingress on 22 from bastion CIDR - accepted by secops on 2026-04-01
The exception file is reviewed at the same cadence as the provider lock file. A finding that has been in the exception file for 12 months is either resolved or re-justified.
Production failure modes
1. The gate blocks on a known-accepted finding
Symptom: a PR is blocked because an aws_security_group
has 0.0.0.0/0 on port 443. Cause: the rule applies
broadly to all 0.0.0.0/0 ingress, including the public
load balancer. Fix: install the inline suppression with a
reason. Do not disable the rule globally.
2. The tool produces hundreds of findings on a new module
Symptom: a 10-resource module produces 180 findings. Cause: the ruleset is broad and the module is incremental on top of a 200-module codebase. Fix: triage by severity. Block on CRITICAL/HIGH; warn on MEDIUM; log on LOW. The volume of findings is a signal, not a verdict.
3. The tool silently ignores a critical rule
Symptom: a known vulnerability is in the plan; the scan does not flag it. Cause: the rule is not in the default ruleset. The tool’s ruleset evolves. Fix: read the release notes. Pin the tool version. Add the rule explicitly if it is missing.
4. The team disables the scanner to unblock the merge
Symptom: CI is failing on a scanner; the operator comments out the scanner step. Cause: the scanner is treated as a checkbox, not a security control. Fix: the scanner is a security control. The right response to a false positive is suppression with a reason, not deletion of the step.
5. The exception file grows without review
Symptom: .trivyignore has 200 entries, half of which
date from 2024. Cause: the exception file is added to but
never reviewed. Fix: the file is reviewed at the same
cadence as the lock file. Each entry has a date and a
reviewer. Entries older than 12 months are re-justified or
removed.
6. The tool and the policy engine disagree
Symptom: OPA or Sentinel says the configuration is allowed; the scanner says it is a CRITICAL. Cause: the two tools have different rulesets. The scanner is opportunistic; the policy engine is authoritative. Fix: the policy engine wins. The scanner is a fast lint. The policy engine is the gate.
Security and performance implications
The scanner is read-only. It does not contact the cloud. It reads the configuration and the plan output. It runs in process. CI does not need IAM permissions.
Performance: a trivy config run on a 30-file module
takes a few seconds. A run on a 1000-file monorepo takes
under a minute. The bottleneck is the rule pack, not the
file count.
Security: the exception file is a security artefact. A
contributor who can edit .trivyignore can suppress any
finding. The file must be reviewed on every change. The
inline suppression is on the resource itself, which means
the audit trail is local to the configuration.
Tool selection in 2026
tfsec is no longer a standalone project. The ruleset
moved into Trivy in 2024. Running tfsec directly still
works (the binary is still on GitHub) but the project is
in maintenance mode.
The modern choice is:
- Trivy for teams that want a single binary that also scans containers, secrets, and SBOMs. The IaC ruleset is the former tfsec.
- Checkov for teams that need deep Terraform support and custom policies. The Python-based ruleset is broader and the rule authoring is more flexible.
A team that already runs Trivy for container scanning should use Trivy for IaC. A team that wants custom policies and does not run Trivy should use Checkov. The overlap is high enough that running both is duplicative.
Production guidance
- Adopt the tool with a staged rollout. Do not start with the full gate.
- Run the scan in CI as a blocking step with
--severity CRITICAL,HIGHat minimum. - Store suppressions inline on the resource with a reason. Do not suppress via the CLI.
- Maintain a team-wide exception file for cross-cutting exceptions. Review the file at the same cadence as the provider lock file.
- Pin the tool version. The ruleset evolves; the gate should not silently widen.
- Read the rule release notes on every upgrade. The new rules are the high-signal findings.
Verification
# Run the security scan
trivy config --severity CRITICAL,HIGH --exit-code 1 --no-progress .
# JSON output for CI integration
trivy config --format json --output trivy-report.json .
# Checkov with a baseline file
checkov -d . --output cli --soft-fail
A clean scan exits 0. A failed gate exits non-zero with the file:line of the finding.
Knowledge check · 7 questions
Q1. Which severity level should block the merge on a brownfield codebase that is adopting a static security scanner?
Q2. It is acceptable to suppress a finding with a CLI flag like `--skip`.
Q3. What is the strongest reason to put the suppression on the resource itself rather than in a global ignore file?
Q4. A team already runs Trivy for container scanning. Which is the right static analysis tool for the Terraform code?
Q5. Which of the following are good practices for the static security scanner? (Select all that apply.)
Q6. What is the right response to a CI failure from the security scanner on a known-accepted finding?
Q7. A team enables the full security scanner on a 200-module repository. The first scan produces 4,200 findings. The merge is blocked. What is the next step?
Passing score: 75%. Answers are checked in this browser.