Checkov and Terrascan: Policy-as-Code Security Testing for Terraform

Checkov and Terrascan: Policy-as-Code Security Testing for Terraform

A misconfigured S3 bucket. An RDS instance without encryption. A security group open to 0.0.0.0/0. These are the infrastructure mistakes that make headlines. Policy-as-code tools like Checkov and Terrascan catch them at write time — before terraform apply ever runs.

This guide explains how both tools work, how to integrate them into your workflow, and when to use one over the other.

The Problem: Terraform Is Powerful but Permissive

Terraform lets you write cidr_blocks = ["0.0.0.0/0"] without complaint. It creates what you tell it to create. There's no built-in security review, no guardrails against common misconfigurations. You could deploy publicly accessible databases, unencrypted storage, and unauthenticated APIs — all valid Terraform, all catastrophic security failures.

Policy-as-code tools solve this by treating security rules as code that runs against your Terraform configurations before deployment.

Checkov

Checkov is an open-source static analysis tool from Bridgecrew (now part of Palo Alto Networks). It ships with 1000+ built-in checks covering AWS, GCP, Azure, and Kubernetes.

Installation

pip install checkov
# or
brew install checkov

Basic Usage

Point Checkov at your Terraform directory:

checkov -d ./terraform/

Sample output:

Check: CKV_AWS_20: "Ensure the S3 bucket has access control list (ACL) is private"
  PASSED for resource: aws_s3_bucket.logs
  File: /terraform/s3.tf:1-20

Check: CKV_AWS_18: "Ensure the S3 bucket has access logging enabled"
  FAILED for resource: aws_s3_bucket.app_data
  File: /terraform/s3.tf:22-35
  Guide: https://docs.bridgecrew.io/docs/s3_13-enable-logging

Passed checks: 47, Failed checks: 3, Skipped checks: 0

Checkov exits with code 1 if any check fails — perfect for CI gating.

Scanning Specific File Types

# Terraform files only
checkov -d . --framework terraform

# Scan a single file
checkov -f main.tf

# Output as JSON for programmatic use
checkov -d . -o json > checkov-results.json

# Output as SARIF for GitHub Security tab
checkov -d . -o sarif > results.sarif

Understanding Checks

Each check has a unique ID following the pattern CKV_<PROVIDER>_<NUMBER>. Common critical checks:

Check ID What It Catches
CKV_AWS_20 S3 bucket with public ACL
CKV_AWS_57 S3 bucket allows public policy
CKV_AWS_16 RDS without encryption
CKV_AWS_17 RDS publicly accessible
CKV_AWS_25 Security group open SSH (22) to 0.0.0.0/0
CKV_AWS_260 Security group open HTTP (80) to 0.0.0.0/0
CKV_AWS_79 EC2 IMDSv1 enabled (prefer IMDSv2)
CKV_AWS_111 IAM policy with wildcard * permissions

Writing Custom Checks

When built-in checks don't cover your policies, write your own:

# checks/custom/s3_requires_kms.py
from checkov.common.models.enums import CheckCategories, CheckResult
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck

class S3RequiresKMSEncryption(BaseResourceCheck):
    def __init__(self):
        name = "Ensure S3 bucket uses KMS encryption, not AES"
        id = "CKV_CUSTOM_1"
        supported_resources = ['aws_s3_bucket_server_side_encryption_configuration']
        categories = [CheckCategories.ENCRYPTION]
        super().__init__(name=name, id=id, categories=categories,
                         supported_resources=supported_resources)

    def scan_resource_conf(self, conf):
        rules = conf.get("rule", [{}])
        for rule in rules:
            apply_config = rule.get("apply_server_side_encryption_by_default", [{}])
            for config in apply_config:
                sse_algo = config.get("sse_algorithm", [""])
                if isinstance(sse_algo, list):
                    sse_algo = sse_algo[0]
                if sse_algo == "aws:kms":
                    return CheckResult.PASSED
        return CheckResult.FAILED

scanner = S3RequiresKMSEncryption()

Run with custom checks:

checkov -d . --external-checks-dir ./checks/custom/

Suppressing False Positives

Some checks don't apply to your context. Suppress them inline:

resource "aws_s3_bucket" "public_website" {
  bucket = "my-public-website"

  # checkov:skip=CKV_AWS_20:This bucket intentionally hosts a public static website
  # checkov:skip=CKV_AWS_57:Public policy required for static website hosting
}

Or suppress globally in a .checkov.yaml config:

skip-check:
  - CKV_AWS_144  # Cross-region replication not required for dev environments
compact: true
quiet: false

Terrascan

