Sensitive Data and Team Workflows
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”การตั้ง variable หรือ output เป็น sensitive = true แค่ซ่อนค่านั้นจาก CLI output แต่ค่านั้นยังถูกเก็บแบบ plaintext อยู่ใน state file ดังนั้นการป้องกัน state เองต่างหากคือปัญหาที่แยกออกไป และสำคัญกว่าด้วย
sensitive = true ทำอะไรจริง ๆ
หัวข้อที่มีชื่อว่า “sensitive = true ทำอะไรจริง ๆ”คุณตั้ง variable หรือ output เป็น sensitive ได้
variable "db_password" { type = string sensitive = true}
output "db_password" { value = var.db_password sensitive = true}พอตั้งแบบนี้แล้ว terraform plan และ terraform apply จะ print (sensitive value) แทนที่จะโชว์ password จริงใน terminal และค่านั้นถูก redact จาก CI log ในแบบเดียวกัน นี่เป็น feature ที่มีประโยชน์จริง เพราะหยุดไม่ให้ secret ไปโผล่ใน build log ที่คนสิบกว่าคนอ่านได้ หรือขึ้นจอตอน pairing session
แต่ก็เป็นสิ่งที่คนเข้าใจผิดบ่อยเหมือนกัน sensitive = true เป็น feature ระดับ display-redaction ไม่ใช่ feature ด้าน encryption ค่าจริงยังถูกเขียนลง terraform.tfstate แบบ plaintext เป๊ะ ๆ ตามที่คุยไว้ในบทแรกของ module นี้ ใครก็ตามที่อ่าน state file ดิบได้ ก็อ่าน password ได้เหมือนกัน ไม่ว่าจะตั้ง resource หรือ variable กี่ตัวเป็น sensitive ระหว่างทางก็ตาม
ป้องกัน state file ให้ปลอดภัยจริง ๆ
หัวข้อที่มีชื่อว่า “ป้องกัน state file ให้ปลอดภัยจริง ๆ”เพราะ redaction ไม่ได้ป้องกันไฟล์ตอน at rest การป้องกันจริงต้องเกิดที่ storage layer แทน
- พึ่ง server-side encryption ของ Cloud Storage สำหรับ state bucket GCS เข้ารหัส object แบบ at rest ให้เป็น default อยู่แล้ว และคุณเพิ่ม customer-managed Cloud KMS key ทับเข้าไปได้อีกชั้นผ่าน argument
kms_encryption_keyของ backend ถ้าองค์กรคุณต้องการ - จำกัดว่าใครอ่าน bucket ได้ด้วย IAM ให้แคบที่สุดเท่าที่ทำได้ ให้
roles/storage.objectViewerบน state bucket ตัวที่ระบุเจาะจง กับ principal ที่ระบุเจาะจงที่ต้องใช้จริง ๆ — service account ของ CI, ทีม platform เล็ก ๆ — แทนที่จะให้ access กว้าง ๆ ระดับ project ปฏิบัติกับ state file เหมือนเป็น secret เพราะในทางปฏิบัติแล้วก็คือ secret ตัวหนึ่ง
ถ้าเป็นไปได้ หลีกเลี่ยงการใส่ secret ที่อยู่ยาว ๆ ลงใน Terraform configuration หรือ state เลย แทนที่จะให้ variable เก็บ password ดิบ ๆ ให้ดึงค่านั้นมาตอน apply จาก secrets manager แทน
data "google_secret_manager_secret_version" "db_password" { secret = "db-password"}
resource "google_sql_user" "app" { name = "app_admin" instance = google_sql_database_instance.main.name password = data.google_secret_manager_secret_version.db_password.secret_data}วิธีนี้ไม่ได้ทำให้ค่านั้นหายไปจาก state ทั้งหมด — attribute ของ resource ที่ Terraform ตั้งไว้ก็ยังถูกบันทึกเหมือนเดิมตลอดมา สิ่งที่เปลี่ยนคือที่ที่ source of truth ระยะยาวของ secret นั้นอยู่ นั่นคือ Secret Manager ที่มี access policy, audit logging และเรื่อง rotation ของตัวเอง แทนที่จะเป็น Terraform variable ที่ต้องมีใครสักคนจำไว้ให้ pass เข้ามาอย่างปลอดภัยทุกครั้งที่รัน
nonsensitive() ยกเลิกการ redact ค่าอย่างตั้งใจ
หัวข้อที่มีชื่อว่า “nonsensitive() ยกเลิกการ redact ค่าอย่างตั้งใจ”บางครั้ง sensitive = true แพร่กระจายไปไกลกว่าที่คุณต้องการ — expression ใด ๆ ที่แตะค่า sensitive จะกลายเป็น sensitive ไปด้วย แม้ผลลัพธ์จริง ๆ จะไม่ใช่ secret เลยก็ตาม ฟังก์ชัน nonsensitive() เอา marking นั้นออกจากค่าเดียวอย่างตั้งใจ สำหรับกรณีแบบนั้นเป๊ะ ๆ พิสูจน์จริงด้วย terraform console แสดงการ redact ก่อน แล้วค่อยเอาออก
$ echo 'var.db_password' | terraform console(sensitive value)$ echo 'nonsensitive(var.db_password)' | terraform console"S3cretPassword!"นั่นคือกลไกทั้งหมด nonsensitive() ไม่ได้เพิ่มการป้องกันอะไร และไม่ได้เอาค่าออกจาก state ไม่ว่ากรณีไหน — มันแค่เปลี่ยนว่าการ redact ของ Terraform จะใช้กับผลลัพธ์ของ expression ตัวนี้โดยเฉพาะต่อจากนี้หรือไม่เท่านั้น ใช้มันเฉพาะตอนที่คุณมั่นใจว่าค่านั้นไม่ได้เป็น secret จริง ๆ ในบริบทนั้น — เรียกมันกับ secret จริงแค่เพื่อปิด warning จะทำลายการป้องกันเดียวที่ sensitive = true มีให้
Ephemeral value ถูกกันออกจาก state และ plan โดยโครงสร้าง
หัวข้อที่มีชื่อว่า “Ephemeral value ถูกกันออกจาก state และ plan โดยโครงสร้าง”Terraform 1.10 เพิ่ม resource, variable และ output แบบ ephemeral — ค่าที่มีอยู่แค่ตลอด operation เดียวของ Terraform และไม่เคยถูกเขียนลง state หรือ saved plan file เลย ไม่ใช่ “เขียนแต่ redact” แบบที่ sensitive = true ทำ ตั้ง variable เป็น ephemeral แบบนี้
variable "session_token" { type = string ephemeral = true}google provider เองก็ ship ephemeral resource ที่ตรงกันไว้ สำหรับอ่าน secret จาก Secret Manager โดยไม่เคย record มันเลย document ไว้พร้อมตัวอย่างจริงครบ (registry.terraform.io/providers/hashicorp/google/latest/docs/ephemeral-resources/secret_manager_secret_version): ephemeral "google_secret_manager_secret_version" "example" { project = "my-project"; secret = "my-secret"; version = "latest" } แล้วเอา .secret_data ของมันไปส่งต่อเข้า write-only argument ของ resource อื่น คอร์สนี้ไม่มี Secret Manager secret จริงให้อ่าน พิสูจน์แบบ offline ข้างล่างจึงใช้ pattern built-in แทน — ค่าอายุสั้นที่ generate สด ๆ ทุกครั้งที่รัน ไม่เคย persist ไว้ที่ไหนเลย
ephemeral "random_password" "db" { length = 16}เพราะ ephemeral value ไม่เคยถูก persist Terraform จึง enforce ว่า ที่ไหน ที่มันไหลไปได้ ตอน plan time ด้วย error จริง — ไม่ใช่แค่ convention พิสูจน์ ส่ง ephemeral.random_password.db.result เข้า argument ธรรมดา (ไม่ใช่ write-only) ของ resource
$ terraform planError: Invalid use of ephemeral value
with terraform_data.marker, on main.tf line 15, in resource "terraform_data" "marker": 15: input = ephemeral.random_password.db.result
Ephemeral values are not valid for "input", because it is not a write-onlyattribute and must be persisted to state.error นั้นระบุกฎจริงตรง ๆ ค่า ephemeral ไหลไปได้แค่ใน attribute ที่ provider ประกาศเป็น write-only ไว้ชัดเจนเท่านั้น เพราะ write-only attribute คือ argument ชนิดเดียวที่ Terraform รับประกันว่าจะไม่ persist เลย ข้อจำกัดจริงอีกข้อ พิสูจน์แบบเดียวกัน output แบบ ephemeral ใช้ใน root module ไม่ได้เลย —
$ terraform planError: Ephemeral outputs are not allowed in context of a root module— ephemeral output มีไว้ให้ child module ส่งค่า ephemeral กลับขึ้นไปให้ตัวที่เรียกมัน (เพื่อให้ผู้เรียกเอาไปใช้กับ write-only argument ของตัวเองหรือส่งต่อเป็น ephemeral chain) ไม่ใช่ให้ root module print มันออกมา
Write-only argument (_wo) อีกครึ่งหนึ่งของกลไก
หัวข้อที่มีชื่อว่า “Write-only argument (_wo) อีกครึ่งหนึ่งของกลไก”Terraform 1.11 เพิ่ม write-only argument — resource attribute ที่ตามธรรมเนียมลงท้ายด้วย _wo คู่กับ integer _wo_version ที่ provider สัญญาว่าจะไม่เขียนลง state หรือ saved plan เลย นี่ไม่ใช่สมมติฐานสำหรับ google provider google_secret_manager_secret_version ประกาศคู่นี้จริง ตาม docs จริงของ provider — “secret_data_wo - (Optional, Write-Only) The secret data. … Note: This property is write-only and will not be read from the API.” / “secret_data_wo_version - (Optional) Triggers update of secret_data_wo write-only. Increment this value when an update to secret_data_wo is needed.” — และ note ระดับบนสุดของ resource เองก็ยืนยันตรง ๆ “All arguments marked as write-only values will not be stored in the state: secret_data_wo”
ยืนยันตรง ๆ กับ schema ของ provider hashicorp/google 8.4.0 ที่ pin ไว้ — ไม่ต้องมี GCP credential เลย terraform providers schema -json แค่อ่านสิ่งที่ init ดาวน์โหลดมาแล้ว และการเช็คแบบเดียวกันนี้เจอ resource ทั้งหมด 7 ตัวทั่วทั้ง provider ที่มี write-only attribute (google_secret_manager_secret_version, google_sql_database_instance, google_sql_user, google_alloydb_user, google_compute_ssl_certificate, google_compute_region_ssl_certificate, google_compute_vpn_tunnel)
$ terraform providers schema -json | python3 -c "import json,sysd = json.load(sys.stdin)attrs = d['provider_schemas']['registry.terraform.io/hashicorp/google']['resource_schemas']['google_secret_manager_secret_version']['block']['attributes']for k, v in attrs.items(): if v.get('write_only') or k.endswith('_wo_version'): print(k, v)"secret_data_wo {'type': 'string', 'description_kind': 'plain', 'optional': True, 'write_only': True}secret_data_wo_version {'type': 'number', 'description_kind': 'plain', 'optional': True}สิ่งที่ต้องการ credential จริงคือการ apply มันต่างหาก — สร้าง Secret Manager secret version จริงต้องมี GCP project จริง คำถามที่ต้องตอบตรง ๆ แทนคือ write-only support เป็นสากลทุก resource หรือ opt-in เช็คกับ random_password ของ hashicorp/random — resource ที่คอร์สนี้ apply ได้เต็มรูปแบบแบบ offline — ด้วย query เดียวกันเป๊ะ
$ terraform providers schema -json | python3 -c "import json,sysd = json.load(sys.stdin)attrs = d['provider_schemas']['registry.terraform.io/hashicorp/random']['resource_schemas']['random_password']['block']['attributes']print([k for k,v in attrs.items() if v.get('write_only')])"[]random_password ไม่มี write-only attribute เลยแม้แต่ตัวเดียว เหมือนกับ built-in terraform_data ภาพจริงที่พิสูจน์แล้วทั้งสองด้านข้างบนคือ write-only support ไม่ใช่การรับประกันทั่วทั้ง Terraform ที่ resource ไหนก็ได้ฟรีตั้งแต่ 1.11 — มันคือสิ่งที่ผู้เขียน provider แต่ละคนเพิ่มเข้ามาทีละ resource ทีละ attribute และตามเวอร์ชันที่ pin ไว้นี้มี resource google 7 ตัวที่มี ในขณะที่ resource ที่ apply ได้แบบ offline เต็มรูปแบบของคอร์สนี้ไม่มีตัวไหนมีเลย สิ่งที่พิสูจน์ได้จริงแบบ offline ครบวงจร และพิสูจน์ไปแล้วข้างบนคือครึ่งหนึ่งของกลไกที่ทำให้ write-only argument มีประโยชน์ตั้งแต่แรก คือการ enforce ค่า ephemeral ว่าอะไรได้รับอนุญาตให้ไหลเข้า _wo argument บ้าง เมื่อ resource นั้นมี attribute แบบนี้แล้ว
จุดที่ shared backend เริ่มไม่พอแล้ว
หัวข้อที่มีชื่อว่า “จุดที่ shared backend เริ่มไม่พอแล้ว”บทก่อนหน้าใน module นี้พา state ไปอยู่บน shared GCS backend ที่มี locking built-in มาให้ ซึ่งแก้ปัญหาทีมที่แย่ที่สุดไปแล้ว — local file ที่ต่างกันไปเรื่อย ๆ และการเขียนพร้อมกันที่ corrupt กัน แต่ยังไม่ได้แก้ปัญหาว่าทีมที่กำลังโตควรทำงานร่วมกันยังไงให้ครบ ถ้าทุกคนยังรัน terraform apply จาก laptop ตัวเองเมื่อไหร่ก็ตามที่อยากรัน การ apply พร้อมกันก็แค่ต่อคิวกันหลัง lock แทนที่จะชนกัน และก็ยังไม่มีจุดตามธรรมชาติที่คนที่สอง review การเปลี่ยนแปลงก่อนที่จะมีผลจริงกับ infrastructure
ช่องว่างนั้น — ที่ที่ review ได้ audit ได้ สำหรับให้การเปลี่ยนแปลงถูก approve ก่อน apply จะรันกับ shared state — คือสิ่งที่ CI/CD สำหรับ infrastructure มาปิดช่องว่างนี้พอดี และเรื่องนี้จะพูดถึงในบทหลัง ๆ ของคอร์สนี้
flowchart LR varInput["variable db_password (sensitive = true)"] -->|redacted in CLI output| cli["terraform plan / apply output"] varInput -->|value still written| state["terraform.tfstate (plaintext)"] secretManager["Secret Manager secret version"] -->|read via data source at apply time| resource["google_sql_user.app"] resource --> state
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”สามกลไกในบทนี้ดูคล้ายกันจากภายนอก — sensitive, ephemeral, และ write-only argument — แต่แก้ปัญหาคนละอย่างกันจริง ๆ และความต่างอยู่ตรงที่ อะไรถูกเขียนลง state sensitive = true เขียนค่าลง state เต็ม ๆ แล้ว redact แค่ terminal — พิสูจน์ข้างบน ค่าดิบห่างแค่ state pull | grep เดียว ค่า ephemeral ไม่เคยถูกเขียนลง state เลย และ Terraform enforce เรื่องนี้ตอน plan time ด้วย error จริงทันทีที่คุณพยายามส่งมันเข้า attribute ธรรมดา — พิสูจน์ข้างบน write-only argument คือครึ่งฝั่ง provider ของการรับประกันเดียวกันนั้น _wo attribute คือที่เดียวที่ ephemeral value ได้รับอนุญาตให้ไปถึง เพราะ provider สัญญาว่าจะไม่ persist มันเลย — พิสูจน์ข้างบนว่า google_secret_manager_secret_version.secret_data_wo (และ resource พี่น้องอีกหกตัว) มีอยู่จริงใน schema ที่ pin ไว้ อ่านตามลำดับนี้ — display-only, กันออกจากโครงสร้าง, provider สัญญาว่าจะไม่ persist — ทั้งสามจะไม่ดูเหมือน “secret” สามแบบอีกต่อไป แต่กลายเป็นคำตอบสามแบบสำหรับคำถาม “ค่านี้ไปถึง disk ไหม”
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
sensitive = true | หยุดค่าเลื่อนผ่าน terminal หรือ CI log ที่แชร์กัน (พิสูจน์ข้างบน (sensitive value)) | ไม่มีผลกับ state เลย — พิสูจน์ข้างบน ค่าดิบยังห่างแค่ state pull เดียว |
variable/resource แบบ ephemeral | ไปไม่ถึง state หรือ saved plan ได้เลยโดยโครงสร้าง — enforce ด้วย error จริง ไม่ใช่แค่ convention (พิสูจน์ข้างบน) google ยัง ship ephemeral "google_secret_manager_secret_version" ให้อ่าน secret โดยไม่ต้อง record เลยด้วย | ใช้ได้แค่บริบทแคบ ๆ (write-only argument, provider config, ephemeral value อื่น) ephemeral output ใน root module ถูกปฏิเสธตรง ๆ (พิสูจน์ข้างบน) |
write-only (_wo) argument | ค่านั้นไม่แตะ state เลยจริง ๆ แม้จะชั่วคราวก็ตาม — พิสูจน์ข้างบนว่า resource google จริง 7 ตัวประกาศ attribute แบบนี้ | opt-in ต่อ resource ต่อ provider ไม่ใช่การรับประกันทั่ว 1.11 — พิสูจน์ข้างบน random_password ไม่มีเลยสักตัว ใช้จริงต้องมี GCP project จริงกับ credential |
nonsensitive() | ให้ค่าที่จริง ๆ ไม่ใช่ secret หลุดจากการ redact ที่ไม่จำเป็น (พิสูจน์ข้างบนผ่าน console) | เอาการป้องกันเดียวที่ sensitive = true มีให้ออกไป ถ้าใช้ผิดกับสิ่งที่เป็น secret จริง |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
sensitive = trueกันไม่ให้ค่าไปอยู่ใน state file” พิสูจน์ข้างบนสองครั้ง ทั้งข้อเท็จจริงจากstate pullก่อนหน้าในโมดูลนี้ และตัวอย่าง redact baseline — มันเปลี่ยนแค่การแสดงผลบน CLI ไม่เคยเปลี่ยนวิธีเก็บ - “
ephemeral = trueเป็นแค่sensitive = trueเวอร์ชันเข้มกว่า” พิสูจน์ข้างบน คนละกลไกกันเลยsensitiveredact การแสดงผลแต่ยังเขียนลง stateephemeralถูกกันออกจาก state และ plan โดยโครงสร้าง enforce ด้วย error จริง “Ephemeral values are not valid for …” ทันทีที่พยายาม persist - “resource ไหนก็รับ ephemeral value ได้ถ้าอ้างถึงตรง ๆ” พิสูจน์ข้างบน
terraform planปฏิเสธด้วยเหตุผลที่ระบุชัดเจน — attribute เป้าหมาย “ไม่ใช่ write-only attribute และต้อง persist ลง state” มีแค่_woattribute ที่ provider สร้างไว้ให้ชัดเจนเท่านั้นที่ผ่านได้ - “write-only argument เป็น feature ทั่วไปของ Terraform 1.11 ใช้กับ resource ไหนก็ได้” พิสูจน์ข้างบนด้วยการ dump provider schema จริงสองตัว
google_secret_manager_secret_versionประกาศsecret_data_wo/secret_data_wo_versionจริง (หนึ่งใน resourcegoogle7 ตัวที่มี) ในขณะที่random_passwordบน toolchain เวอร์ชันเดียวกันเป๊ะไม่มี write-only attribute เลย — มันคือ opt-in ต่อ resource ต่อ provider ไม่ใช่ความสามารถที่ทุก resource ได้ฟรีตั้งแต่ 1.11
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน review โค้ดที่ generate มาซึ่งแตะ secret หรือ ephemeral data เช็คสิ่งเหล่านี้
sensitive = trueถูกนำเสนอเป็น “ทางแก้” ของ credential โดยไม่พูดถึงว่า backend ยังเก็บมันแบบ plaintext อยู่ดี — พิสูจน์ข้างบน นี่คือช่องว่างที่บทนี้ปิดพอดี- ephemeral value ถูกอ้างถึงตรง ๆ ใน attribute ธรรมดา (ไม่ใช่
_wo) ของ resource สันนิษฐานว่า “ก็แค่ใช้ได้” เพราะ compile ผ่าน — พิสูจน์ข้างบนterraform planปฏิเสธด้วย error เฉพาะเจาะจง ไม่มีทางไปถึงapplyเลย - root-module
outputที่ตั้งephemeral = trueตั้งใจส่ง secret ที่ generate มาให้สิ่งภายนอก Terraform — พิสูจน์ข้างบน สิ่งนี้ถูกปฏิเสธตรง ๆ ephemeral output มีไว้ให้ child module ส่งค่ากลับขึ้นไปให้ผู้เรียกเท่านั้น nonsensitive()ถูกใช้ห่อค่าที่จริง ๆ เป็น secret แค่เพื่อปิด error “sensitive value in output” แทนที่จะตั้ง output เป็นsensitive = true— สิ่งนี้ทำลายการป้องกันเดียวที่sensitiveมีให้โดยไม่ได้อะไรคืนมาจริง- argument แบบ
secret_data_woบนgoogle_secret_manager_secret_versionถูกใช้โดยไม่เพิ่มsecret_data_wo_versionคู่กัน แล้วมีรายงานว่า “ค่า secret ใหม่ไม่เคย apply เลย” — พิสูจน์ข้างบน เลข version คือ signal เดียวที่ Terraform มีว่าต้องส่งค่า write-only ใหม่ เพราะมันไม่เคยเก็บค่าเองไว้ diff
🐛 หา bug ในโค้ดนี้
ephemeral "random_password" "db" {length = 16}
resource "google_secret_manager_secret_version" "db" {secret = google_secret_manager_secret.db.idsecret_data = ephemeral.random_password.db.result}เฉลย
พิสูจน์ข้างบนด้วย error จริงสำหรับความผิดพลาดแบบเดียวกันบน terraform_data.marker — secret_data บน google_secret_manager_secret_version เป็น attribute ธรรมดาที่ persist ไม่ใช่ write-only Terraform ปฏิเสธ plan นี้ด้วย “Ephemeral values are not valid for …, because it is not a write-only attribute and must be persisted to state” ทางแก้ไม่ใช่ยอมแพ้เรื่อง generate secret แบบ ephemeral แต่คือส่งมันผ่าน write-only pair จริงของ resource แทน secret_data_wo = ephemeral.random_password.db.result คู่กับ secret_data_wo_version ที่เพิ่มขึ้นทุกครั้งที่ควรเปลี่ยนค่า เขียนลง argument secret_data ธรรมดาจะไปไม่ถึง GCP ด้วยซ้ำ — plan เองพังก่อน ซึ่งคือ safety net ที่ทำงานตามที่ออกแบบไว้ ไม่ใช่ bug ที่ต้องหลบด้วย sensitive = true (แบบนั้นจะแค่ redact การแสดงผล ค่า ephemeral ก็ยัง persist ลง attribute ธรรมดาไม่ได้อยู่ดี)