Skip to main content
RunBook Academy

TerraformXXI · Testing, Linting, and Static AnalysisProduction Terraform

Static Security Analysis

Intermediate⏱ ~12 minbash

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

Not yet marked complete on this device.

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:

ToolMaintainerStrengthsLimitations
tfsecAqua Security (now Trivy)Tight Terraform focus, friendly outputNo longer standalone; folded into Trivy
trivyAqua SecurityMulti-tool (containers, IaC, secrets, SBOM)IaC scan is shallower than dedicated tools
checkovBridgecrew (now Palo Alto)Deep Terraform ruleset, GraphQL policies, custom policiesSlower 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,HIGH at 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

  1. Q1. Which severity level should block the merge on a brownfield codebase that is adopting a static security scanner?

  2. Q2. It is acceptable to suppress a finding with a CLI flag like `--skip`.

  3. Q3. What is the strongest reason to put the suppression on the resource itself rather than in a global ignore file?

  4. Q4. A team already runs Trivy for container scanning. Which is the right static analysis tool for the Terraform code?

  5. Q5. Which of the following are good practices for the static security scanner? (Select all that apply.)

  6. Q6. What is the right response to a CI failure from the security scanner on a known-accepted finding?

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