Terraform Security trên GCP
Tại sao Security trong IaC khác Security trong Application
IaC security có một điểm đặc biệt: Terraform process cần elevated permissions để provision resources, nhưng elevated permissions trong CI/CD pipeline là attack surface lớn nhất.
Nếu attacker compromise được CI/CD pipeline đang chạy Terraform với roles/owner — họ có thể tạo service account keys, export secrets, thay đổi IAM policies, hoặc exfiltrate data. Đây là lý do tại sao service account design và credential management cho Terraform là critical, không phải best-effort.
Bên cạnh đó, Terraform state file chứa plaintext sensitive data — database passwords, private keys, secret values — và cần được bảo vệ tương đương với database production.
Service Account cho Terraform — Ba Mô Hình
Mô Hình 1: JSON Key File (Anti-Pattern)
# Tạo key và lưu vào file
gcloud iam service-accounts keys create key.json \
--iam-account=terraform@my-project.iam.gserviceaccount.com
# Set env var cho Terraform
export GOOGLE_APPLICATION_CREDENTIALS=key.json
terraform applyĐây là cách phổ biến nhất và là anti-pattern nguy hiểm nhất:
- Key file có thể bị commit vào git (
.gitignoremiss, rebase accident) - Key file tồn tại trong filesystem — nếu machine bị compromise, key bị lộ
- Key không expire tự động — có thể tồn tại nhiều năm sau khi không cần
- Rotation khó và disruptive
GCP cho phép tối đa 10 keys per service account. Nhiều team có thể có hàng chục keys "orphaned" từ các CI/CD jobs cũ.
Mô Hình 2: Service Account Impersonation (Tốt hơn)
Thay vì cho Terraform process có quyền trực tiếp, dùng impersonation:
- Terraform process authenticate bằng identity của nó (user, Cloud Build SA, Workload Identity)
- Identity này có
roles/iam.serviceAccountTokenCreatortrên Terraform SA - Terraform impersonate Terraform SA để lấy short-lived access token
- Apply được thực hiện với quyền của Terraform SA
# backend.tf
terraform {
backend "gcs" {
bucket = "my-tf-state"
prefix = "prod/networking"
impersonate_service_account = "terraform@my-project.iam.gserviceaccount.com"
}
}
# providers.tf
provider "google" {
impersonate_service_account = "terraform@my-project.iam.gserviceaccount.com"
}Lợi thế:
- Không có long-lived key
- Impersonation được log trong Cloud Audit Logs: ai impersonate, khi nào, làm gì
- Có thể revoke quyền impersonation ngay lập tức (remove
iam.serviceAccountTokenCreator) - Token từ impersonation expire sau 1 giờ (không thể extend)
# IAM: cho phép Cloud Build SA impersonate Terraform SA
resource "google_service_account_iam_member" "cloud_build_impersonation" {
service_account_id = google_service_account.terraform.name
role = "roles/iam.serviceAccountTokenCreator"
member = "serviceAccount:${var.project_number}@cloudbuild.gserviceaccount.com"
}Mô Hình 3: Workload Identity cho Cloud Build (Best)
Khi dùng Cloud Build, không cần bất kỳ credential nào. Cloud Build worker chạy trên GCP infrastructure và có thể dùng Workload Identity Federation để authenticate mà không cần key:
Cloud Build SA (PROJECT_NUMBER@cloudbuild.gserviceaccount.com) đã được Google tự động tạo và gắn với Cloud Build VMs. Sa này có thể được grant quyền trực tiếp hoặc quyền impersonation.
# cloudbuild.yaml
steps:
- name: 'hashicorp/terraform:latest'
entrypoint: 'sh'
args:
- '-c'
- |
terraform init
terraform apply -auto-approve
env:
- 'GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=terraform@my-project.iam.gserviceaccount.com'Bên trong Cloud Build step, Cloud Build SA authenticate tự động qua metadata server (169.254.169.254). Terraform SDK nhận token từ metadata server, sau đó impersonate Terraform SA.
Không có key file nào. Không có secret nào. Credentials là ephemeral và tied to the build.
Least Privilege Design cho Terraform SA
Terraform SA không bao giờ nên có roles/editor hay roles/owner. Permissions phải minimal và specific theo những gì Terraform cần quản lý.
Phân Chia Terraform SA theo Layer
terraform-bootstrap@my-org-project.iam.gserviceaccount.com
→ roles/resourcemanager.organizationAdmin (giới hạn)
→ roles/billing.admin
→ roles/resourcemanager.folderAdmin
terraform-networking@shared-infra.iam.gserviceaccount.com
→ roles/compute.networkAdmin
→ roles/dns.admin
→ roles/compute.securityAdmin
terraform-gke@my-project.iam.gserviceaccount.com
→ roles/container.admin
→ roles/compute.viewer (đọc network info)
→ roles/iam.serviceAccountAdmin (tạo node SA)
terraform-apps@my-project.iam.gserviceaccount.com
→ roles/run.admin
→ roles/cloudfunctions.developer
→ roles/secretmanager.secretAccessorMỗi SA chỉ có quyền cho layer của nó. Compromise một SA không compromise toàn bộ infrastructure.
Quyền Tối Thiểu cho Một Số Tác Vụ Phổ Biến
Tạo GKE cluster:
resource "google_project_iam_member" "gke_admin" {
project = var.project_id
role = "roles/container.admin"
member = "serviceAccount:${google_service_account.terraform_gke.email}"
}
# GKE cần tạo service accounts cho node pools
resource "google_project_iam_member" "sa_admin" {
project = var.project_id
role = "roles/iam.serviceAccountAdmin"
member = "serviceAccount:${google_service_account.terraform_gke.email}"
}
# Để attach roles cho node SA
resource "google_project_iam_member" "role_viewer" {
project = var.project_id
role = "roles/iam.roleViewer"
member = "serviceAccount:${google_service_account.terraform_gke.email}"
}Quản lý IAM bindings (cẩn thận):
Terraform SA cần roles/resourcemanager.projectIamAdmin để set IAM bindings. Đây là quyền nhạy cảm vì cho phép grant bất kỳ role nào cho bất kỳ principal nào trong project. Nên giới hạn bằng IAM Conditions:
resource "google_project_iam_member" "iam_admin" {
project = var.project_id
role = "roles/resourcemanager.projectIamAdmin"
member = "serviceAccount:${google_service_account.terraform.email}"
condition {
title = "only_specific_roles"
description = "Chỉ cho phép grant specific roles"
expression = <<-EOT
request.resource.name.startsWith("projects/${var.project_id}") &&
"roles/container.developer" in resource.bindings.role
EOT
# Chú ý: IAM Conditions trong project IAM binding có giới hạn
# Nên dùng IAM Deny Policies thay thế cho complex restrictions
}
}Secret Management trong Terraform
Vấn đề: State File chứa Plaintext Secrets
Khi Terraform tạo resources có sensitive values, chúng được lưu trong state:
# Terraform tạo database password → lưu plaintext trong state
resource "random_password" "db" {
length = 32
}
resource "google_sql_database_instance" "main" {
name = "prod-db"
settings {
tier = "db-n1-standard-2"
}
}
resource "google_sql_user" "main" {
name = "app"
instance = google_sql_database_instance.main.name
password = random_password.db.result # Lưu plaintext trong state!
}State file sẽ chứa:
{
"resources": [{
"type": "random_password",
"instances": [{
"attributes": {
"result": "abc123xyz789..." // plaintext!
}
}]
}]
}Mitigations:
CMEK cho state bucket: Encrypt state với Cloud KMS key. Revoke key = state inaccessible. Không giải quyết hoàn toàn nhưng giảm blast radius.
Không tạo service account keys bằng Terraform:
google_service_account_keytạo JSON key và lưu vào state. Dùng Workload Identity thay thế.Dùng Secret Manager: Thay vì generate secrets trong Terraform, lưu secrets tay vào Secret Manager và reference:
# Đọc secret từ Secret Manager (không lưu vào state)
data "google_secret_manager_secret_version" "db_password" {
secret = "prod-db-password"
project = var.project_id
}
resource "google_sql_user" "main" {
name = "app"
instance = google_sql_database_instance.main.name
password = data.google_secret_manager_secret_version.db_password.secret_data
# Chú ý: secret_data vẫn có trong state dạng sensitive value
}- Sensitive outputs: Đánh dấu outputs nhạy cảm để Terraform ẩn trong plan output:
output "db_connection_string" {
value = "postgres://app:${random_password.db.result}@${google_sql_database_instance.main.private_ip_address}/mydb"
sensitive = true # Hiển thị "(sensitive value)" trong plan/apply output
}Lưu ý: sensitive = true chỉ ẩn trong terminal output. Value vẫn trong state file plaintext. State encryption là bắt buộc nếu có sensitive values.
gcloud terraform vet — Policy Validation Pre-Apply
gcloud terraform vet tích hợp Terraform với GCP org policies. Nó chạy Terraform plan và validate kết quả chống lại org policy constraints — trước khi apply.
# Generate plan
terraform plan -out=terraform.plan
# Convert sang JSON format
terraform show -json terraform.plan > terraform.plan.json
# Run policy validation
gcloud terraform vet terraform.plan.json \
--policy-library=./policy-library \
--project=my-projectpolicy-library là thư mục chứa constraint templates (GCP Policy Library format). Ví dụ constraint:
# policy-library/policies/constraints/gke-no-public-endpoint.yaml
apiVersion: constraints.gatekeeper.sh/v1alpha1
kind: GKEPrivateCluster
metadata:
name: gke-no-public-endpoint
spec:
severity: HIGH
match:
target:
- "organizations/**"
parameters: {}Khi constraint vi phạm, terraform vet exit với error code non-zero, CI pipeline fail trước khi apply.
GCP cũng cung cấp pre-built policy bundles cho HIPAA, PCI-DSS, CIS Benchmark. Đây là way để enforce security guardrails tại apply time, không chỉ detect sau khi deploy.
Drift Detection
Drift xảy ra khi ai đó thay đổi GCP resources ngoài Terraform — manual console change, emergency hotfix, một automation script khác.
Refresh-Only Plan
# Detect drift: refresh state từ actual resources, xem có gì thay đổi
terraform plan -refresh-only
# Output:
# ~ google_container_cluster.main
# ~ node_config {
# ~ machine_type = "n2-standard-2" -> "n2-standard-4" (change ngoài Terraform)
# }-refresh-only không thực hiện bất kỳ thay đổi nào. Nó chỉ cập nhật state để reflect actual resources.
Sau khi review:
- Nếu thay đổi hợp lệ →
terraform apply -refresh-onlyđể sync state - Nếu thay đổi không mong muốn →
terraform applyđể revert về desired state
Scheduled Drift Detection trong Cloud Build
# cloudbuild.yaml (triggered mỗi giờ bằng Cloud Scheduler)
steps:
- name: 'hashicorp/terraform:latest'
entrypoint: 'sh'
args:
- '-c'
- |
terraform init
# -detailed-exitcode: exit 2 nếu có changes, exit 0 nếu không có
terraform plan -refresh-only -detailed-exitcode || echo "DRIFT DETECTED"
dir: 'environments/prod/networking'
env:
- 'TF_WORKSPACE=default'Kết hợp với alerting: nếu build exit non-zero, gửi alert lên PagerDuty/Slack.
Cloud Asset Inventory cho Drift Detection
Cloud Asset Inventory (CAI) có thể detect thay đổi resources không qua Terraform:
# List tất cả resources trong project tại một thời điểm
gcloud asset search-all-resources \
--scope=projects/my-project \
--asset-types=container.googleapis.com/Cluster
# Lưu snapshot và compare theo thời gian
gcloud asset export \
--project=my-project \
--output-path=gs://my-bucket/asset-snapshot.json \
--content-type=resourceTerraform google_cloud_asset_inventory_feed resource có thể tạo real-time feed cho CAI changes → push sang Pub/Sub → trigger Cloud Function để alert.
Separation of Duties trong IaC Pipelines
Một pipeline duy nhất có quyền plan và apply là anti-pattern. Nên tách:
Developer → PR → Plan pipeline (read-only, post plan output)
↓
Human review plan output
↓
Approve PR → Apply pipeline (write access, chạy apply)Cách implement:
# plan-cloudbuild.yaml (chạy trên mọi PR)
steps:
- name: 'hashicorp/terraform:latest'
args: ['plan', '-out=terraform.plan']
- name: 'gcr.io/cloud-builders/gcloud'
args: ['builds', 'submit', '--no-source',
'--config=comment-plan.yaml',
'--substitutions=_PR_NUMBER=${_PR_NUMBER}']
# Post plan output as PR comment
# apply-cloudbuild.yaml (chỉ chạy khi merge vào main)
steps:
- name: 'hashicorp/terraform:latest'
args: ['apply', '-auto-approve']
env:
- 'GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=terraform-apply@project.iam.gserviceaccount.com'Plan pipeline dùng read-only SA. Apply pipeline dùng SA với quyền ghi và chỉ trigger trên branch main. Hai pipelines có service accounts khác nhau hoàn toàn.