Introduction
A growing e-commerce company adopted Terraform to manage their AWS infrastructure, enabling consistent and repeatable deployments. Their infrastructure team created modules for VPCs, EC2 instances, RDS databases, and S3 buckets. A security audit revealed that their RDS module defaulted to publicly_accessible = true, the S3 module allowed public bucket policies, and worst of all, the Terraform state file stored in S3 contained plaintext database passwords—accessible to anyone with bucket read permissions.
Infrastructure as Code transforms how organizations manage cloud resources, but it also changes the security model. Misconfigurations aren't one-time mistakes; they're codified into modules that get deployed repeatedly. Secrets in state files persist indefinitely. This guide covers Terraform security best practices.
Understanding Terraform Security Risks
State File Exposure: Terraform state files contain the complete picture of managed infrastructure, including sensitive values like database passwords, API keys, and certificate private keys. If the backend isn't properly secured, state files can be accessed by unauthorized parties.
Insecure Default Configurations: Terraform resources often have insecure defaults. When teams create reusable modules without explicitly setting secure defaults, those insecure configurations propagate across the organization.
Secrets in Code: Hardcoding secrets in Terraform configurations is common but dangerous. These secrets end up in version control history, CI/CD logs, and state files.
Drift: Manual changes to infrastructure create drift between actual state and Terraform configurations, potentially introducing untracked security misconfigurations.
Securing Terraform State
State security is foundational. Use encrypted remote backends:
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "production/infrastructure.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
kms_key_id = "alias/terraform-state-key"
}
}Key configuration elements:
encrypt = trueenables server-side encryptionkms_key_iduses a customer-managed KMS keydynamodb_tableprevents concurrent modifications (state locking)
Apply least-privilege access to state storage. Enable versioning to maintain state history. Restrict backend access to specific IAM roles.
Secrets Management
Never hardcode secrets. Use external secret management:
# Using AWS Secrets Manager
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "production/database/master-password"
}
resource "aws_db_instance" "main" {
identifier = "production-db"
engine = "postgres"
username = "admin"
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}For CI/CD pipelines, inject secrets as environment variables:
variable "database_password" {
type = string
sensitive = true
description = "Database master password"
}Pass via environment variable: TF_VAR_database_password=<secret> terraform apply
Mark variables as sensitive to prevent console output (state will still contain the secret).
Security Scanning
Integrate security scanning into your pipeline:
# Checkov - open-source static analysis
pip install checkov
checkov -d ./terraform --framework terraform
# tfsec - Terraform-specific scanner
brew install tfsec
tfsec ./terraformExample findings these tools catch:
- S3 buckets without encryption
- Security groups allowing 0.0.0.0/0
- RDS instances without encryption at rest
- IAM policies with excessive permissions
Writing Secure Modules
Create modules with secure defaults:
resource "aws_s3_bucket" "main" {
bucket = var.bucket_name
}
resource "aws_s3_bucket_public_access_block" "main" {
bucket = aws_s3_bucket.main.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" "main" {
bucket = aws_s3_bucket.main.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = var.kms_key_arn != null ? "aws:kms" : "AES256"
kms_master_key_id = var.kms_key_arn
}
}
}
resource "aws_s3_bucket_versioning" "main" {
bucket = aws_s3_bucket.main.id
versioning_configuration {
status = "Enabled"
}
}CI/CD Integration
# GitHub Actions example
name: Terraform Security Check
on:
pull_request:
paths: ['terraform/**']
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Terraform Validate
run: terraform validate
working-directory: ./terraform
- name: Run Checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: ./terraformBest Practices
- Pin provider versions: Prevent unexpected changes
- Pin module versions: Use specific versions from registries
- Plan review process: Never apply without reviewing the plan—generate with
terraform plan -out=tfplan, review withterraform show tfplan - Separate environments: Use workspaces or separate state files
- Pre-commit hooks: Catch issues before they're committed
Conclusion
Terraform security requires attention to state protection, secrets management, secure module design, and automated scanning. Unlike manual infrastructure changes, IaC misconfigurations can propagate across all environments and persist in version control history.
Integrating security scanning into CI/CD catches issues before deployment, but it's equally important to test deployed infrastructure. On-demand security testing validates that Terraform configurations result in secure deployments and that no drift has introduced vulnerabilities. RedVeil's AI-powered platform can assess your cloud infrastructure alongside applications, verifying that your IaC practices result in secure environments.
Start testing your infrastructure with RedVeil today.