Cloud Security

Terraform Security Scanning: tfsec, Checkov, and Terraform Sentinel

Scan Terraform infrastructure as code with tfsec rules, Checkov in CI, Sentinel policy as code, and detection of common misconfigs like public S3 buckets and open security groups.

March 9, 20266 min readShipSafer Team

Terraform Security Scanning: tfsec, Checkov, and Terraform Sentinel

Writing Terraform is easy. Writing secure Terraform consistently, across a team, is harder. A single misconfigured S3 bucket or open security group can expose sensitive data to the entire internet. Static analysis tools catch these issues before they reach production. This guide covers the three main tools in the Terraform security scanning ecosystem.

Why IaC Security Scanning Matters

Traditional cloud security tools check your running infrastructure. IaC scanners check your Terraform code before anything is provisioned. This means:

  • Misconfigurations are caught in pull request review, not after a breach
  • Security policy is codified and consistent, not dependent on individual review
  • Developers get immediate, actionable feedback

The most common critical findings in Terraform codebases:

  • S3 buckets with public access enabled
  • Security groups with 0.0.0.0/0 ingress on sensitive ports
  • RDS instances not encrypted at rest
  • CloudTrail logging disabled
  • IAM policies with wildcard permissions

tfsec: Fast Static Analysis

tfsec is a purpose-built Terraform security scanner. It's fast (written in Go), opinionated, and integrates directly with GitHub Actions and other CI systems.

# Install
brew install tfsec               # macOS
curl -s https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash

# Basic scan of current directory
tfsec .

# Scan with specific severity threshold
tfsec . --minimum-severity HIGH

# Output as JSON for CI integration
tfsec . --format json > tfsec-results.json

# Ignore a specific check inline (with justification)
resource "aws_s3_bucket" "logs" {
  # tfsec:ignore:aws-s3-enable-bucket-logging
  bucket = "my-log-bucket"
}

Common tfsec Findings

Public S3 Bucket:

# BAD — triggers aws-s3-no-public-buckets
resource "aws_s3_bucket_public_access_block" "bad" {
  bucket                  = aws_s3_bucket.example.id
  block_public_acls       = false
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}

# GOOD
resource "aws_s3_bucket_public_access_block" "good" {
  bucket                  = aws_s3_bucket.example.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Open Security Group:

# BAD — triggers aws-ec2-no-public-ingress-sgr
resource "aws_security_group_rule" "bad" {
  type        = "ingress"
  from_port   = 22
  to_port     = 22
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]   # Open to the internet
}

# GOOD — restrict to specific CIDR
resource "aws_security_group_rule" "good" {
  type        = "ingress"
  from_port   = 22
  to_port     = 22
  protocol    = "tcp"
  cidr_blocks = ["10.0.0.0/8"]   # Internal network only
}

Checkov: Policy-as-Code for IaC

Checkov is a more comprehensive scanner that covers Terraform, CloudFormation, Kubernetes manifests, Dockerfiles, and more. It has 1000+ built-in checks.

# Install
pip install checkov

# Scan Terraform directory
checkov -d ./infrastructure

# Scan a specific file
checkov -f main.tf

# Output in SARIF format for GitHub Security tab
checkov -d . --output sarif --output-file-path results.sarif

# Run only specific checks
checkov -d . --check CKV_AWS_18,CKV_AWS_19

# Skip specific checks with justification
checkov -d . --skip-check CKV_AWS_144  # Skip S3 cross-region replication

Integrating Checkov in GitHub Actions CI

# .github/workflows/security.yml
name: IaC Security Scan

on:
  pull_request:
    paths:
      - '**.tf'
      - '**.tfvars'

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

      - name: Run Checkov
        id: checkov
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: infrastructure/
          framework: terraform
          output_format: sarif
          output_file_path: results.sarif
          soft_fail: false          # Fail the build on findings
          check: CRITICAL,HIGH      # Only fail on HIGH and CRITICAL

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

Custom Checkov Policy

Write custom checks for your organization's specific requirements:

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

class S3LoggingBucketCheck(BaseResourceCheck):
    def __init__(self):
        name = "Ensure S3 logging bucket has versioning enabled"
        id = "CKV_CUSTOM_S3_001"
        supported_resources = ["aws_s3_bucket"]
        categories = [CheckCategories.LOGGING]
        super().__init__(name=name, id=id, categories=categories,
                         supported_resources=supported_resources)

    def scan_resource_conf(self, conf):
        versioning = conf.get("versioning", [{}])
        if versioning and versioning[0].get("enabled") == [True]:
            return CheckResult.PASSED
        return CheckResult.FAILED

