Skip to content
Home Cloud & Infrastructure Security Terraform Security Scanning
Guide

Terraform Security Scanning

How to catch Terraform misconfigurations before they reach production. Covers Checkov, KICS, tfsec, and Trivy for IaC scanning with CI/CD pipeline examples.

Suphi Cankurt
Suphi Cankurt
AppSec Enthusiast
Updated February 11, 2026
8 min read
0 Comments

Why scanning Terraform matters

Terraform defines your cloud infrastructure in code. A typo or oversight in a .tf file can open a security group to the internet, grant admin access to a service account, or deploy an unencrypted database. These mistakes deploy in seconds and create real exposure.

The 2024 Datadog State of Cloud Security report found that 1 in 4 AWS organizations had at least one publicly exposed S3 bucket. Most of those started as Terraform resources missing a single block_public_access block. The fix takes 30 seconds in a .tf file. The breach investigation takes months.

Manual code review catches some of these issues, but not reliably. A senior engineer reviewing Terraform can spot obvious problems, but Terraform projects grow to hundreds of files across dozens of modules. No human consistently catches every 0.0.0.0/0 in every security group rule on every pull request.

Automated scanners check every resource against hundreds of policies on every pull request. They do not get tired, they do not miss the 50th security group, and they run in seconds. The feedback is immediate: the developer who wrote the misconfiguration sees the finding before the PR merges.

For a broader look at IaC security across all frameworks, see our What is IaC Security guide.


Tool comparison: Checkov vs KICS vs tfsec vs Trivy IaC

Four tools dominate Terraform scanning. Here is how they compare.

Checkov

Checkov has the most Terraform-specific coverage of any open-source scanner. 3,000+ built-in policies covering AWS, Azure, GCP, Kubernetes, and more. What sets it apart is graph-based analysis: Checkov maps relationships between resources and checks cross-resource security properties. It can verify that an EC2 instance’s security group, in a specific subnet, in a specific VPC, is properly configured as a chain.

Custom policies in Python or YAML. YAML policies are declarative and handle 80% of custom rule needs. Python policies give you programmatic control for complex logic. Maintained by Palo Alto Networks (Prisma Cloud). 8,500+ GitHub stars.

Best for: Teams where Terraform is the primary IaC and depth of analysis matters most.

KICS

KICS (Keeping Infrastructure as Code Secure) covers the most IaC frameworks at 22+ platforms. 2,400+ Rego-based queries. For Terraform specifically, KICS covers AWS, Azure, GCP, and Alibaba Cloud provider resources. Built by Checkmarx. 2,600+ GitHub stars.

Best for: Teams using multiple IaC frameworks (Terraform plus CloudFormation plus Ansible plus Pulumi) who want one tool for everything. See our Checkov vs KICS comparison for a detailed head-to-head.

Trivy (absorbed tfsec)

Trivy absorbed tfsec in 2024 and took over its full rule library. Run trivy config . on a Terraform directory and you get the same coverage that tfsec provided, plus Trivy’s other scanning capabilities (container images, dependencies, secrets, Kubernetes).

If you were using tfsec, migrate to Trivy. The rules are the same, inline suppressions carry over, and you gain additional scanning capabilities. 31,700+ GitHub stars.

Best for: Teams that want one scanner for Terraform, container images, dependencies, and secrets.

Quick comparison

FeatureCheckovKICSTrivy
Terraform policies3,000+2,400+~1,500 (ex-tfsec)
Graph-based checksYes (800+)NoNo
Custom rule languagePython, YAMLRegoRego
Plan file scanningYesYesYes
Other IaC frameworks10+22+8+
Image scanningNoNoYes
Dependency scanningNoNoYes
LicenseApache 2.0Apache 2.0Apache 2.0

All three are free, open-source, and actively maintained.


Setting up scanning in CI/CD

The standard setup runs the scanner on every pull request that touches .tf files. Start in warning mode, graduate to blocking.

GitHub Actions with Checkov

name: Terraform Security Scan
on:
  pull_request:
    paths:
      - '**/*.tf'
      - '**/*.tfvars'

jobs:
  checkov:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: ./infrastructure
          framework: terraform
          output_format: sarif
          soft_fail: true  # warn, don't block (remove when ready to enforce)

GitLab CI with Trivy

terraform-scan:
  image: aquasec/trivy:latest
  stage: test
  script:
    - trivy config --severity HIGH,CRITICAL --exit-code 1 ./infrastructure
  rules:
    - changes:
        - "**/*.tf"
        - "**/*.tfvars"

SARIF output for code scanning

