Skip to main content

Secret Management

จัดการ secrets (passwords, API keys, certificates) ใน Terraform อย่างปลอดภัย — ไม่ให้ leak ใน state, code, log

ปัญหา: Secrets ใน Terraform

Terraform เก็บ resource attributes ใน state file เป็น plain text — รวมถึง:

  • Database passwords
  • API tokens
  • Private keys
  • Auth credentials

→ ใครเข้าถึง state = เห็น secrets

Layered Defense

Anti-Patterns

❌ Hard-coded Secrets

resource "aws_db_instance" "main" {
password = "MyP@ssw0rd123" # ❌ in code → in Git → leak
}

❌ Plain .tfvars Committed

terraform.tfvars (committed!)
db_password = "MyP@ssw0rd"
api_key = "sk_live_abc123"

❌ Sensitive Variable without External Source

variable "db_password" {
sensitive = true
default = "MyP@ssw0rd" # ❌ still in code
}

✅ Pattern 1: External Secret Manager

AWS Secrets Manager

# Read existing secret (created out-of-band)
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/db-password"
}

resource "aws_db_instance" "main" {
username = "admin"
password = data.aws_secretsmanager_secret_version.db_password.secret_string

lifecycle {
ignore_changes = [password] # rotate by Secrets Manager, ignore drift
}
}

→ Secret อยู่ใน Secrets Manager, Terraform แค่ read

AWS SSM Parameter Store

data "aws_ssm_parameter" "db_password" {
name = "/prod/db/password"
with_decryption = true
}

resource "aws_db_instance" "main" {
password = data.aws_ssm_parameter.db_password.value
}

HashiCorp Vault

provider "vault" {
address = "https://vault.example.com"
# auth via token, AWS auth, etc.
}

data "vault_generic_secret" "db" {
path = "secret/data/prod/db"
}

resource "aws_db_instance" "main" {
password = data.vault_generic_secret.db.data["password"]
}

✅ Pattern 2: Generate + Store

ให้ Terraform generate secret + store ใน Secrets Manager:

# Generate
resource "random_password" "db" {
length = 32
special = true

lifecycle {
ignore_changes = [length, special] # don't regenerate
}
}

# Store
resource "aws_secretsmanager_secret" "db_password" {
name = "prod/db-password"
recovery_window_in_days = 30

kms_key_id = aws_kms_key.secrets.arn
}

resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
secret_string = random_password.db.result
}

# Use
resource "aws_db_instance" "main" {
username = "admin"
password = random_password.db.result
}

# Output reference, NOT value
output "db_secret_arn" {
value = aws_secretsmanager_secret.db_password.arn
}

→ Generated once, stored encrypted, retrievable via ARN

✅ Pattern 3: TF_VAR via Env

# Fetch จาก Vault ตอน apply
export TF_VAR_db_password=$(vault kv get -field=password secret/db)
terraform apply
variable "db_password" {
type = string
sensitive = true
}

resource "aws_db_instance" "main" {
password = var.db_password
}

→ Secret ไม่อยู่ใน code/Git แต่ยังอยู่ใน state

✅ Pattern 4: SOPS (Encrypted .tfvars)

SOPS = encrypt YAML/JSON ด้วย KMS/PGP

# Install
brew install sops

# Encrypt
sops --encrypt --kms 'arn:aws:kms:...' secrets.tfvars > secrets.tfvars.encrypted

# Commit encrypted file
git add secrets.tfvars.encrypted

ตอนใช้:

sops --decrypt secrets.tfvars.encrypted > secrets.tfvars
terraform apply -var-file=secrets.tfvars
rm secrets.tfvars

EC2 Instance Profile (No Secret Needed!)

แทน password — ใช้ IAM:

resource "aws_iam_instance_profile" "app" {
name = "app"
role = aws_iam_role.app.name
}

resource "aws_iam_role" "app" {
name = "app"
assume_role_policy = jsonencode({...})
}

resource "aws_iam_role_policy" "app" {
policy = jsonencode({
Statement = [{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
Resource = aws_secretsmanager_secret.db_password.arn
}]
})
}

resource "aws_instance" "app" {
iam_instance_profile = aws_iam_instance_profile.app.name
# Application fetches secret via AWS SDK + IAM role
}

→ EC2 ดึง secret เอง ตอน runtime — Terraform ไม่ต้องรู้

Sensitive Variables/Outputs

variable "api_key" {
type = string
sensitive = true # ซ่อนจาก plan/apply log
}

