🔒 Security Best Practices¶
Sensitive Data Management¶
1. Mark Variables as Sensitive¶
variable "db_password" {
description = "Database password"
type = string
sensitive = true # Redacts from output
}
variable "api_key" {
description = "API key"
type = string
sensitive = true
}
2. Mark Outputs as Sensitive¶
output "database_password" {
description = "Database password"
value = aws_db_instance.main.password
sensitive = true # Hidden in CLI output
}
3. Use sensitive() Function¶
Secret Management¶
❌ DON'T Store Secrets in Code¶
✅ DO Use Environment Variables¶
# Set via environment
export TF_VAR_db_password="SecurePassword123"
# Use in Terraform
terraform apply
✅ DO Use External Secret Managers¶
AWS Secrets Manager:
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/db/password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
HashiCorp Vault:
data "vault_generic_secret" "db_creds" {
path = "secret/database"
}
resource "aws_db_instance" "main" {
username = data.vault_generic_secret.db_creds.data["username"]
password = data.vault_generic_secret.db_creds.data["password"]
}
State File Security¶
State Contains Sensitive Data¶
{
"resources": [{
"instances": [{
"attributes": {
"password": "MySecretPassword", // ⚠️ Visible in state
"private_key": "-----BEGIN RSA..."
}
}]
}]
}
Secure State Storage¶
S3 Backend with Encryption:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true # Server-side encryption
kms_key_id = "arn:aws:kms:..." # Optional: Use KMS
dynamodb_table = "terraform-locks"
# Access control
acl = "private"
}
}
Terraform Cloud:
terraform {
cloud {
organization = "my-org"
workspaces {
name = "production"
}
}
}
# State automatically encrypted at rest
.gitignore Configuration¶
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
crash.*.log
# Exclude all .tfvars files (may contain secrets)
*.tfvars
*.tfvars.json
# Ignore override files
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Ignore CLI configuration files
.terraformrc
terraform.rc
# Ignore plan files
*.tfplan
# Ignore backup files
*.backup
IAM and Access Control¶
Principle of Least Privilege¶
# ✅ GOOD: Specific permissions
resource "aws_iam_policy" "terraform" {
name = "terraform-policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"ec2:Describe*",
"ec2:CreateTags",
"ec2:RunInstances"
]
Resource = "*"
}]
})
}
# ❌ BAD: Overly permissive
resource "aws_iam_policy" "bad_example" {
policy = jsonencode({
Statement = [{
Effect = "Allow"
Action = "*" # ❌ Too broad
Resource = "*"
}]
})
}
Use Assume Roles¶
provider "aws" {
region = "us-east-1"
assume_role {
role_arn = "arn:aws:iam::123456789012:role/TerraformRole"
}
}
Network Security¶
Security Groups¶
# ✅ GOOD: Restrictive rules
resource "aws_security_group" "web" {
name = "web-sg"
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"] # Internal only
}
}
# ❌ BAD: Open to world
resource "aws_security_group" "bad" {
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # ❌ SSH open to internet
}
}
Use Variables for IP Ranges¶
variable "allowed_ips" {
description = "Allowed IP ranges"
type = list(string)
default = ["10.0.0.0/8", "172.16.0.0/12"]
}
resource "aws_security_group" "app" {
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = var.allowed_ips
}
}
Encryption¶
Encrypt at Rest¶
# S3 Bucket Encryption
resource "aws_s3_bucket" "data" {
bucket = "my-data-bucket"
}
resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
bucket = aws_s3_bucket.data.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.main.arn
}
}
}
# RDS Encryption
resource "aws_db_instance" "main" {
storage_encrypted = true
kms_key_id = aws_kms_key.main.arn
}
# EBS Encryption
resource "aws_ebs_volume" "data" {
encrypted = true
kms_key_id = aws_kms_key.main.arn
}
Encrypt in Transit¶
# ALB with HTTPS
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
certificate_arn = aws_acm_certificate.main.arn
}
# RDS with SSL
resource "aws_db_instance" "main" {
ca_cert_identifier = "rds-ca-2019"
}
Compliance and Auditing¶
Resource Tagging¶
locals {
common_tags = {
Environment = var.environment
ManagedBy = "Terraform"
Owner = "DevOps Team"
CostCenter = "Engineering"
Compliance = "PCI-DSS"
}
}
resource "aws_instance" "web" {
tags = merge(
local.common_tags,
{
Name = "web-server"
Role = "WebServer"
}
)
}
Enable Logging¶
# S3 Access Logging
resource "aws_s3_bucket_logging" "main" {
bucket = aws_s3_bucket.main.id
target_bucket = aws_s3_bucket.logs.id
target_prefix = "s3-access-logs/"
}
# VPC Flow Logs
resource "aws_flow_log" "main" {
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
}
# CloudTrail
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_log_file_validation = true
}
Secure CI/CD¶
GitHub Actions Example¶
name: Terraform
on:
push:
branches: [main]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Terraform
uses: hashicorp/setup-terraform@v1
- name: Terraform Init
run: terraform init
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform Plan
run: terraform plan
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply -auto-approve
Use OIDC Instead of Static Credentials¶
# Configure AWS to trust GitHub OIDC
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [
"6938fd4d98bab03faadb97b34396831e3780aea1"
]
}
Security Checklist¶
Before Deployment¶
- [ ] No hardcoded secrets in code
- [ ] Sensitive variables marked as sensitive
- [ ] State backend encrypted
- [ ] .gitignore configured properly
- [ ] IAM roles follow least privilege
- [ ] Security groups are restrictive
- [ ] Encryption enabled (at rest & in transit)
- [ ] Logging and monitoring enabled
- [ ] Resource tagging implemented
- [ ] Code reviewed by team
After Deployment¶
- [ ] Verify no secrets in state file
- [ ] Check security group rules
- [ ] Validate encryption settings
- [ ] Review IAM permissions
- [ ] Monitor CloudTrail logs
- [ ] Regular security audits
- [ ] Rotate credentials regularly
- [ ] Update dependencies
Common Security Mistakes¶
1. Exposing Secrets¶
# ❌ BAD
variable "password" {
default = "MyPassword123"
}
# ✅ GOOD
variable "password" {
type = string
sensitive = true
# No default - must be provided securely
}
2. Overly Permissive Security Groups¶
3. Unencrypted State¶
# ❌ BAD
backend "s3" {
bucket = "my-state"
# No encryption!
}
# ✅ GOOD
backend "s3" {
bucket = "my-state"
encrypt = true
}
4. Committing State Files¶
Summary¶
Key Security Principles: 1. Never hardcode secrets 2. Always encrypt state 3. Use least privilege IAM 4. Enable encryption everywhere 5. Implement proper logging 6. Regular security audits 7. Use .gitignore properly 8. Secure CI/CD pipelines