Infrastructure as Code (IaC) revolutionized cloud provisioning—but it also introduced new security risks. A single misconfigured Terraform resource can expose databases to the internet or grant excessive permissions across your entire cloud environment.
This guide covers essential Terraform security practices to protect your infrastructure from misconfigurations, credential exposure, and compliance violations.
Why Terraform Security Matters
Terraform configurations define your production infrastructure. Security issues in code become security issues in production:
- Misconfigurations deploy automatically - A public S3 bucket in code means a public bucket in AWS
- Secrets in state files - Terraform state contains sensitive data in plain text
- Blast radius is large - One
terraform applycan affect hundreds of resources - Configuration drift - Manual changes create discrepancies between code and reality
The good news: Because infrastructure is code, you can apply software security practices—code review, testing, scanning, and version control.
1. Secure Your State Files
Terraform state files contain sensitive information including resource IDs, IP addresses, and sometimes passwords or connection strings.
Use Remote State with Encryption
Never store state locally in production. Use encrypted remote backends:
AWS S3 with encryption:
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "prod/infrastructure.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
Azure Storage with encryption:
terraform {
backend "azurerm" {
resource_group_name = "terraform-state-rg"
storage_account_name = "tfstateaccount"
container_name = "tfstate"
key = "prod.terraform.tfstate"
}
}
GCP Cloud Storage:
terraform {
backend "gcs" {
bucket = "mycompany-terraform-state"
prefix = "terraform/state"
}
}
Enable State Locking
Prevent concurrent modifications that can corrupt state:
# AWS: DynamoDB for locking
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Restrict State Access
Limit who can read state files—they contain your infrastructure blueprint:
# S3 bucket policy restricting access
resource "aws_s3_bucket_policy" "state_policy" {
bucket = aws_s3_bucket.terraform_state.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "DenyUnencryptedUploads"
Effect = "Deny"
Principal = "*"
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.terraform_state.arn}/*"
Condition = {
StringNotEquals = {
"s3:x-amz-server-side-encryption" = "AES256"
}
}
}
]
})
}
2. Never Hardcode Secrets
Hardcoded credentials in Terraform files end up in version control, state files, and logs.
Use Variables for Secrets
# variables.tf
variable "database_password" {
description = "RDS master password"
type = string
sensitive = true # Prevents logging
}
# main.tf
resource "aws_db_instance" "main" {
# ...
password = var.database_password
}
Integrate with Secrets Managers
Pull secrets at apply time:
AWS Secrets Manager:
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/database/password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
Azure Key Vault:
data "azurerm_key_vault_secret" "db_password" {
name = "database-password"
key_vault_id = azurerm_key_vault.main.id
}
resource "azurerm_mssql_server" "main" {
administrator_login_password = data.azurerm_key_vault_secret.db_password.value
}
HashiCorp Vault:
data "vault_generic_secret" "db" {
path = "secret/data/database"
}
resource "aws_db_instance" "main" {
password = data.vault_generic_secret.db.data["password"]
}
Use Environment Variables
For CI/CD pipelines:
export TF_VAR_database_password="secure-password-from-vault"
terraform apply
3. Scan Configurations Before Deployment
Catch misconfigurations before they reach production using IaC scanning tools.
Popular Scanning Tools
| Tool | Strengths |
|---|---|
| Checkov | 1000+ policies, multi-framework (Terraform, CloudFormation, K8s) |
| tfsec | Terraform-specific, fast, integrates with pre-commit |
| Terrascan | Policy as code, custom rules, CI/CD integration |
| Snyk IaC | Developer-friendly, fix suggestions, IDE plugins |
| KICS | Open source, 7000+ queries, multiple formats |
Integrate Scanning in CI/CD
GitHub Actions example:
name: Terraform Security Scan
on: [pull_request]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Checkov
uses: bridgecrewio/checkov-action@master
with:
directory: ./terraform
framework: terraform
output_format: sarif
- name: Run tfsec
uses: aquasecurity/[email protected]
with:
soft_fail: true
Common Issues Scanners Catch
- Public S3 buckets or storage accounts
- Unencrypted databases and storage
- Overly permissive security groups
- Missing logging and monitoring
- Hardcoded secrets
- Missing tags for cost allocation
4. Enforce Policies with Sentinel or OPA
For enterprise environments, enforce security policies that block non-compliant deployments.
HashiCorp Sentinel (Terraform Cloud/Enterprise)
# Require encryption on all S3 buckets
import "tfplan/v2" as tfplan
s3_buckets = filter tfplan.resource_changes as _, rc {
rc.type is "aws_s3_bucket" and
rc.mode is "managed" and
(rc.change.actions contains "create" or rc.change.actions contains "update")
}
encryption_enabled = rule {
all s3_buckets as _, bucket {
bucket.change.after.server_side_encryption_configuration is not null
}
}
main = rule {
encryption_enabled
}
Open Policy Agent (OPA)
# deny_public_buckets.rego
package terraform
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
resource.change.after.acl == "public-read"
msg := sprintf("S3 bucket %s cannot be public", [resource.address])
}
5. Use Modules for Consistency
Modules encapsulate security best practices so teams don't reinvent security for every resource.
Create Secure Baseline Modules
# modules/secure-s3-bucket/main.tf
resource "aws_s3_bucket" "this" {
bucket = var.bucket_name
}
resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
bucket = aws_s3_bucket.this.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "this" {
bucket = aws_s3_bucket.this.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_versioning" "this" {
bucket = aws_s3_bucket.this.id
versioning_configuration {
status = "Enabled"
}
}
Use Verified Modules
Leverage community modules with security built in:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
name = "production-vpc"
cidr = "10.0.0.0/16"
enable_nat_gateway = true
single_nat_gateway = false
enable_vpn_gateway = false
enable_dns_hostnames = true
enable_flow_log = true
}
6. Implement Least Privilege for Terraform
The credentials Terraform uses should have only the permissions needed.
Create Dedicated IAM Roles
# Role for Terraform with limited permissions
resource "aws_iam_role" "terraform" {
name = "terraform-deployment-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
}
# Attach only necessary policies
resource "aws_iam_role_policy_attachment" "terraform_vpc" {
role = aws_iam_role.terraform.name
policy_arn = "arn:aws:iam::aws:policy/AmazonVPCFullAccess"
}
Use Short-Lived Credentials
In CI/CD, use OIDC federation instead of long-lived access keys:
# GitHub Actions with OIDC
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/terraform-github-actions
aws-region: us-east-1
7. Version Control Best Practices
Use Branch Protection
Require reviews before merging infrastructure changes:
- Require pull request reviews
- Require status checks (security scans) to pass
- Require signed commits
- Restrict who can push to main
Tag Sensitive Files in .gitignore
# Never commit these
*.tfvars
*.tfstate
*.tfstate.backup
.terraform/
*.pem
*.key
Review Plans Before Apply
Always review terraform plan output:
# Generate plan file
terraform plan -out=tfplan
# Review the plan
terraform show tfplan
# Apply only after review
terraform apply tfplan
8. Enable Drift Detection
Configuration drift—manual changes outside Terraform—creates security blind spots.
Detect Drift Regularly
# Refresh state and detect drift
terraform plan -refresh-only
# In CI/CD, fail if drift detected
terraform plan -detailed-exitcode
# Exit code 2 means changes detected
Use Terraform Cloud Drift Detection
Terraform Cloud can automatically detect and alert on drift:
terraform {
cloud {
organization = "mycompany"
workspaces {
name = "production"
}
}
}
Security Checklist
| Practice | Priority |
|---|---|
| Remote state with encryption | Critical |
| State file access controls | Critical |
| No hardcoded secrets | Critical |
| Pre-commit security scanning | High |
| CI/CD security gates | High |
| Secure modules for common patterns | High |
| Least privilege for Terraform credentials | High |
| Policy enforcement (Sentinel/OPA) | Medium |
| Drift detection | Medium |
| Signed commits | Medium |
Frequently Asked Questions
What's the biggest Terraform security risk?
Exposed state files and hardcoded secrets are the most common issues. State files contain your entire infrastructure blueprint and often include sensitive values. Always use encrypted remote backends with strict access controls.
Should I use Terraform Cloud or self-managed backends?
Terraform Cloud provides built-in security features: encrypted state, access controls, audit logs, policy enforcement, and drift detection. Self-managed backends (S3, Azure Storage) work well but require you to implement these controls yourself.
How do I handle secrets in Terraform?
Never hardcode secrets. Use the sensitive = true flag for variables, integrate with secrets managers (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault), and use environment variables in CI/CD. Mark sensitive outputs as sensitive too.
What IaC scanning tool should I use?
Start with Checkov or tfsec—both are free, well-maintained, and catch common issues. For enterprise environments, consider commercial tools like Snyk IaC or Prisma Cloud that provide remediation guidance and policy management.
How do I prevent configuration drift?
Run terraform plan -refresh-only regularly to detect drift. Use Terraform Cloud's drift detection feature for automated monitoring. Most importantly, enforce a culture where all changes go through Terraform—not manual console changes.
Take Action
- Audit your current setup - Check for hardcoded secrets, local state files, and missing encryption
- Implement remote state - Move to encrypted backends with state locking
- Add security scanning - Integrate Checkov or tfsec into your CI/CD pipeline
- Create secure modules - Build reusable modules with security built in
- Enable drift detection - Monitor for manual changes outside Terraform
For more cloud security guidance, see our comprehensive guide: 30 Cloud Security Tips for 2026.
