Sensitive Data in State
State file มักมี secrets (passwords, keys, tokens) เป็น plain text — เรียนวิธีจัดการ + ป้องกัน
ปัญหา: Secrets ใน State Plain Text
ตัวอย่าง:
resource "aws_db_instance" "main" {
username = "admin"
password = "supersecret123" # ← state เก็บค่านี้!
}
State file:
{
"resources": [{
"type": "aws_db_instance",
"instances": [{
"attributes": {
"username": "admin",
"password": "supersecret123" // plain text!
}
}]
}]
}
→ ใครเข้าถึง state file ได้ = เห็น password
Threats
| Threat | Impact |
|---|---|
| State file commit ลง Git | 🔴 ทุกคนใน repo เห็น |
| S3 bucket public | 🔴 ทั่วโลกเห็น |
| Local laptop hacked | 🔴 secret leak |
| State sent in PR | 🔴 secret in CI logs |
Mitigations: Multiple Layers
Layer 1: Encrypt at Rest
S3 Server-Side Encryption
resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms" # ← KMS encryption
kms_master_key_id = aws_kms_key.tfstate.arn
}
}
}
resource "aws_kms_key" "tfstate" {
description = "Encryption for Terraform state"
enable_key_rotation = true
deletion_window_in_days = 30
}
Backend ระบุ encryption
terraform {
backend "s3" {
bucket = "my-tfstate"
key = "prod/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true # ⭐
kms_key_id = "alias/my-tfstate-key" # ⭐
dynamodb_table = "terraform-locks"
}
}
Layer 2: Encrypt in Transit
S3 + DynamoDB ใช้ HTTPS อัตโนมัติ — แต่ enforce policy:
resource "aws_s3_bucket_policy" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "DenyInsecureTransport"
Effect = "Deny"
Principal = "*"
Action = "s3:*"
Resource = [
aws_s3_bucket.tfstate.arn,
"${aws_s3_bucket.tfstate.arn}/*"
]
Condition = {
Bool = {
"aws:SecureTransport" = "false"
}
}
}]
})
}
Layer 3: IAM Restriction
resource "aws_iam_policy" "tfstate_access" {
name = "tfstate-access"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject"
]
Resource = "${aws_s3_bucket.tfstate.arn}/*"
},
{
Effect = "Allow"
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
]
Resource = aws_dynamodb_table.tfstate_lock.arn
}
]
})
}
→ มอบสิทธิ์เฉพาะคน/role ที่จำเป็น
Layer 4: Use External Secret Manager
แทนที่จะให้ Terraform manage secret — fetch จากที่อื่นทุกครั้ง
# อ่าน secret จาก AWS Secrets Manager (ไม่ใช่ create)
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
}
→ State เก็บ ARN reference, ไม่เก็บค่าจริง... WAIT — จริงๆ Terraform เก็บค่าใน state เพราะ argument ตอน apply
แก้: ใช้ ignore_changes + write-only
resource "aws_db_instance" "main" {
username = "admin"
password = data.aws_secretsmanager_secret_version.db_password.secret_string
lifecycle {
ignore_changes = [password]
}
}
Layer 5: Rotate ผ่าน External Tool
ให้ AWS Secrets Manager rotate password เอง:
resource "aws_secretsmanager_secret_rotation" "db" {
secret_id = aws_secretsmanager_secret.db.id
rotation_lambda_arn = aws_lambda_function.rotate.arn
rotation_rules {
automatically_after_days = 30
}
}
→ Password เปลี่ยนนอกเหนือ Terraform — state ไม่ track
Sensitive Marker
ทำให้ Terraform ซ่อนค่าจาก plan/apply log:
variable "db_password" {
type = string
sensitive = true # ← ซ่อนจาก log
}
output "db_endpoint" {
value = aws_db_instance.main.endpoint
sensitive = true # ← ซ่อนจาก output
}
- Sensitive = ซ่อนจาก log/output
- ค่ายังเก็บ plain text ใน state
ต้องใช้ encryption + access control ปกป้อง state file ตัวเอง
Detect Sensitive Leak
ใช้ tools:
git-secrets (AWS)
brew install git-secrets
git secrets --register-aws --global
git secrets --install
→ เตือนถ้าเผลอ commit AWS credential
detect-secrets (Yelp)
pip install detect-secrets
detect-secrets scan
→ Scan repo หา hard-coded secret
Pre-commit Hook
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
ตัวอย่าง: Secure Pattern
# Generate password (ครั้งเดียว)
resource "random_password" "db" {
length = 32
special = true
lifecycle {
ignore_changes = [length, special] # อย่าสร้างใหม่
}
}
# Store ใน Secrets Manager
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 in RDS
resource "aws_db_instance" "main" {
identifier = "prod-db"
username = "admin"
password = random_password.db.result # state มีค่านี้
lifecycle {
ignore_changes = [password]
}
}
# Output reference, not value
output "db_secret_arn" {
value = aws_secretsmanager_secret.db_password.arn
description = "Get password: aws secretsmanager get-secret-value --secret-id $(terraform output -raw db_secret_arn)"
}
Best Practices
✅ DO:
- Encrypt state (S3 + KMS)
- IAM policy ที่จำกัดเฉพาะคน/role ที่จำเป็น
- Use external secret manager (Vault, Secrets Manager)
- Sensitive variables/outputs (ซ่อนจาก log)
- Audit access via CloudTrail
- Rotate secrets นอกเหนือ Terraform
❌ DON'T:
- ห้าม commit state file
- ห้าม hard-code password ใน .tf
- ห้าม share state file ผ่าน email/Slack
- ห้ามคิดว่า sensitive = encrypted
หาก State Leak แล้วทำยังไง?
- Rotate ทุก secret ที่อยู่ใน state (assume compromised)
- Audit access logs (CloudTrail) ใครดู
- Delete leaked file (S3 versions, Git history with
git filter-repo) - Block source ของ leak
- Re-deploy infrastructure ที่ผูกกับ secret เก่า
สรุป
- State มี secrets เป็น plain text
- ป้องกัน 5 layers: encrypt at rest, encrypt in transit, IAM, secret manager, rotation
- Sensitive marker ซ่อนจาก log แต่ไม่ encrypt state
- ใช้ external secret manager (Vault, Secrets Manager) แทนเก็บใน state
- Detect leak ด้วย git-secrets / detect-secrets / pre-commit
- Plan recovery — รู้ว่าจะทำยังไงถ้า leak
ต่อไป → Best Practices for State