Both Checkov and Trivy produce SARIF (Static Analysis Results Interchange Format) output. Upload SARIF to GitHub Code Scanning or GitLab Security Dashboard to see findings inline on pull request diffs:

- name: Upload SARIF
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: results.sarif

Plan scanning

For more accurate results, scan the Terraform plan output instead of raw HCL:

terraform init
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
checkov -f tfplan.json --framework terraform_plan

Plan scanning resolves variables, module references, and data sources. It catches issues that HCL scanning misses when values are computed at plan time. The tradeoff is that plan scanning requires cloud credentials and takes longer.


Writing custom policies

Built-in policies cover common misconfigurations. Custom policies encode your organization’s specific requirements.

Checkov YAML policies

YAML policies are declarative and handle most custom checks. Example: require a specific tag on all S3 buckets:

metadata:
  id: "CUSTOM_AWS_1"
  name: "Ensure S3 buckets have a 'data-classification' tag"
  severity: HIGH
definition:
  cond_type: attribute
  resource_types:
    - aws_s3_bucket
  attribute: tags.data-classification
  operator: exists

Save this in a directory and pass it to Checkov with --external-checks-dir.

Checkov Python policies

For logic that YAML cannot express:

from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckResult, CheckCategories

class S3BucketDataClassification(BaseResourceCheck):
    def __init__(self):
        name = "Ensure S3 buckets have data-classification tag"
        id = "CUSTOM_AWS_1"
        supported_resources = ["aws_s3_bucket"]
        categories = [CheckCategories.GENERAL_SECURITY]
        super().__init__(name=name, id=id,
                        categories=categories,
                        supported_resources=supported_resources)

    def scan_resource_conf(self, conf):
        tags = conf.get("tags", [{}])[0]
        if "data-classification" in tags:
            return CheckResult.PASSED
        return CheckResult.FAILED

check = S3BucketDataClassification()

Rego policies for KICS and Trivy

Rego is the policy language for OPA (Open Policy Agent). Both KICS and Trivy accept custom Rego rules. Rego is more expressive than YAML but takes time to learn:

package custom.terraform.s3

deny[msg] {
    resource := input.resource.aws_s3_bucket[name]
    not resource.tags["data-classification"]
    msg := sprintf("S3 bucket '%s' is missing data-classification tag", [name])
}

If your team already uses OPA for Kubernetes admission control or other policy enforcement, Rego-based tools keep everything in one language.


Common Terraform misconfigurations

These are what scanners flag most, and what causes most cloud breaches.

S3 bucket misconfigurations

Missing block_public_access, disabled encryption, no versioning, no access logging. Every Terraform scanner has 10+ rules just for S3. The number of S3-related breaches makes this the single most important resource type to get right.

# Bad: missing public access block
resource "aws_s3_bucket" "data" {
  bucket = "my-data-bucket"
}

# Fixed: public access blocked, encryption enabled
resource "aws_s3_bucket" "data" {
  bucket = "my-data-bucket"
}

resource "aws_s3_bucket_public_access_block" "data" {
  bucket                  = aws_s3_bucket.data.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
  bucket = aws_s3_bucket.data.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
  }
}

IAM policy wildcards

"Action": "*" or "Resource": "*" in IAM policy documents. These grant far more access than any workload needs. Scanners flag both action and resource wildcards. The fix is to enumerate specific actions and restrict resources to specific ARNs.

Security groups open to the world

Ingress rules with cidr_blocks = ["0.0.0.0/0"] on ports like 22 (SSH), 3389 (RDP), 3306 (MySQL), 5432 (PostgreSQL). Restrict these to known IP ranges or VPN CIDR blocks.

Unencrypted resources

RDS instances without storage_encrypted = true, EBS volumes without encryption, Elasticsearch domains without node-to-node encryption. Most compliance frameworks require encryption at rest. The Terraform fix is a single boolean attribute.

Missing logging

CloudTrail not enabled, VPC flow logs not configured, ALB access logging disabled. Without logs, you cannot detect or investigate incidents. Scanners verify that logging resources exist and reference the correct destinations.


Handling false positives and suppressions

Every scanner produces some false positives. Handling them well is the difference between a useful tool and alert fatigue.

Inline suppressions

Add comments to your Terraform files to suppress specific checks:

# Checkov
#checkov:skip=CKV_AWS_18:Access logging not needed for static assets bucket

# Trivy (tfsec legacy format also works)
#trivy:ignore:AVD-AWS-0086

Always include a reason. Suppressions without explanations become security debt that nobody understands.

Config file exclusions

For checks that do not apply to your environment, suppress them globally in a config file:

