Terraform กับ HashiCorp Vault: จัดการ Secrets อย่างมืออาชีพ

การเก็บข้อมูลลับ (secrets) เช่น รหัสผ่านฐานข้อมูล, API token, private key หรือ certificate เป็นเรื่องที่ต้องระมัดระวังเป็นพิเศษเมื่อทำงานร่วมกับ Infrastructure as Code การ hardcode ค่าเหล่านี้ไว้ในไฟล์โครงสร้างพื้นฐานหรือเก็บใน Git repository ถือเป็นความเสี่ยงที่ใหญ่ที่สุดอย่างหนึ่ง HashiCorp Vault เป็นเครื่องมือจัดการ secrets ที่ถูกออกแบบมาเพื่อแก้ปัญหานี้โดยเฉพาะ

บทความนี้จะอธิบายการเชื่อมต่อ Vault เข้ากับ HCL workflow ตั้งแต่การตั้งค่า provider, วิธี authenticate, การอ่านค่าจาก KV secret engine, การออก dynamic secrets, ไปจนถึงแนวทางปฏิบัติที่ดีเพื่อให้ระบบ IaC ปลอดภัยและสามารถตรวจสอบย้อนหลังได้

HashiCorp Vault คืออะไร

HashiCorp Vault เป็นระบบกลางสำหรับเก็บ, เข้ารหัส และควบคุมการเข้าถึง secrets ทุกชนิด มีคุณสมบัติหลักคือ การเข้ารหัสข้อมูลระดับ AES-256, ระบบ policy ที่ควบคุมว่าใครเข้าถึงอะไรได้, audit log สำหรับตรวจสอบย้อนหลัง และความสามารถสร้าง dynamic secrets ที่มีอายุจำกัด (TTL) ทำให้แต่ละ session ได้ credential ที่แตกต่างกันและจะถูกเพิกถอนอัตโนมัติหลังหมดอายุ

Vault มีสองโหมดหลัก ได้แก่ static secrets ที่ผู้ดูแลระบบบันทึกค่าไว้ล่วงหน้า และ dynamic secrets ที่ Vault จะสร้างและทำลายอัตโนมัติตามคำขอ โหมดหลังเหมาะกับการเชื่อมต่อกับ cloud provider, ฐานข้อมูล หรือระบบที่รองรับการสร้างบัญชีผ่าน API

ติดตั้ง Vault Provider

ก่อนใช้งานต้องประกาศ provider ในบล็อก required_providers และระบุ address ของ Vault server ค่า token หรือวิธี authenticate อื่น ๆ ควรตั้งเป็น environment variable เพื่อไม่ให้หลุดเข้า state หรือ Git repository

terraform {
  required_providers {
    vault = {
      source  = "hashicorp/vault"
      version = "~> 4.0"
    }
  }
}

provider "vault" {
  address = "https://vault.example.com:8200"
  # token รับจาก env var VAULT_TOKEN (อย่า hardcode)
}

ตั้งค่า environment variable ก่อนสั่งรัน:

export VAULT_ADDR="https://vault.example.com:8200"
export VAULT_TOKEN="s.xxxxxxxxxxxxxxxx"
terraform init
terraform plan

วิธี Authenticate กับ Vault

Vault รองรับหลาย auth method ขึ้นอยู่กับสภาพแวดล้อม ในการใช้งานจริงไม่ควรใช้ root token โดยตรงเนื่องจากมีสิทธิ์ทั้งหมดและไม่มีการหมดอายุ ให้เลือก auth method ที่เหมาะกับผู้เรียกใช้งาน

1. Token Authentication

เหมาะสำหรับการทดสอบหรือเครื่องมือที่สร้าง short-lived token ผ่านระบบอื่น ควรเป็น token ที่มี policy จำกัดและมี TTL สั้น

provider "vault" {
  address = "https://vault.example.com:8200"
  token   = var.vault_token
}

2. AppRole Authentication

AppRole ออกแบบมาสำหรับ machine-to-machine โดยใช้ role_id คู่กับ secret_id วิธีนี้เหมาะกับ CI/CD pipeline ที่สามารถเก็บ credential สองส่วนแยกกันเพื่อลดความเสี่ยง

provider "vault" {
  address = "https://vault.example.com:8200"

  auth_login {
    path = "auth/approle/login"

    parameters = {
      role_id   = var.role_id
      secret_id = var.secret_id
    }
  }
}

3. AWS IAM Authentication

เมื่อรัน IaC ภายใน EC2 หรือ EKS สามารถใช้ IAM role authenticate กับ Vault โดยไม่ต้องเก็บ credential ใด ๆ เลย Vault จะตรวจสอบ signature ของ AWS request เพื่อยืนยันตัวตน

provider "vault" {
  address = "https://vault.example.com:8200"

  auth_login_aws {
    role = "iac-runner"
  }
}

อ่าน Static Secrets จาก KV Engine

