Terraform Security Checklist
Comprehensive security validation checklist for Terraform configurations. Use this reference when performing security reviews or auditing infrastructure-as-code.
Overview
Terraform Security Checklist
Comprehensive security validation checklist for Terraform configurations. Use this reference when performing security reviews or auditing infrastructure-as-code.
Secrets Management
Hardcoded Credentials
Risk: Secrets committed to version control can be exposed.
Detection:
# Search for common secret patterns
grep -rE "(password|secret|api_key|access_key)\\s*=\\s*\\"[^$]" *.tf
grep -rE "private_key\\s*=\\s*\\"" *.tf
grep -rE "token\\s*=\\s*\\"[^$]" *.tf
Remediation:
- Use Terraform variables with
sensitive = true - Use environment variables (TF_VAR_*)
- Use HashiCorp Vault or AWS Secrets Manager
- Use AWS Systems Manager Parameter Store
- Never commit
.tfvarsfiles with secrets
Example - Insecure:
resource "aws_db_instance" "example" {
username = "admin"
password = "hardcoded_password123" # SECURITY ISSUE
}
Example - Secure:
variable "db_password" {
type = string
sensitive = true
}
resource "aws_db_instance" "example" {
username = "admin"
password = var.db_password
}
Sensitive Output Exposure
Risk: Sensitive data exposed in terraform state or plan output.
Detection:
- Review output blocks for sensitive data
- Check state files for plaintext secrets
Remediation:
output "db_password" {
value = aws_db_instance.example.password
sensitive = true # Prevents display in console
}
Network Security
Overly Permissive Security Groups
Risk: Unrestricted access to resources from the internet.
Detection Patterns:
# SECURITY ISSUE: SSH open to world
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# SECURITY ISSUE: All ports open
ingress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
Best Practices:
- Restrict SSH/RDP to specific IP ranges or VPN
- Use security group references instead of CIDR blocks
- Implement least-privilege access
- Document exceptions with comments
Example - Secure:
variable "admin_cidr" {
description = "CIDR block for admin access"
type = string
}
resource "aws_security_group" "app" {
ingress {
description = "SSH from admin network only"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.admin_cidr]
}
}
Public S3 Buckets
Risk: Data exposure through public S3 access.
Detection:
# SECURITY ISSUE: Public bucket
resource "aws_s3_bucket_public_access_block" "example" {
bucket = aws_s3_bucket.example.id
block_public_acls = false # Should be true
block_public_policy = false # Should be true
ignore_public_acls = false # Should be true
restrict_public_buckets = false # Should be true
}
Best Practices:
resource "aws_s3_bucket_public_access_block" "example" {
bucket = aws_s3_bucket.example.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
Encryption
Encryption at Rest
Resources to Check:
- RDS databases
- S3 buckets
- EBS volumes
- DynamoDB tables
- Elasticsearch domains
- Kinesis streams
- SQS queues
Example - RDS Encryption:
resource "aws_db_instance" "example" {
storage_encrypted = true # Required
kms_key_id = aws_kms_key.db.arn # Use customer-managed keys
}
Example - S3 Encryption:
resource "aws_s3_bucket_server_side_encryption_configuration" "example" {
bucket = aws_s3_bucket.example.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.s3.arn
}
}
}
Encryption in Transit
Risk: Data intercepted during transmission.
Best Practices:
- Enforce HTTPS/TLS for all endpoints
- Use SSL/TLS for database connections
- Enable encryption for load balancers
Example - ALB HTTPS:
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.example.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
certificate_arn = aws_acm_certificate.cert.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.example.arn
}
}
# Redirect HTTP to HTTPS
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.example.arn
port = "80"
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
IAM Security
Overly Permissive Policies
Risk: Privilege escalation and unauthorized access.
Detection Patterns:
# SECURITY ISSUE: Admin access
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
# SECURITY ISSUE: Too broad
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}
Best Practices:
- Follow least-privilege principle
- Use specific actions instead of wildcards
- Scope resources narrowly
- Use conditions to restrict access
Example - Least Privilege:
data "aws_iam_policy_document" "s3_read_only" {
statement {
effect = "Allow"
actions = [
"s3:GetObject",
"s3:ListBucket"
]
resources = [
aws_s3_bucket.app_data.arn,
"${aws_s3_bucket.app_data.arn}/*"
]
}
}
Missing MFA Requirements
Best Practice:
data "aws_iam_policy_document" "require_mfa" {
statement {
effect = "Deny"
actions = ["*"]
resources = ["*"]
condition {
test = "BoolIfExists"
variable = "aws:MultiFactorAuthPresent"
values = ["false"]
}
}
}
Cross-Account Access
Risk: Unauthorized access from other AWS accounts.
Best Practices:
- Explicitly specify trusted accounts
- Require external ID for third-party access
- Use conditions to restrict access
data "aws_iam_policy_document" "assume_role" {
statement {
effect = "Allow"
principals {
type = "AWS"
identifiers = ["arn:aws:iam::123456789012:root"]
}
actions = ["sts:AssumeRole"]
condition {
test = "StringEquals"
variable = "sts:ExternalId"
values = [var.external_id]
}
}
}
Logging and Monitoring
Missing CloudTrail
Risk: No audit trail for API calls.
Best Practice:
resource "aws_cloudtrail" "main" {
name = "main-trail"
s3_bucket_name = aws_s3_bucket.cloudtrail.id
include_global_service_events = true
is_multi_region_trail = true
enable_logging = true
event_selector {
read_write_type = "All"
include_management_events = true
}
}
Missing VPC Flow Logs
Best Practice:
resource "aws_flow_log" "vpc" {
vpc_id = aws_vpc.main.id
traffic_type = "ALL"
iam_role_arn = aws_iam_role.flow_logs.arn
log_destination = aws_cloudwatch_log_group.flow_logs.arn
}
Unencrypted Logs
Best Practice:
resource "aws_cloudwatch_log_group" "app" {
name = "/aws/app/logs"
retention_in_days = 90
kms_key_id = aws_kms_key.logs.arn # Encrypt logs
}
Resource-Specific Checks
RDS Databases
-
storage_encrypted = true -
publicly_accessible = false - Backup retention enabled
- Multi-AZ for production
- IAM authentication enabled
- Enhanced monitoring enabled
- SSL/TLS required for connections
ElastiCache
-
at_rest_encryption_enabled = true -
transit_encryption_enabled = true - Auth token enabled for Redis
- Subnet group in private subnets
Lambda Functions
- Environment variables encrypted with KMS
- VPC configuration if accessing private resources
- IAM role with least-privilege
- Dead letter queue configured
- Reserved concurrency to prevent cost overruns
ECS/EKS
- Secrets managed via Secrets Manager
- Container images scanned
- Network policy enforcement
- Pod security policies
- RBAC configured
State File Security
Remote State
Risk: State files contain sensitive data in plaintext.
Best Practices:
Terraform 1.11+ (S3 Native Locking - Recommended):
terraform {
backend "s3" {
bucket = "terraform-state-bucket"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true # Required
kms_key_id = "arn:aws:kms:..."
use_lockfile = true # S3 native locking (1.11+)
}
}
Note: Terraform 1.11 introduced S3 native state locking via the
use_lockfileargument. This uses S3's conditional writes to implement locking without requiring DynamoDB. The DynamoDB-based locking (dynamodb_table) is now deprecated but still supported for backward compatibility.
Legacy (Terraform < 1.11 or backward compatibility):
terraform {
backend "s3" {
bucket = "terraform-state-bucket"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true # Required
kms_key_id = "arn:aws:kms:..."
dynamodb_table = "terraform-locks" # State locking (deprecated in 1.11+)
}
}
Checklist:
- Encryption enabled for state storage
- State locking configured (
use_lockfile = truefor 1.11+ or DynamoDB for older versions) - Versioning enabled on state bucket
- Access restricted via IAM policies
- MFA delete enabled on state bucket
- State files never committed to version control
Compliance Checks
Tagging
Best Practice:
locals {
common_tags = {
Environment = var.environment
ManagedBy = "Terraform"
Owner = var.owner
CostCenter = var.cost_center
Compliance = "HIPAA" # If applicable
}
}
resource "aws_instance" "example" {
# ... other config ...
tags = merge(local.common_tags, {
Name = "app-server"
})
}
Data Residency
- Ensure resources in correct regions
- Check for cross-region replication
- Verify data sovereignty requirements
Terraform-Specific Security
Provider Version Pinning
Risk: Unexpected behavior from provider updates.
Best Practice:
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # Pin major version
}
}
}
Module Sources
Risk: Malicious code from untrusted modules.
Best Practices:
- Use verified modules from Terraform Registry
- Pin module versions
- Review module code before use
- Use private module registry for internal modules
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.2" # Pin specific version
}
Automated Security Scanning
Tools to integrate:
- trivy - Unified security scanner (successor to tfsec, includes IaC scanning)
- checkov - Policy-as-code security scanner (3000+ built-in policies)
- terraform-compliance - BDD-style testing
Note: Terrascan was archived by Tenable on November 20, 2025 and is no longer maintained. Use Checkov or Trivy instead for OPA/Rego-style policy enforcement.
Trivy (Recommended)
Trivy is Aqua Security's unified scanner that absorbed tfsec. It scans Terraform, CloudFormation, Kubernetes, Helm, and more.
Version Note:
Warning: Trivy v0.60.0 has known regression issues that can cause panics when scanning Terraform configurations. If you experience crashes or unexpected behavior, downgrade to v0.59.x until v0.61.0+ is released with fixes.
To install a specific version:
# macOS brew install trivy@0.59.1 # Linux - specify version in install script curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.59.1
Installation:
# macOS
brew install trivy
# Linux
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Docker
docker pull aquasec/trivy
Usage:
# Scan Terraform directory
trivy config ./terraform
# Scan with specific severity
trivy config --severity HIGH,CRITICAL ./terraform
# Scan with JSON output
trivy config -f json -o results.json ./terraform
# Scan specific file
trivy config main.tf
# Skip specific checks
trivy config --skip-dirs .terraform ./terraform
# Scan Terraform plan JSON (more accurate)
terraform show -json tfplan > tfplan.json
trivy config tfplan.json
# Use tfvars files for accurate variable resolution
trivy config --tf-vars prod.terraform.tfvars ./terraform
# Exclude downloaded modules from scanning
trivy config --tf-exclude-downloaded-modules ./terraform
Common Trivy Checks for Terraform:
AVD-AWS-0086- S3 bucket encryptionAVD-AWS-0089- S3 bucket versioningAVD-AWS-0132- Security group unrestricted ingressAVD-AWS-0107- RDS encryption at restAVD-AWS-0078- EBS encryption
Output Formats:
table- Human-readable table (default)json- JSON format for CI/CD integrationsarif- SARIF format for IDE integrationtemplate- Custom template output
Ignore Findings:
# trivy:ignore:AVD-AWS-0086
resource "aws_s3_bucket" "example" {
bucket = "my-bucket"
}
Advanced Trivy Configuration (trivy.yaml):
# trivy.yaml
exit-code: 1
severity:
- HIGH
- CRITICAL
scan:
scanners:
- vuln
- secret
- misconfig
misconfiguration:
terraform:
tfvars-files:
- prod.tfvars
Checkov 3.0
Checkov 3.0 introduces major improvements for Terraform scanning with enhanced graph policies and deeper analysis.
Key 3.0 Features:
-
Deep Analysis Mode: Fully resolve for_each, dynamic blocks, and complex configurations:
# Enable deep analysis with plan file checkov -f tfplan.json --deep-analysis --repo-root-for-plan-enrichment . -
Baseline Feature: Track only new misconfigurations (ignore existing):
# Create baseline from current state checkov -d . --create-baseline # Run subsequent scans against baseline checkov -d . --baseline .checkov.baseline -
Enhanced Policy Language: 36 new operators including:
SUBSET- Check if values are subset of allowed valuesjsonpath_*operators - Deep JSON path queries- Enhanced graph traversal for complex dependencies
-
Improved Dynamic Block Support:
# Scan with full dynamic block resolution checkov -d . --download-external-modules true
Checkov 3.0 Commands:
# Basic scan
checkov -d .
# Deep analysis with Terraform plan
terraform plan -out=tf.plan
terraform show -json tf.plan > tfplan.json
checkov -f tfplan.json --deep-analysis
# Create and use baseline
checkov -d . --create-baseline
checkov -