# .checkov.yaml
skip-check:
  - CKV_AWS_144  # S3 cross-region replication (single-region deployment)
  - CKV_AWS_145  # S3 default encryption with CMK (using SSE-S3)

Baseline approach

On existing projects with hundreds of findings, establish a baseline. Run the scanner once, export all current findings, and only flag new findings on subsequent runs. Checkov supports this with --baseline. This lets you adopt scanning immediately without fixing every historical issue first.

Review cycle

Review suppressions quarterly. Requirements change, and a suppression that was valid six months ago may not be valid now. Treat suppressed checks as accepted risk that needs periodic re-evaluation.


Policy as Code with Sentinel and OPA

Beyond individual scanner rules, you can implement governance-level policies that apply across your entire Terraform workflow.

HashiCorp Sentinel

Sentinel is HashiCorp’s policy-as-code framework, embedded in Terraform Cloud and Terraform Enterprise. Sentinel policies run after terraform plan and before terraform apply, so they have access to the full planned state.

Sentinel can enforce organization-wide rules: all resources must have cost-center tags, no EC2 instances above a certain size without approval, all databases must be in specific regions. It operates at the governance layer above individual resource checks.

The limitation is that Sentinel only works with Terraform Cloud/Enterprise. If you run Terraform OSS with your own CI, Sentinel is not available.

Open Policy Agent (OPA)

OPA evaluates Terraform plan JSON against Rego policies. It works with any Terraform workflow (OSS, Cloud, or Enterprise). Tools like Conftest wrap OPA with a developer-friendly CLI:

terraform show -json tfplan > tfplan.json
conftest test tfplan.json --policy ./policies/

OPA policies can be shared across Terraform, Kubernetes, CI/CD pipelines, and other systems. If your organization standardizes on OPA, using it for Terraform governance keeps everything in one framework.

What to enforce at the governance level

  • Required tags on all resources (cost-center, owner, environment, data-classification)
  • Allowed cloud regions (data residency compliance)
  • Maximum instance sizes without approval workflows
  • Banned resource types (e.g., no public-facing RDS instances)
  • Required encryption methods (CMK vs service-managed keys)

For more on IaC security tools, see our IaC security tools page and Cloud Infrastructure Security hub.


FAQ

This guide is part of our Cloud & Infrastructure Security resource hub.

Frequently Asked Questions

What is Terraform security scanning?
Terraform security scanning is the automated analysis of Terraform HCL files to detect security misconfigurations before deployment. Scanners check your resource definitions against policy libraries covering IAM, networking, encryption, logging, and access control. Issues are flagged in the pull request or CI pipeline, letting you fix them before they become live cloud misconfigurations.
Which Terraform security scanner should I use?
For Terraform-heavy teams, Checkov has the deepest coverage with 3,000+ policies and unique graph-based analysis. For teams that also need container image scanning and dependency checking, Trivy covers IaC plus those use cases in one tool. For the widest IaC framework coverage beyond Terraform, KICS supports 22+ platforms.
Is tfsec still maintained?
No. tfsec was absorbed into Trivy in 2024. Aqua Security migrated tfsec’s rule library into Trivy’s IaC scanning engine. If you are using tfsec, migrate to Trivy. The transition is straightforward since Trivy uses the same rules and supports tfsec’s inline suppressions.
Can I write custom Terraform security rules?
Yes. Checkov supports custom rules in Python or YAML. KICS and Trivy use Rego (the OPA policy language). Checkov’s YAML format is the easiest to write for simple attribute checks. Rego offers more power for complex logic but has a steeper learning curve.
How do I handle false positives in Terraform scanning?
Most tools support inline suppressions using comments in your Terraform files. Checkov uses ‘#checkov:skip=CKV_AWS_18:Reason here’. Trivy uses ‘#trivy:ignore:AVD-AWS-0086’. You can also configure a .checkov.yaml or trivy.yaml file to skip specific checks globally. Document why each suppression exists so future reviewers understand the decision.
Should I scan Terraform plan output or HCL files directly?
Both have value. HCL scanning is fast and runs without cloud credentials. Plan scanning (terraform plan -out=plan.json then scan the JSON) resolves variables, modules, and data sources, giving more accurate results. Plan scanning catches issues that HCL scanning misses when values come from variables or remote state. Run HCL scanning on every PR and plan scanning as a secondary gate.
Suphi Cankurt
Written by
Suphi Cankurt

Suphi Cankurt is an application security enthusiast based in Helsinki, Finland. He reviews and compares 129 AppSec tools across 10 categories on AppSec Santa. Learn more.

Comments

Powered by Giscus — comments are stored in GitHub Discussions.