ข้ามไปยังเนื้อหา

Sensitive Data and Team Workflows

การตั้งค่าเป็น sensitive = true ซ่อนค่านั้นจาก terminal แต่ค่ายังอยู่แบบ plaintext ใน state file เหมือนเดิม ดังนั้นทางแก้ที่แท้จริงคือปกป้อง state backend เอง และไม่เอา secret ระยะยาวเข้ามาไว้ใน Terraform เลยตั้งแต่แรก

ทั้ง variable และ output block รับ argument sensitive ได้

variable "database_password" {
type = string
sensitive = true
}
output "db_connect_string" {
value = "Server=${aws_db_instance.main.address};Pwd=${var.database_password}"
sensitive = true
}

พอตั้ง sensitive = true แล้ว Terraform จะซ่อนค่านั้นทุกที่ที่ปกติจะโผล่บน terminal หรือใน CI log — โดย print (sensitive value) ใน output ของ terraform plan และ terraform apply แทนค่าจริง และไม่ยอม print ค่าดิบจาก terraform output เว้นแต่คุณจะขอตรง ๆ ด้วย terraform output db_connect_string นี่คือ guardrail ที่มีประโยชน์จริง เพราะกันไม่ให้ password เลื่อนผ่านหน้าจอไปใน CI log ที่แชร์กัน หรือ terminal ที่ screen-share อยู่โดยไม่ตั้งใจ

สิ่งที่ต้องพูดให้ชัดคือความเข้าใจผิดที่พบบ่อย sensitive = true เป็น feature ด้าน display ไม่ใช่ feature ด้านการเข้ารหัส ค่าถูกคำนวณ เก็บ และ diff โดย Terraform เหมือนเดิมทุกอย่างไม่ว่าจะตั้ง sensitive หรือไม่ — ค่ายังไปอยู่ใน terraform.tfstate แบบ plaintext เต็ม ๆ พร้อม tag ทุกตัว ใครก็ตามที่อ่าน state file ดิบได้ ไม่ว่าจะจาก S3 bucket, .tfstate local หรือ terraform state pull ก็เห็น password ตัวนั้นนอนอยู่ใน JSON ตรง ๆ ไม่ว่าจะมี flag sensitive หรือไม่มีก็ตาม

Terminal window
terraform state pull | grep -A3 db_connect_string
# "db_connect_string": {
# "value": "Server=mydb.abc123.us-east-1.rds.amazonaws.com;Pwd=S3cretPassword!",
# "type": "string"

เพราะการตั้งค่าให้เป็น sensitive ไม่ได้เอาค่านั้นออกจาก state การป้องกันจริง ๆ ต้องเกิดที่ layer ของ storage ให้ดูแล terraform.tfstate เองเหมือนที่คุณดูแลไฟล์ .env ที่เต็มไปด้วย credential ของ production

  • เข้ารหัส backend ที่ rest สำหรับ S3 backend ที่พูดถึงในบทก่อน หมายถึงเปิด S3 server-side encryption (SSE) บน state bucket เพื่อให้ทุก object — รวมถึงทุก version ของ state file — ถูกเข้ารหัสบน disk ไม่ว่าใครจะเรียก bucket API
  • จำกัดว่าใครอ่าน bucket ได้ IAM policy ที่จำกัด access ของ state bucket ให้เหลือแค่ role และ user ที่ต้องรัน Terraform จริง ๆ คือสิ่งที่กันไม่ให้คนที่ไม่ได้รับอนุญาตไปถึง plaintext ได้ ไม่ว่าจะเข้ารหัสที่ rest ไว้หรือไม่ก็ตาม — encryption at rest ป้องกันคนที่เข้าถึง disk ดิบ ไม่ใช่ป้องกันคนที่มี AWS credential ถูกต้องแล้วเรียก s3:GetObject
resource "aws_s3_bucket" "state" {
bucket = "my-company-terraform-state"
}
resource "aws_s3_bucket_server_side_encryption_configuration" "state" {
bucket = aws_s3_bucket.state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}

ถ้าเป็นไปได้ วิธีที่ดีกว่าคือหลีกเลี่ยงปัญหานี้ไปเลย อย่าเอา secret ระยะยาวใส่เข้าไปใน Terraform variable ตั้งแต่แรกถ้าดึงตอน apply ได้ data source ที่อ่านจาก secrets manager จะดึงค่าปัจจุบันตอน Terraform รัน โดยที่คุณไม่ต้องพิมพ์ secret ลงไฟล์ .tf หรือ .tfvars เลย

