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

Secrets and Credential Management

Azure credential และ application secret ไม่ควร hardcode ลงใน .tf file หรือ .tfvars file ที่ commit เข้า git เลย provider authentication ควรมาจาก Azure CLI session local, managed identity, หรือ OIDC ส่วน application secret ควร pull จาก secrets manager จริงตอน apply แทนการพิมพ์ลงใน Terraform variable

provider "azurerm" block รับ argument client_id, client_secret, tenant_id, และ subscription_id ได้ตรง ๆ และถูกกฎในแง่ HCL syntax ถ้าจะพิมพ์ client secret จริงลงไปในนั้น แต่นี่คือ mistake เดียวกับที่ course นี้เตือนซ้ำ ๆ มาตลอด ค่าที่อยู่ใน .tf file คือค่าที่ห่างจาก git add . โดยไม่ตั้งใจแค่ครั้งเดียวจากการไปอยู่ใน public GitHub history ตลอดไป ไม่ว่าใครจะ review pull request หลังจากนั้นละเอียดแค่ไหนก็ตาม

# do not do this — a real client secret should never appear in a .tf file
provider "azurerm" {
features {}
client_id = "11111111-2222-3333-4444-555555555555"
client_secret = "this-is-a-real-secret-and-now-it-is-in-git-forever"
tenant_id = "66666666-7777-8888-9999-000000000000"
subscription_id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
}

วิธี authenticate azurerm provider ที่ถูกต้องทุกแบบ หลีกเลี่ยงการพิมพ์ secret ลงใน config เลยทั้งหมด

  • local development — engineer รัน az login ครั้งเดียว แล้ว azurerm provider จะดึง Azure CLI session นั้นมาใช้เองอัตโนมัติ ไม่ต้องมีอะไรใน provider block เกินกว่า features {}
  • managed identity — system- หรือ user-assigned managed identity ที่ attach ไว้กับ compute ที่รัน Terraform (Azure VM, Azure Container Instance, AKS pod) ให้ provider authenticate เป็น identity นั้นได้โดยไม่มี credential material อยู่ที่ไหนเลย เพราะ Azure เองเป็นคน broker token ให้
  • OIDC ผ่าน federated credential — ที่พูดถึงใน lesson CI/CD แล้ว azure/login แลก identity token ของ GitHub Actions แบบ short-lived ที่ sign มาแล้ว เป็น Azure credential ชั่วคราวผ่าน federated credential บน Azure AD App Registration
# provider block for all three approaches — identical HCL, different actual auth mechanism
provider "azurerm" {
features {}
}

สังเกตว่า provider block เองแทบไม่เปลี่ยนเลยระหว่าง local development, managed identity, และ CI ความต่างอยู่ที่ identity ไหนมีอยู่ใน environment ที่ Terraform รันอยู่ ล้วน ๆ ไม่ใช่ที่ argument client_secret ที่อยู่ในไฟล์

application secret เป็นปัญหาคนละแบบกับ provider authentication เช่น database administrator password, API key ที่ resource ต้องใช้ตอนสร้าง, private key ของ TLS certificate module State Management ได้พูดไปแล้วว่าการตั้ง sensitive = true บน Terraform variable แค่ redact ค่านั้นจาก CLI output เท่านั้น ค่ายังไปอยู่ใน terraform.tfstate แบบ plaintext เต็ม ๆ อยู่ดี เมื่อเป็นแบบนั้น การ define secret เป็น variable เลยแปลว่ายังต้องมีคนหาทางเอาค่าจริงเข้า Terraform ให้ได้อยู่ดี ปกติก็คือ .tfvars file ที่เป็นไฟล์ประเภทที่ commit เข้าไปโดยไม่ตั้งใจได้ง่ายที่สุด

pattern ที่ดีกว่าคือ pull secret เข้ามาจาก secrets manager จริงผ่าน data source ตอน apply ทำให้ค่าจริงของ secret ไม่ต้องถูกพิมพ์ลงใน .tf หรือ .tfvars file เลยตั้งแต่แรก

data "azurerm_key_vault" "main" {
name = "app-keyvault"
resource_group_name = "app-shared-rg"
}
data "azurerm_key_vault_secret" "db_password" {
name = "app-db-password"
key_vault_id = data.azurerm_key_vault.main.id
}
resource "azurerm_postgresql_flexible_server" "main" {
name = "app-db"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
version = "15"
sku_name = "GP_Standard_D2s_v3"
administrator_login = "appadmin"
administrator_password = data.azurerm_key_vault_secret.db_password.value
}

