Skip to content

tf-nag: Scan Terraform Plans with AWS Solutions Rules

Posted on:October 13, 2026 at 10:50 PM

tf-nag

Abstract

tf-nag is an offline Python CLI for checking Terraform plan or state JSON against the AWS Solutions rules from cdk-nag. It brings those controls into the Terraform workflow, so teams can identify risky infrastructure changes before applying a plan.

The scanner does not contact AWS and does not need cloud credentials. Its rule IDs and severity levels follow the AWS Solutions pack, whose controls trace back to AWS Config managed rules and conformance packs.

Table Of Contents

Open Table Of Contents

Why tf-nag

Terraform makes it easy to provision AWS infrastructure, but configuration gaps can go unnoticed until an audit or an incident. Examples include missing access logs, unencrypted volumes, wildcard IAM permissions, or Lambda functions using runtimes that AWS has deprecated.

The best time to catch these issues is before applying the plan. tf-nag scans Terraform’s JSON representation locally, making it suitable for command-line use and pull-request checks without requiring access to an AWS account.

Rule provenance matters

tf-nag uses the AWS Solutions rule set from cdk-nag rather than defining a separate catalog. This gives each finding a traceable control ID and severity level. The project reports 131 AWS Solutions rules: 126 implemented, 5 not applicable to Terraform because there is no equivalent provider resource, and none currently unimplemented.

tf-nag and Checkov

Checkov is a broad infrastructure-as-code scanner with its own extensive policy catalog. tf-nag is intentionally narrower: it focuses on AWS Solutions controls and their AWS Config lineage as applied to Terraform plan or state JSON. The tools can complement one another when a team wants both broad checks and findings aligned with this specific control pack.

One example is Lambda runtime currency. AwsSolutions-L1 checks aws_lambda_function.runtime against the latest generally available runtime for its language family. The runtime reference is vendored from AWS’s supported-runtimes documentation; public-preview runtimes are distinguished from GA releases, and container-image or custom provided runtimes are treated as not applicable.

How it works

Terraform can serialize a plan to JSON, which tf-nag evaluates against its registered controls. The scanner is offline: it reads the JSON and local suppression configuration, and does not call AWS APIs.

The basic flow is:

  1. Create a Terraform plan file.
  2. Convert the plan to JSON with terraform show -json.
  3. Scan the resulting JSON with tf-nag.
  4. Review findings and address or suppress them with a reason.

The tool accepts both plan JSON and state JSON. HCL parsing is intentionally deferred, so the input is Terraform’s JSON representation rather than .tf source files.

Install and scan a Terraform plan

Install the published CLI from PyPI:

pip install tf-nag
tf-nag --version

Alternatively, run it on demand with uvx:

uvx tf-nag --version

For repeatable CI builds, pin the package version:

pip install tf-nag==0.1.0

Create a plan and scan its JSON:

terraform plan -out=tf.plan -refresh=false -lock=false
terraform show -json tf.plan > plan.json
tf-nag scan --plan-json plan.json

You can also pipe the JSON directly to the scanner:

terraform show -json tf.plan | tf-nag scan --plan-json -

Run the commands from the directory or workspace you want to inspect. For a specific workspace, select it before creating the plan:

terraform workspace select production
terraform plan -out=tf.plan -refresh=false -lock=false
terraform show -json tf.plan > plan.json
tf-nag scan --plan-json plan.json

A scan prints a table of findings with the severity, rule ID, Terraform resource address, and message. The same controls can be explored with:

tf-nag list-rules
tf-nag scan --plan-json plan.json --format sarif

SARIF output can be used by compatible code-scanning tools. Exit status can also be used in automation:

  • 0 for a clean scan, or when --fail-on never is selected.
  • 1 when an ERROR finding is present.
  • 2 for WARN-only findings when --fail-on warn is selected.
  • 3 for malformed suppression configuration.

With the default --fail-on error, WARN findings are shown but do not fail the command.

Manage suppressions

The canonical suppression file is .tfnag.json in the current directory. You can choose another path with --suppressions PATH. If no path is specified, .tfnag.json takes precedence over the legacy .tfnag.yml format.

For JSON, provide either an object containing a suppressions array or a bare array. Each entry needs a registered rule ID, matching resource addresses, and a non-empty reason:

{
  "suppressions": [
    {
      "id": "AwsSolutions-APIG1",
      "resources": ["demo_gw.aws_api_gateway_stage.demo-gw-stage"],
      "reason": "Access logging is handled centrally."
    }
  ]
}

Resource matching supports full Terraform addresses and trailing address suffixes on whole .-segment boundaries. A * glob can match within an address segment. An address without an instance key matches expanded count or for_each instances; include the key to match a specific instance. Use "resources": ["*"] to suppress all resources for a rule.

An optional expires value uses the YYYY-MM-DD format. Expired entries do not suppress findings and are reported as warnings. Suppressed findings remain visible in table, Markdown, and JSON reports with their reason. Reports also summarize suppressions and warn about entries that matched no findings. The --strict option ignores all suppressions.

The legacy .tfnag.yml format is intentionally restricted to a top-level list of entries:

- rule: AwsSolutions-S1
  address: aws_s3_bucket.site
  reason: Access logs are delivered by the centralized platform account.
  expires: 2027-01-01

For this format, rule (or id) accepts an exact AwsSolutions-* ID or *; address is an optional shell-style glob and defaults to *; reason is required; and expires is optional. Expired entries are not applied.

Rule coverage and development

To inspect the complete rule inventory, run:

tf-nag list-rules

To contribute to the project, clone it and install the development environment with uv:

git clone git@github.com:vumdao/terraform-nag.git
cd terraform-nag
uv sync --group dev

Run the quality checks and test suite:

uv run ruff check .
uv run ruff format --check .
uv run pytest

The test fixtures include hand-crafted Terraform plan JSON and a committed real-plan integration fixture. The test suite does not require AWS credentials, and Terraform is not required to run the tests.

Limitations

tf-nag currently scans plan or state JSON; it does not parse HCL or resolve resources created outside Terraform. It also does not inspect null_resource/local-exec or embedded CloudFormation, and it does not provide baseline support.

Terraform plan JSON does not include source-file or line locations, so SARIF findings identify Terraform resource addresses rather than HCL source locations. Dynamic policy documents that cannot be statically resolved are reported as UNKNOWN and handled according to --unknown-as.

Conclusion

tf-nag brings the AWS Solutions control set to Terraform plan review in a local, credential-free CLI. Scanning the plan before apply helps teams surface configuration risks earlier, while severity-aware exit codes, SARIF output, and documented suppressions make the results practical to integrate into development workflows.


References: