Secrets: sensitive, ephemeral, và Write-Only Arguments

K
Kai··6 min read

Suốt Part II ta nhắc đi nhắc lại một câu: state lưu mọi thuộc tính ở dạng plaintext, kể cả secret. Bài 7 còn chứng kiến mật khẩu nằm thẳng trong file. Bài này xử lý nó. Có ba mức công cụ, và điểm dễ hiểu sai là mức đầu tiên, sensitive, vốn không giải quyết vấn đề state dù nhiều người tưởng vậy. Hai mức sau, ephemeral resources và write-only arguments, mới thực sự giữ secret khỏi state, và chúng là tính năng mới của các bản Terraform gần đây.

Mục tiêu

Phân biệt rõ ba cơ chế: sensitive làm gì và không làm gì, ephemeral resources và write-only arguments giải quyết phần sensitive còn thiếu ra sao. Chứng minh bằng cách grep thẳng file state.

sensitive: chỉ che màn hình

Đánh dấu một biến hay output là sensitive để Terraform không in giá trị ra terminal:

variable "db_password" {
  type      = string
  sensitive = true
  default   = "super-secret-123"
}

output "password_echo" {
  value     = var.db_password
  sensitive = true
}

Apply, và output bị che:

$ terraform apply -auto-approve
  + password_echo = (sensitive value)
  + conn_string   = (sensitive value)
...
Outputs:

conn_string   = <sensitive>
password_echo = <sensitive>

Một chi tiết hay: sensitivity lan truyền. Nếu bạn lấy length(var.db_password) hay nhúng mật khẩu vào một chuỗi kết nối, kết quả cũng bị coi là sensitive. Terraform thậm chí báo lỗi nếu một output chứa giá trị sensitive mà không được đánh dấu sensitive, để bạn không vô tình lộ:

Error: Output refers to sensitive values
...
Terraform requires that any root module output containing sensitive data
be explicitly marked as sensitive, to confirm your intent.

Muốn xem giá trị thật phải hỏi tường minh, và đó là cố ý:

$ terraform output -raw password_echo
super-secret-123

Đến đây sensitive trông ổn. Nhưng kiểm tra file state:

$ grep 'super-secret-123' terraform.tfstate
super-secret-123

Mật khẩu nằm thẳng trong state, không mã hóa gì. Tài liệu nói rõ giới hạn này: sensitive chỉ "ngăn Terraform hiển thị giá trị trong CLI output", còn "Terraform vẫn lưu giá trị của biến sensitive vào state của bạn". Nói cách khác, sensitive chống nhìn qua vai, không chống đọc file state. Ai lấy được state vẫn lấy được secret. Đây là lý do bài 6 phải mã hóa bucket state — nhưng tốt hơn là đừng để secret vào state ngay từ đầu.

ephemeral: giá trị chỉ sống trong một lần chạy

Terraform 1.10 đưa ra khái niệm ephemeral (phù du). Tài liệu định nghĩa: ephemeral resource là "resource về bản chất là tạm thời", và "Terraform không lưu thông tin về ephemeral resource trong state hay plan". Chúng tồn tại đúng trong một lần thao tác rồi biến mất, hợp với những thứ như "mật khẩu tạm" hay "kết nối tới hệ thống khác".

Cú pháp là block ephemeral thay cho resource:

ephemeral "aws_secretsmanager_secret_version" "db" {
  secret_id = "my-db-secret"
}

Khác biệt cốt lõi so với resource: resource thường được Terraform ghi vào state; ephemeral thì không ghi ở đâu cả. Bạn dùng nó để đọc một secret từ nơi cất an toàn (Secrets Manager, Vault) ngay lúc apply, dùng giá trị đó cấu hình resource khác, mà bản thân secret không bao giờ chạm vào state. Giá trị từ ephemeral chỉ truyền được vào những chỗ chấp nhận giá trị ephemeral, mà chỗ tiêu biểu là write-only argument.

write-only arguments: gửi cho provider rồi quên

Terraform 1.11 bổ sung write-only arguments. Ý tưởng, theo tài liệu: "provider dùng giá trị write-only để cấu hình resource, rồi Terraform vứt giá trị đi mà không lưu". Secret được gửi tới provider trong lúc apply, provider áp nó lên hạ tầng, xong thì giá trị tan biến, không lọt vào state hay plan.

Quy ước đặt tên: argument write-only có hậu tố _wo, đi kèm một argument _wo_version. Bản thân giá trị _wo không vào state; chỉ con số _wo_version được lưu, để bạn điều khiển khi nào cập nhật: tăng version lên thì provider mới ghi lại secret mới. Provider AWS hỗ trợ ở nhiều resource, ví dụ aws_db_instancepassword_wo + password_wo_version, và aws_secretsmanager_secret_versionsecret_string_wo + secret_string_wo_version (cần Terraform 1.11+).