ต้องชัดเจนว่าวิธีนี้แก้อะไรได้และแก้อะไรไม่ได้ นี่ไม่ใช่วิธีกัน secret ออกจาก state ได้ทั้งหมด ผลของ data source ก็ถูก cache ไว้ใน state เหมือน attribute ของ resource เป๊ะ ๆ ดังนั้น data.azurerm_key_vault_secret.db_password.value กับ administrator_password ที่ป้อนให้ ก็จบลงใน terraform.tfstate แบบ plain เหมือน variable ที่ตั้ง sensitive = true อยู่ดี การป้องกัน state backend ด้วย Azure RBAC ตามที่ module State Management พูดไว้ ยังคงเป็นส่วนที่ non-negotiable ไม่ว่าจะเลือกวิธีไหน

สิ่งที่ data source แก้ได้จริง ๆ คือ secret ค่าจริงมาจากไหนและ rotate ยังไง ถ้าใช้ variable ค่าต้องมีคนพิมพ์ไว้ที่ไหนสักที่ก่อนที่ Terraform จะรันเสมอ ไม่ว่าจะเป็น .tfvars file, CI secret, หรือค่าที่ paste เข้า prompt ถ้าใช้ data source อ่านจาก Key Vault ค่าจะถูกตั้งและ rotate อยู่ที่เดียวเท่านั้นคือใน Key Vault เอง แล้วทุก terraform plan หรือ apply ก็ดึงค่าปัจจุบันมาใช้เองอัตโนมัติ ไม่มีใครต้องแตะ Terraform configuration เพื่อ rotate เลย

ไม่ว่าจะ handle secret ใน config ได้ระมัดระวังแค่ไหน Terraform หรือ Terragrunt repository ก็มีไฟล์อยู่หลายประเภทที่ไม่ควรเข้า git ตั้งแต่แรกเลย เพราะแต่ละอย่างอาจมี resource attribute value, configuration ที่ดูเหมือน sensitive, หรือ provider credential ใน local filesystem ได้

Terminal window
# .gitignore — Terraform / Terragrunt
# state files can contain plaintext secrets and full resource attributes
*.tfstate
*.tfstate.backup
# provider binaries and working files downloaded by terraform init
.terraform/
# variable files often carry real values for local runs — audit case by case,
# a checked-in *.tfvars with only non-sensitive defaults can be fine
*.tfvars
# Terragrunt's per-unit cache of generated and downloaded Terraform files
.terragrunt-cache/

*.tfstate กับ *.tfstate.backup สำคัญด้วยเหตุผลเดียวกับที่ module State Management พูดวนกลับมาตลอด state คือ record แบบ plaintext ของทุก attribute ที่ Terraform เคยตั้งค่า ไม่ว่าจะ sensitive หรือไม่ .terraform/ เป็น output ที่ derive มาล้วน ๆ จาก init มีทั้ง provider, module, และ lock metadata ไม่มีอะไรควร commit และทุกอย่าง regenerate ใหม่ได้ใน CI *.tfvars เป็นเรื่องที่ต้องใช้วิจารณญาณมากกว่าจะ block อัตโนมัติ dev.tfvars ที่มีแค่ instance size กับชื่อ region ไม่มีปัญหาถ้า commit แต่ .tfvars ตัวไหนก็ตามที่มี credential จริงหรือ connection string ต้องไม่เข้า git เด็ดขาด ทีมส่วนใหญ่เลย exclude pattern นี้ไปเลยตรง ๆ หรือไม่ก็ใช้ naming convention อย่าง *.auto.tfvars สำหรับไฟล์ที่ปลอดภัย แล้วแยกชัดเจนสำหรับไฟล์ที่มีอะไร sensitive

flowchart LR
  hardcoded["Hardcoded client_secret in .tf file"] -->|committed| git["git history forever"]
  kv["Azure Key Vault secret"] -->|data source at apply time| tf["Terraform resource attribute"]
  tf -->|recorded| state["terraform.tfstate"]
  kv -->|rotated in one place| tf
Azure Key Vault เป็นแหล่งความจริงของ credential อ่านเข้า Terraform ผ่าน data source ตอน apply เทียบกับ credential ที่ hardcode ไว้ในไฟล์ที่ commit แล้ว
best practice ปัจจุบันสำหรับ authenticate azurerm provider ใน CI pipeline ที่รันบน Azure คืออะไร
ทำไม pull secret เข้า Terraform ผ่าน data source จาก Azure Key Vault โดยทั่วไปดีกว่า define เป็น Terraform variable
อะไรต่อไปนี้ควร exclude จาก gitignore ของ Terraform หรือ Terragrunt repository เสมอ
provider "azurerm" block มีแค่ features {} ไม่มี argument client_id หรือ client_secret เลย นี่แปลว่าอะไร