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.
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/0ingress 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 blocksoft-mandatory— blocks by default, privileged users can overridehard-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
| Feature | tfsec | Checkov | Sentinel |
|---|---|---|---|
| Scope | Terraform only | Multi-IaC | Terraform plan |
| Speed | Very fast | Moderate | Plan-time |
| Custom policies | Limited | Python | HCL DSL |
| CI integration | Easy | Easy | Terraform Cloud |
| Cost | Free | Free (OSS) | Enterprise |
| Block applies | No | No | Yes |
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
.tffiles - High/Critical findings block PR merge
- No
0.0.0.0/0ingress 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.hclcommitted and provider checksums verified