Chứng minh: cùng mật khẩu, hai số phận

Dựng hai secret cạnh nhau — một dùng secret_string kiểu cũ, một dùng secret_string_wo write-only — với cùng một giá trị, rồi xem state:

variable "secret_value" {
  type      = string
  sensitive = true
  default   = "p@ssw0rd-bai8-demo"
}

# CŨ: giá trị sẽ vào state
resource "aws_secretsmanager_secret_version" "legacy" {
  secret_id     = aws_secretsmanager_secret.legacy.id
  secret_string = var.secret_value
}

# MỚI: write-only, giá trị KHÔNG vào state
resource "aws_secretsmanager_secret_version" "wo" {
  secret_id                = aws_secretsmanager_secret.wo.id
  secret_string_wo         = var.secret_value
  secret_string_wo_version = 1
}

Apply rồi đếm xem mật khẩu xuất hiện mấy lần trong state:

$ terraform apply -auto-approve
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

$ grep -o 'p@ssw0rd-bai8-demo' terraform.tfstate | wc -l
1

Đúng một lần, dù có hai secret cùng giá trị. Soi kỹ từng resource version:

legacy -> secret_string: p@ssw0rd-bai8-demo | secret_string_wo: None
wo     -> secret_string:                     | secret_string_wo: None

Bản legacy lưu mật khẩu vào trường secret_string. Bản wo thì cả secret_string lẫn secret_string_wo đều rỗng/null trong state — giá trị không được ghi lại. Lần grep ra "1" chính là từ bản legacy.

Câu hỏi tự nhiên: vậy mật khẩu của bản write-only có thực sự tới AWS không, hay bị mất luôn? Kiểm tra thẳng trên AWS:

$ aws secretsmanager get-secret-value --secret-id tf-series-bai8-wo --query SecretString --output text
p@ssw0rd-bai8-demo

Giá trị nằm đúng trên AWS. Nó đã được gửi tới provider trong lúc apply và ghi vào Secrets Manager — chỉ là không được Terraform lưu lại trong state. Đây chính xác là điều write-only hứa: secret đi qua Terraform tới đích, nhưng không để lại dấu vết trong file state. (ARN trong output thật có chứa mã tài khoản, ở đây đã thay bằng 111122223333.)

💰 Chi phí

Mỗi secret trong Secrets Manager tính khoảng 0,40 USD mỗi tháng, tính theo tỉ lệ thời gian tồn tại. Bài này tạo hai secret rồi destroy ngay, nên chi phí thực tế chỉ vài cent. recovery_window_in_days = 0 cho phép xóa ngay thay vì chờ cửa sổ khôi phục mặc định.

Nguồn secret nên đến từ đâu

Một điểm thực hành: ngay cả khi dùng write-only, đừng hardcode secret trong file .tf như demo (chỉ để minh họa). Giá trị nên đến từ ngoài: biến môi trường TF_VAR_secret_value, hoặc tốt hơn là một ephemeral resource đọc thẳng từ Secrets Manager / Vault lúc apply rồi đẩy vào argument _wo. Kết hợp ephemeral (đọc không lưu) với write-only (ghi không lưu) cho ta đường đi trọn vẹn của secret mà state sạch từ đầu tới cuối.

🧹 Dọn dẹp

$ terraform destroy -auto-approve
Destroy complete! Resources: 4 destroyed.

Tổng kết

sensitive chỉ che giá trị khỏi output, vẫn lưu plaintext vào state — nó chống nhìn qua vai chứ không chống đọc state. Ephemeral resources (1.10) đọc giá trị mà không ghi vào state hay plan; write-only arguments (1.11, hậu tố _wo + _wo_version) gửi secret tới provider rồi vứt đi. Demo cho thấy cùng một mật khẩu, bản secret_string lọt vào state còn bản secret_string_wo thì không, dù giá trị vẫn tới đúng AWS. Đây là cách hiện hành để giữ state sạch secret — thay cho lối cũ chỉ trông cậy vào mã hóa state.

Part II khép lại ở đây: state đã ở nơi an toàn, khóa được, thao tác được, và không còn rò secret. Part III chuyển sang làm cho cấu hình linh hoạt. Bài 9 mở màn với variable, output và locals — cách tham số hóa cấu hình để cùng một code chạy được cho nhiều môi trường, kèm validation và precondition/postcondition để bắt lỗi sớm.