scanner = S3LoggingBucketCheck()

Terraform Sentinel: Policy-as-Code at the Enterprise Level

Sentinel is HashiCorp's policy-as-code framework built into Terraform Cloud and Terraform Enterprise. Unlike tfsec and Checkov (which scan files), Sentinel integrates directly into the Terraform plan/apply workflow and can enforce rules based on the plan output.

Sentinel Policy Example: No Public S3 Buckets

# policies/no-public-s3.sentinel

import "tfplan/v2" as tfplan

# Get all S3 bucket public access block resources in the plan
s3_public_access_blocks = filter tfplan.resource_changes as _, change {
    change.type is "aws_s3_bucket_public_access_block" and
    change.change.actions contains "create" or
    change.change.actions contains "update"
}

# Rule: all S3 buckets must have public access blocked
main = rule {
    all s3_public_access_blocks as _, block {
        block.change.after.block_public_acls    is true and
        block.change.after.block_public_policy  is true and
        block.change.after.ignore_public_acls   is true and
        block.change.after.restrict_public_buckets is true
    }
}
# sentinel.hcl — policy set configuration
policy "no-public-s3" {
  source = "./policies/no-public-s3.sentinel"
  enforcement_level = "hard-mandatory"  # Blocks apply, cannot be overridden
}

policy "require-encryption-at-rest" {
  source = "./policies/require-encryption.sentinel"
  enforcement_level = "soft-mandatory"  # Can be overridden with justification
}

Enforcement levels:

  • advisory — logs violations, does not block
  • soft-mandatory — blocks by default, privileged users can override
  • hard-mandatory — always blocks, no override possible

Sentinel Policy: Restrict Security Group Rules

# policies/no-open-security-groups.sentinel

import "tfplan/v2" as tfplan

# All security group rules being created/modified
sg_rules = filter tfplan.resource_changes as _, change {
    change.type is "aws_security_group_rule" and
    (change.change.actions contains "create" or
     change.change.actions contains "update")
}

# Block any ingress rule that allows 0.0.0.0/0 on sensitive ports
sensitive_ports = [22, 3389, 5432, 27017, 6379]

main = rule {
    all sg_rules as _, rule {
        rule.change.after.type is "egress" or
        not (
            (rule.change.after.cidr_blocks contains "0.0.0.0/0" or
             rule.change.after.ipv6_cidr_blocks contains "::/0") and
            rule.change.after.from_port in sensitive_ports
        )
    }
}

Pre-commit Hooks for Developer Workflow

Catch issues before they reach CI:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.88.0
    hooks:
      - id: terraform_tfsec
        args:
          - --args=--minimum-severity HIGH
      - id: terraform_checkov
        args:
          - --args=--framework terraform --check HIGH,CRITICAL
      - id: terraform_fmt
      - id: terraform_validate
# Install and run
pip install pre-commit
pre-commit install
pre-commit run --all-files

Comparing the Tools

FeaturetfsecCheckovSentinel
ScopeTerraform onlyMulti-IaCTerraform plan
SpeedVery fastModeratePlan-time
Custom policiesLimitedPythonHCL DSL
CI integrationEasyEasyTerraform Cloud
CostFreeFree (OSS)Enterprise
Block appliesNoNoYes

Recommendation:

  • Use tfsec for developer-side feedback (pre-commit, IDE plugins)
  • Use Checkov in CI to fail pull requests with a rich check set
  • Use Sentinel if you're on Terraform Cloud/Enterprise and need hard policy enforcement

Security Checklist

  • tfsec or Checkov runs on every PR touching .tf files
  • High/Critical findings block PR merge
  • No 0.0.0.0/0 ingress on ports 22, 3389, 5432, 6379, 27017
  • All S3 buckets have public access block enabled
  • RDS, EBS, and S3 encryption at rest enabled
  • CloudTrail enabled and logs sent to S3 with MFA delete
  • IAM policies reviewed for wildcard * actions
  • Pre-commit hooks installed for developers
  • Sentinel hard-mandatory policies for critical controls (if using TFC/TFE)
  • .terraform.lock.hcl committed and provider checksums verified

Check Your Security Score — Free

See exactly how your domain scores on DMARC, TLS, HTTP headers, and 25+ other automated security checks in under 60 seconds.