Terrascan is an open-source tool from Tenable that focuses on cloud security and compliance. It supports Terraform, Kubernetes, Helm, Docker, and more.

Installation

# macOS
brew install terrascan

# Binary
curl -L "https://github.com/tenable/terrascan/releases/latest/download/terrascan_Linux_x86_64.tar.gz" | tar -xz

Basic Usage

# Scan current directory
terrascan scan -t aws

# Scan specific Terraform files
terrascan scan -t aws -i terraform -d ./terraform/

# Output as JSON
terrascan scan -t aws -o json

Sample output:

Violation Details -
  Description    :  Ensure S3 bucket is not publicly exposed
  File           :  s3.tf
  Module Name    :  root
  Plan Root      :  ./
  Line           :  15
  Severity       :  HIGH
  Rule Name      :  s3BucketAuthenticatedUsers
  Rule ID        :  AWS.S3.Data Protection.High.0404

Terrascan Policies

Terrascan uses Rego (OPA policy language) for its rules:

# policies/deny_public_s3.rego
package accurics

#rule ID: AWS.S3.Data Protection.High.0404
s3BucketAuthenticatedUsers[retVal] {
    bucket := input.aws_s3_bucket[_]
    acl := bucket.config.acl
    acl == "public-read"
    retVal := {
        "Id": bucket.id,
        "Config": bucket.config
    }
}

Write custom policies in Rego and scan with them:

terrascan scan -t aws --policy-path ./custom-policies/

Compliance Frameworks

Terrascan maps violations to compliance frameworks:

# See which CIS benchmarks are violated
terrascan scan -t aws -o json | jq '.results.violations[].rule_reference_id'

Supported frameworks: CIS, GDPR, HIPAA, NIST, PCI-DSS, SOC 2.

This makes Terrascan particularly useful if you need compliance reporting — you can tell auditors exactly which CIS AWS Foundations benchmarks pass.

Checkov vs Terrascan: Side-by-Side

Feature Checkov Terrascan
Custom rules language Python Rego (OPA)
Built-in checks 1000+ 500+
Compliance mapping ✅ (stronger)
SARIF output
Terraform Plan support
Speed Fast Fast
Active development High Medium
Learning curve Low (Python) Higher (Rego)

Use Checkov if:

  • You want the most checks out of the box
  • Your team knows Python for custom rules
  • You want GitHub Security tab integration via SARIF
  • You need Terraform, Kubernetes, and Dockerfile scanning in one tool

Use Terrascan if:

  • You already use OPA/Rego elsewhere
  • Compliance reporting is a primary requirement
  • You need Terrascan's specific provider coverage

Many teams run both.

CI/CD Integration

GitHub Actions with Checkov

name: Security Scan
on: [pull_request]

jobs:
  checkov:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Checkov
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: terraform/
          framework: terraform
          output_format: sarif
          output_file_path: results.sarif
          soft_fail: false  # fail the PR on violations

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

GitHub Actions with Terrascan

name: Terrascan
on: [pull_request]

jobs:
  terrascan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Terrascan
        uses: tenable/terrascan-action@main
        with:
          iac_type: terraform
          iac_dir: terraform/
          policy_type: aws
          only_warn: false
          sarif_upload: true

Pre-commit Hooks

Catch issues before they even reach CI:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/bridgecrewio/checkov
    rev: '3.2.0'
    hooks:
      - id: checkov
        args: ['--framework', 'terraform']
pre-commit install
# Now checkov runs on every git commit

Scanning Terraform Plan Output

Scanning .tf files catches most issues, but some misconfigurations only appear after variable substitution. Scan the plan JSON for full coverage:

# Generate plan
terraform init
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json

# Scan the plan
checkov -f tfplan.json --file-type terraform_plan
terrascan scan -t aws -i terraform_plan -f tfplan.json

This catches dynamic values that can't be seen in static .tf analysis.

Baseline Workflow

  1. Start blocking only HIGH/CRITICAL — don't gate PRs on every medium check initially
  2. Fix violations in existing code — establish a clean baseline
  3. Enable SARIF upload to GitHub for developer visibility
  4. Suppress legitimate exceptions with inline comments explaining why
  5. Graduate to blocking medium once the team is used to the process
  6. Add custom checks for your organization's specific policies
# Only fail on HIGH and CRITICAL
checkov -d . --check-threshold HIGH

Security misconfigurations in Terraform are preventable. Checkov and Terrascan turn your security policy into executable checks that run before any infrastructure is deployed. Add one to your CI pipeline today — it takes 10 minutes to set up and could prevent the breach that takes months to recover from.

Read more

Start now free