KV (Key-Value) เป็น secret engine ที่ใช้งานบ่อยที่สุด แบ่งเป็น v1 (simple) และ v2 (versioned) การเขียน configuration จะต่างกันเล็กน้อย ดังตัวอย่างต่อไปนี้

# KV v2 (versioned)
data "vault_kv_secret_v2" "db" {
  mount = "secret"
  name  = "app/database"
}

resource "aws_db_instance" "app" {
  identifier = "app-db"
  engine     = "mysql"
  username   = data.vault_kv_secret_v2.db.data["username"]
  password   = data.vault_kv_secret_v2.db.data["password"]
  # ...
}
# KV v1 (simple)
data "vault_generic_secret" "api" {
  path = "secret/api/keys"
}

output "api_key_reference" {
  value     = data.vault_generic_secret.api.data["key"]
  sensitive = true
}

สังเกตว่าค่าที่อ่านมาจะถูกเก็บใน state ด้วย ดังนั้นต้องเข้ารหัส backend state เสมอ (ดูบทความ Remote State และ Sensitive Data Security)

Dynamic Secrets (Credential ที่สร้างตามคำขอ)

จุดเด่นที่สุดของ Vault คือความสามารถออก credential แบบ ephemeral เช่น database username/password ที่สร้างใหม่ทุกครั้งที่มีการเรียกและถูกเพิกถอนเองเมื่อ TTL หมด ทำให้ไม่ต้องจัดเก็บ credential ระยะยาว

Database Secret Engine

# ฝั่ง admin: กำหนด connection และ role
resource "vault_database_secret_backend_connection" "mysql" {
  backend       = "database"
  name          = "app-mysql"
  allowed_roles = ["readonly", "readwrite"]

  mysql {
    connection_url = "admin:{{password}}@tcp(db.internal:3306)/"
  }
}

resource "vault_database_secret_backend_role" "readonly" {
  backend             = "database"
  name                = "readonly"
  db_name             = vault_database_secret_backend_connection.mysql.name
  creation_statements = [
    "CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; GRANT SELECT ON app.* TO '{{name}}'@'%';"
  ]
  default_ttl = 3600   # 1 ชั่วโมง
  max_ttl     = 86400  # 24 ชั่วโมง
}
# ฝั่ง consumer: ขอ credential ใหม่ทุกครั้งที่รัน plan/apply
data "vault_database_secret_backend_dynamic_credentials" "app_readonly" {
  backend = "database"
  role    = "readonly"
}

# ใช้ในแอป
output "db_user" {
  value     = data.vault_database_secret_backend_dynamic_credentials.app_readonly.username
  sensitive = true
}

AWS Secret Engine

ใช้ออก IAM access key ชั่วคราวสำหรับการ deploy โดยไม่ต้องเก็บ long-lived access key ในเครื่องนักพัฒนาเลย

resource "vault_aws_secret_backend_role" "deployer" {
  backend         = "aws"
  name            = "deployer"
  credential_type = "iam_user"
  policy_arns     = ["arn:aws:iam::aws:policy/PowerUserAccess"]
  default_sts_ttl = 3600
}

data "vault_aws_access_credentials" "deploy" {
  backend = "aws"
  role    = "deployer"
  type    = "creds"
}

TTL, Lease และการเพิกถอน

ทุก credential ที่ Vault ออกให้จะมี lease_id ผูกกับ TTL เมื่อหมดอายุ Vault จะเพิกถอนเองอัตโนมัติ ถ้าต้องการเพิกถอนก่อนเวลาสามารถใช้คำสั่ง CLI:

# ดู lease ปัจจุบัน
vault list sys/leases/lookup/database/creds/readonly

# เพิกถอน lease ทันที
vault lease revoke database/creds/readonly/abc123

# เพิกถอนทั้ง prefix
vault lease revoke -prefix database/

ใน IaC ไม่ต้องจัดการ lease เอง เพราะเมื่อรัน destroy หรือเมื่อ data source ถูกใช้งานในรอบถัดไป ระบบจะขอ credential ชุดใหม่โดยอัตโนมัติ

Policy และ ACL

Vault ใช้ policy ภาษา HCL เพื่อกำหนดว่า token หรือ role หนึ่ง ๆ เข้าถึง path ใดได้บ้าง policy ควรออกแบบตามหลัก least privilege คือให้สิทธิ์น้อยที่สุดเท่าที่จำเป็น

resource "vault_policy" "app_readonly" {
  name = "app-readonly"

  policy = <<-EOT
    path "secret/data/app/*" {
      capabilities = ["read"]
    }

    path "database/creds/readonly" {
      capabilities = ["read"]
    }
  EOT
}

capabilities หลักประกอบด้วย create, read, update, delete, list, sudo การใช้ sudo ควรหลีกเลี่ยงในงานทั่วไป เพราะให้สิทธิ์ bypass root protection

ผสาน Vault เข้ากับ Modules