output "db_password" {
value = random_password.db.result
sensitive = true # ซ่อนจาก output
}
Sensitive ≠ Encrypted

แค่ซ่อนจาก log — state ยังเก็บ plain text

State Encryption

S3 + KMS

terraform {
backend "s3" {
bucket = "my-tfstate"
key = "prod/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
kms_key_id = "alias/tfstate" # KMS key
dynamodb_table = "terraform-locks"
}
}

ดูเพิ่มใน Section 10: Sensitive Data in State

Detect Leaked Secrets

git-secrets

brew install git-secrets
git secrets --register-aws
git secrets --install
git secrets --scan

detect-secrets

pip install detect-secrets
detect-secrets scan > .secrets.baseline

Pre-commit hook:

.pre-commit-config.yaml
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets

gitleaks

brew install gitleaks
gitleaks detect --source .

Secret Rotation

ตั้งให้ Secrets Manager rotate อัตโนมัติ:

resource "aws_secretsmanager_secret_rotation" "db" {
secret_id = aws_secretsmanager_secret.db_password.id
rotation_lambda_arn = aws_lambda_function.rotate.arn

rotation_rules {
automatically_after_days = 30
}
}

→ Password rotate ทุก 30 วัน นอกเหนือ Terraform

Multi-Cloud Secret Managers

CloudServiceProvider Resource
AWSSecrets Manageraws_secretsmanager_secret_version (data)
AWSSSM Parameter Storeaws_ssm_parameter (data)
GCPSecret Managergoogle_secret_manager_secret_version (data)
AzureKey Vaultazurerm_key_vault_secret (data)
HashiCorpVaultvault_generic_secret (data)

.gitignore (สำคัญ!)

.gitignore
# Terraform
.terraform/
*.tfstate
*.tfstate.*
*.tfplan

# Variables
*.tfvars
!example.tfvars
secrets.auto.tfvars
*.auto.tfvars

# Backups
*.bak
*.backup

# Sensitive files
.env
*.pem
*.key

Best Practices

✅ DO:
- เก็บ secret ใน external secret manager
- Encrypt state at rest (S3 + KMS)
- IAM ที่จำกัด state access
- Rotate secrets automatically
- Use sensitive marker
- Detect secrets ก่อน commit (git-secrets)
- ใช้ EC2 instance profile แทน hard-coded credentials

❌ DON'T:
- ห้าม commit secret ใน .tf หรือ .tfvars
- ห้าม share secret ผ่าน Slack/email
- ห้ามคิดว่า sensitive = encrypted
- ห้ามใช้ provisioner รับ password
- ห้าม keep credentials ใน laptop forever

ตัวอย่าง: Production-Grade Setup

# 1. KMS for state + secrets
resource "aws_kms_key" "secrets" {
description = "Application secrets"
enable_key_rotation = true
deletion_window_in_days = 30
}

# 2. Generate password
resource "random_password" "db" {
length = 32
special = true
lifecycle {
ignore_changes = [length, special]
}
}

# 3. Store in Secrets Manager
resource "aws_secretsmanager_secret" "db" {
name = "prod/db-password"
kms_key_id = aws_kms_key.secrets.arn
}

resource "aws_secretsmanager_secret_version" "db" {
secret_id = aws_secretsmanager_secret.db.id
secret_string = random_password.db.result
}

# 4. Auto rotation
resource "aws_secretsmanager_secret_rotation" "db" {
secret_id = aws_secretsmanager_secret.db.id
rotation_lambda_arn = aws_lambda_function.rotate_db.arn
rotation_rules {
automatically_after_days = 30
}
}

# 5. RDS uses password
resource "aws_db_instance" "main" {
password = random_password.db.result
lifecycle {
ignore_changes = [password] # rotation handles updates
}
}

# 6. Output ARN, not value
output "db_secret_arn" {
value = aws_secretsmanager_secret.db.arn
description = "Use: aws secretsmanager get-secret-value --secret-id $(terraform output -raw db_secret_arn)"
}

สรุป

  • Layered defense: external secret manager + encrypted state + IAM + rotation
  • ใช้ AWS Secrets Manager / Vault / SSM แทน hard-code
  • sensitive = true ซ่อนจาก log (ไม่ใช่ encryption)
  • ใช้ IAM instance profile แทน password เมื่อทำได้
  • Detect leaks ด้วย git-secrets / detect-secrets / gitleaks
  • Rotate secrets นอกเหนือ Terraform

ต่อไป → Compliance / Sentinel