data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/app-db/password"
}
resource "aws_db_instance" "main" {
identifier = "app-db"
engine = "postgres"
instance_class = "db.t3.micro"
username = "app_admin"
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}

ต้องเข้าใจให้ชัดว่าวิธีนี้แก้อะไรและไม่แก้อะไร ค่า secret ที่ resolve ออกมายังไปจบอยู่ใน state entry ของ aws_db_instance.main เหมือนเดิม เพราะ Terraform ต้อง record attribute ที่ตั้งไว้ เหมือนความเสี่ยง plaintext จากบทแรกของ module นี้ สิ่งที่แก้ได้จริงคือ lifecycle ของ secret ในที่อื่น ๆ ทั้งหมด — ไม่เคยถูกพิมพ์ลง variable ไม่เคยอยู่ในไฟล์ .tfvars ที่อาจโดน commit พลาด และการ rotate ใน Secrets Manager ก็ไม่ต้องแตะ Terraform configuration เลย การปกป้อง state backend เองยังคงเป็นส่วนที่ต่อรองไม่ได้

บทก่อนตั้ง S3 backend พร้อม locking ไว้แล้ว ซึ่งแก้ปัญหา “ทุกคนอ่านและเขียน state เดียวกันได้อย่างปลอดภัย” นั่นจำเป็น แต่ไม่พอเมื่อทีมโตเกินสองสามคน ถ้าไม่มีอะไรเพิ่มเติม การที่ “ทุกคนรัน terraform apply จาก laptop ของตัวเองเมื่อไหร่ก็ได้ตามใจ” ยังมีช่องว่างจริง ๆ อยู่

  • run apply ที่เกิดพร้อมกันไม่ทำให้ state เสียหายแล้วเพราะมี locking แต่ก็ยังเข้าคิวรอกัน — engineer คนที่สองแค่รอเฉย ๆ โดยไม่รู้ว่า run แรกกำลังทำอะไรอยู่หรือจะใช้เวลานานแค่ไหน
  • ไม่มีจุดตามธรรมชาติให้คนที่สอง review change ก่อนขึ้นจริง locking กันแค่ apply สองตัวไม่ให้แข่งกัน แต่ไม่ได้กัน engineer คนหนึ่งจาก apply change ที่ไม่มีใครเคยดูมาก่อน
  • engineer แต่ละคนต้องมี AWS credential ที่มีสิทธิ์พอจะรัน apply ได้จาก local นั่นเป็น blast radius ที่กว้างกว่าที่ทีมส่วนใหญ่อยากแจกให้ทุก laptop

ทั้งหมดนี้ไม่ได้แปลว่า shared backend จากบทก่อนผิด เพราะเป็นสิ่งที่ต้องมีก่อน ไม่ใช่ความผิดพลาด แค่ยังไม่ใช่เรื่องทั้งหมด การปิดช่องว่างนี้คือสิ่งที่ CI/CD สำหรับ infrastructure มีไว้ทำ — pipeline รัน plan บนทุก change ที่เสนอมาเพื่อ review ก่อนใครจะ apply และ apply รันจาก pipeline identity ที่ควบคุมได้ แทนที่จะรันจาก laptop ของแต่ละคน นี่เป็นหัวข้อของบทหลังในคอร์สนี้ ไม่ใช่สิ่งที่ต้องแก้ตรงนี้ — ประเด็นตอนนี้คือให้รู้จักรูปร่างของช่องว่างนี้ไว้ก่อน จะได้ไม่แปลกใจทีหลัง

flowchart LR
  sv["variable with sensitive = true"] -->|redacted| cli["CLI output: (sensitive value)"]
  sv -->|full value written| state["terraform.tfstate: plaintext"]
  sm["Secrets Manager"] -->|data source at apply time| res["Resource attribute"]
  res -->|attribute recorded| state
A sensitive variable is redacted from CLI output but still lands in state as plaintext, versus a secret pulled from Secrets Manager at apply time
การตั้ง sensitive = true บน Terraform variable ป้องกันอะไรจริง ๆ
database password ถูกตั้ง sensitive = true บน variable block ของตัวเอง ค่านั้นยังอ่านได้แบบ plaintext ที่ไหนไหม
วิธีป้องกัน sensitive data ที่เก็บอยู่ใน Terraform state file ที่แท้จริงคืออะไร
ทำไมการดึง secret จาก secrets manager ผ่าน data source มักดีกว่าการเก็บเป็น Terraform variable สำหรับ credential ระยะยาว