แนวทางที่ดีคือให้ module ไม่รู้จัก Vault โดยตรง แต่รับค่าผ่าน input variable จาก root module ที่เรียก data source ของ Vault เอง วิธีนี้ทำให้ module นำไปใช้ซ้ำได้แม้โครงการที่ไม่ได้ใช้ Vault

# root.tf
data "vault_kv_secret_v2" "db" {
  mount = "secret"
  name  = "app/database"
}

module "app" {
  source       = "./modules/app"
  db_username  = data.vault_kv_secret_v2.db.data["username"]
  db_password  = data.vault_kv_secret_v2.db.data["password"]
}
# modules/app/variables.tf
variable "db_username" {
  type = string
}

variable "db_password" {
  type      = string
  sensitive = true
}

Audit Log และการตรวจสอบ

Vault สามารถเปิด audit device เพื่อบันทึกทุกการเรียก API ลงไฟล์, syslog หรือ socket ทำให้ตรวจได้ว่าใครอ่าน secret ใดเมื่อใด ซึ่งเป็นข้อกำหนดสำคัญของการ audit ความปลอดภัยและ compliance เช่น SOC 2, PCI-DSS, ISO 27001

resource "vault_audit" "file" {
  type = "file"

  options = {
    file_path = "/var/log/vault/audit.log"
  }
}

Best Practices ของ Vault + IaC

  • ห้าม hardcode VAULT_TOKEN ในไฟล์ .tf หรือ tfvars ใช้ environment variable เสมอ
  • เปิด audit device ตั้งแต่ production day-1 เพื่อให้มี log ครบถ้วน
  • ตั้ง TTL สั้นที่สุดเท่าที่ workload ยอมรับได้ เพื่อลดผลกระทบเมื่อ credential หลุด
  • แยก policy ตาม workload: policy ของ app, ของ CI/CD, ของ admin ต้องแยกกันชัดเจน
  • ใช้ dynamic secrets สำหรับฐานข้อมูลและ cloud provider ถ้าเทคโนโลยีรองรับ
  • เปิด state encryption ทุก backend เพราะค่าจาก Vault จะถูก cache ลง state
  • หมุน root token สม่ำเสมอและเก็บใน offline vault เช่น HSM หรือ offline backup
  • เชื่อม Vault กับ identity provider (OIDC, LDAP) เพื่อให้ผู้ใช้ login ด้วยบัญชีขององค์กร

ตัวอย่าง Workflow ครบวงจรใน CI/CD

workflow ทั่วไปสำหรับ pipeline ที่ใช้ Vault มีลำดับดังนี้:

  • Runner authenticate กับ Vault ด้วย AppRole หรือ cloud IAM
  • Vault ออก short-lived token พร้อม policy จำกัด (เช่น อ่านเฉพาะ path ของโปรเจกต์)
  • Runner รัน plan/apply โดย provider ดึง secret จาก Vault ตาม data source
  • เมื่อ pipeline จบ token จะหมดอายุเอง และ dynamic credential ทั้งหมดจะถูก revoke
  • Audit log บันทึกทุก event พร้อม request_id เพื่อใช้ตรวจย้อนหลัง

ข้อควรระวัง

  • ค่าที่อ่านจาก Vault data source ยังคงปรากฏใน state ดังนั้น state backend ต้องมี encryption at rest เสมอ
  • เมื่อ Vault ล่ม workflow จะหยุดทันที ควรมี high-availability cluster และทดสอบ recovery
  • การใช้ token root ใน CI/CD เป็นข้อผิดพลาดที่พบบ่อย ต้องเปลี่ยนมาใช้ AppRole หรือ cloud IAM
  • Dynamic credential ที่อายุสั้นเกินไปอาจทำให้ plan ล้มเหลวเพราะหมดอายุก่อน apply เสร็จ ต้องทดสอบ TTL ให้เหมาะ

สรุป

การใช้ HashiCorp Vault ร่วมกับ Infrastructure as Code ทำให้การจัดการ secrets เป็นระบบ มี audit log ครบถ้วน และลดความเสี่ยงจาก long-lived credential อย่างมาก ผู้ดูแลระบบสามารถเริ่มจากการตั้งค่า provider, ใช้ AppRole หรือ cloud IAM สำหรับ authenticate, อ่าน static secrets จาก KV engine, และค่อย ๆ เปลี่ยนมาใช้ dynamic secrets สำหรับฐานข้อมูลและ cloud credentials

ควรมองว่า Vault คือโครงสร้างพื้นฐานสำคัญระดับเดียวกับระบบ state และ CI/CD เพราะเมื่อระบบนี้ล่มหรือถูกโจมตี ผลกระทบจะครอบคลุมทุก workload ที่พึ่งพา secret จากที่เดียวกัน การออกแบบ HA cluster, กำหนด policy ที่เข้มงวด, เปิด audit log และทดสอบ backup/restore อย่างสม่ำเสมอจึงเป็นเรื่องที่ต้องทำตั้งแต่ day-1 ไม่ใช่รอจนเกิดปัญหา