Workload Identity trên GKE: mỗi pod một danh tính Google riêng
Hướng dẫn thực tế Workload Identity Federation for GKE: cấp quyền trực tiếp cho principal hay liên kết service account, triển khai bằng gcloud và Terraform, kiểm tra, truy cập cross-project, các cái bẫy, và quản lý bằng YAML với Config Connector.

Có hai cách phổ biến khiến một pod trên GKE gọi được Google API, và cả hai đều tệ:
- Service account key (
key.json) nằm trong Kubernetes Secret. Key không bao giờ hết hạn, bị copy khắp nơi, và dùng được từ bất kỳ đâu. - Service account của node. Mọi pod trên node âm thầm nhận chung một bộ quyền. Ở cluster cũ, đó thường là Compute Engine default service account, vốn có role Editor trên toàn project.
Workload Identity Federation for GKE giải quyết cả hai. Mỗi Kubernetes ServiceAccount (KSA) trở thành một danh tính Google riêng. Pod nhận token ngắn hạn từ một metadata server trên chính node của nó. Không có key, và không workload nào dùng chung quyền với workload nào.
Đây là bài “bản địa GCP” trong loạt bài: EKS Pod Identity vs IRSA, AWS → GCP, AWS → Azure.
Cơ chế hoạt động
Khi bật Workload Identity, GKE chạy một GKE metadata server trên mỗi node. Thư viện client của Google vốn đã hỏi credentials ở 169.254.169.254, và giờ server này trả lời thay cho KSA của pod chứ không phải cho node. Code hiện có chạy được mà không cần sửa.
Mọi cluster trong project shop-prod đều thuộc workload identity pool shop-prod.svc.id.goog. Trong pool đó, KSA orders-api ở namespace orders là principal sau:
principal://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/shop-prod.svc.id.goog/subject/ns/orders/sa/orders-api
Có hai cách cấp quyền cho danh tính đó:
| A: cấp quyền trực tiếp cho principal (khuyến nghị) | B: liên kết IAM service account | |
|---|---|---|
| Cấp role cho ai | principal://…/ns/NS/sa/KSA |
một IAM service account (GSA) |
| Object phải tạo thêm | không | GSA + roles/iam.workloadIdentityUser + annotation trên KSA |
| Audit log hiển thị | principal của KSA | GSA |
| Dùng khi | API hỗ trợ principal identifier (phần lớn đều hỗ trợ) | API chưa hỗ trợ, hoặc cần email của GSA (ví dụ ký signed URL) |
Google khuyến nghị dùng A khi sản phẩm hỗ trợ, và chỉ dùng B để lấp những chỗ chưa hỗ trợ.
Bước 1: bật trên cluster
# Cluster Standard (Autopilot bật sẵn)
gcloud container clusters update prod-eu-1 \
--location=europe-west1 \
--workload-pool=shop-prod.svc.id.goog
# node pool hiện có phải chuyển sang GKE metadata server
gcloud container node-pools update default-pool \
--cluster=prod-eu-1 --location=europe-west1 \
--workload-metadata=GKE_METADATA
Cập nhật node pool sẽ tạo lại các node, hãy lên kế hoạch như một lần nâng cấp node bình thường.
Nhân tiện, đừng cho node một danh tính quá mạnh. Tạo một service account riêng cho node, chỉ có role roles/container.defaultNodeServiceAccount. Khi Workload Identity đã bật, pod không chạm được tới nó, nên nó chỉ cần đủ quyền để vận hành node.
Bước 2a: cấp quyền trực tiếp cho principal
Tạo một KSA bình thường, không cần annotation:
apiVersion: v1
kind: ServiceAccount
metadata:
name: orders-api
namespace: orders
Cấp role trên tài nguyên cho principal của KSA:
PRINCIPAL="principal://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/shop-prod.svc.id.goog/subject/ns/orders/sa/orders-api"
gcloud storage buckets add-iam-policy-binding gs://shop-prod-orders-reports \
--role=roles/storage.objectViewer --member="$PRINCIPAL"
gcloud pubsub subscriptions add-iam-policy-binding orders-events \
--project=shop-prod --role=roles/pubsub.subscriber --member="$PRINCIPAL"
Muốn cấp rộng hơn một KSA, dùng principalSet:
# mọi KSA trong một namespace
principalSet://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/shop-prod.svc.id.goog/namespace/orders
# mọi KSA trong một cluster
principalSet://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/shop-prod.svc.id.goog/kubernetes.cluster/https://container.googleapis.com/v1/projects/shop-prod/locations/europe-west1/clusters/prod-eu-1
Bước 2b: liên kết IAM service account (khi cần)
gcloud iam service-accounts create orders-api --project=shop-prod
gcloud iam service-accounts add-iam-policy-binding \
orders-api@shop-prod.iam.gserviceaccount.com \
--role=roles/iam.workloadIdentityUser \
--member="serviceAccount:shop-prod.svc.id.goog[orders/orders-api]"
gcloud storage buckets add-iam-policy-binding gs://shop-prod-orders-reports \
--role=roles/storage.objectViewer \
--member="serviceAccount:orders-api@shop-prod.iam.gserviceaccount.com"
apiVersion: v1
kind: ServiceAccount
metadata:
name: orders-api
namespace: orders
annotations:
iam.gke.io/gcp-service-account: orders-api@shop-prod.iam.gserviceaccount.com
Cần cả hai: IAM binding và annotation. Thiếu một trong hai là lý do phổ biến nhất khiến mô hình này “không chạy”.
Bước 3: dùng trong Deployment
Pod spec không có gì riêng cho GCP, chỉ cần chỉ định KSA:
spec:
template:
spec:
serviceAccountName: orders-api
containers:
- name: app
image: europe-docker.pkg.dev/shop-prod/apps/orders-api:1.8.0
Thư viện client tự lấy credentials qua Application Default Credentials. Không đặt GOOGLE_APPLICATION_CREDENTIALS, không mount key.
Bước 4: kiểm tra từ bên trong cluster
kubectl run wi-test -n orders -it --rm --image=google/cloud-sdk:slim \
--overrides='{"spec":{"serviceAccountName":"orders-api"}}' -- bash
# bên trong pod
curl -s -H "Metadata-Flavor: Google" \
http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/email
# A → shop-prod.svc.id.goog B → orders-api@shop-prod.iam.gserviceaccount.com
gcloud storage ls gs://shop-prod-orders-reports
Nếu email trả về là service account của node, nghĩa là node pool chưa dùng GKE_METADATA.
Terraform
data "google_project" "this" {
project_id = "shop-prod"
}
resource "google_container_cluster" "prod" {
name = "prod-eu-1"
location = "europe-west1"
# …
workload_identity_config {
workload_pool = "shop-prod.svc.id.goog"
}
}
resource "google_container_node_pool" "default" {
cluster = google_container_cluster.prod.name
# …
node_config {
service_account = google_service_account.nodes.email # SA tối giản cho node
workload_metadata_config {
mode = "GKE_METADATA"
}
}
}
locals {
orders_api = "principal://iam.googleapis.com/projects/${data.google_project.this.number}/locations/global/workloadIdentityPools/shop-prod.svc.id.goog/subject/ns/orders/sa/orders-api"
}
resource "google_storage_bucket_iam_member" "orders_reports" {
bucket = google_storage_bucket.orders_reports.name
role = "roles/storage.objectViewer"
member = local.orders_api
}
Truy cập cross-project
Danh tính nằm trong pool của project chứa cluster, nhưng có thể được cấp role ở bất kỳ đâu. Muốn orders-api (cluster ở shop-prod) đọc dataset trong analytics-prod, chỉ cần thêm binding ở analytics-prod với đúng member principal://…/shop-prod.svc.id.goog/…. Phía cluster không phải thay đổi gì.
Quản lý bằng YAML với Config Connector
Làm bằng gcloud hay một repo Terraform riêng thì quyền của ứng dụng nằm rất xa ứng dụng. Config Connector là phiên bản GCP của AWS Controllers for Kubernetes. Nó biến tài nguyên Google, kể cả IAM binding, thành object Kubernetes, nhờ đó Helm chart của ứng dụng tự sở hữu danh tính và quyền của chính nó.
Bật Config Connector dưới dạng add-on của GKE (chính nó cũng cần Workload Identity), cấp cho controller một danh tính Google, rồi chỉ định project cần quản lý:
gcloud container clusters update prod-eu-1 --location=europe-west1 \
--update-addons ConfigConnector=ENABLED
gcloud iam service-accounts create kcc-controller --project=shop-prod
gcloud projects add-iam-policy-binding shop-prod \
--member="serviceAccount:kcc-controller@shop-prod.iam.gserviceaccount.com" \
--role="roles/editor" # bắt đầu từ đây, thu hẹp lại cho production
gcloud iam service-accounts add-iam-policy-binding \
kcc-controller@shop-prod.iam.gserviceaccount.com \
--role=roles/iam.workloadIdentityUser \
--member="serviceAccount:shop-prod.svc.id.goog[cnrm-system/cnrm-controller-manager]"
apiVersion: core.cnrm.cloud.google.com/v1beta1
kind: ConfigConnector
metadata:
name: configconnector.core.cnrm.cloud.google.com
spec:
mode: cluster
googleServiceAccount: kcc-controller@shop-prod.iam.gserviceaccount.com
stateIntoSpec: Absent
---
apiVersion: v1
kind: Namespace
metadata:
name: orders
annotations:
cnrm.cloud.google.com/project-id: shop-prod
Giờ chart của ứng dụng có thể khai báo bucket và quyền ngay cạnh Deployment:
# templates/gcp/bucket.yaml
apiVersion: storage.cnrm.cloud.google.com/v1beta1
kind: StorageBucket
metadata:
name: shop-prod-orders-reports # = tên bucket
annotations:
cnrm.cloud.google.com/deletion-policy: abandon # giữ dữ liệu nếu release bị xoá
spec:
location: EU
uniformBucketLevelAccess: true
versioning:
enabled: true
---
# templates/gcp/iam.yaml
apiVersion: iam.cnrm.cloud.google.com/v1beta1
kind: IAMPolicyMember
metadata:
name: orders-api-reports-reader
spec:
member: {{ include "orders-api.wiPrincipal" . }}
role: roles/storage.objectViewer
resourceRef:
kind: StorageBucket
name: shop-prod-orders-reports
# templates/_helpers.tpl — principal của KSA, định nghĩa một lần
{{- define "orders-api.wiPrincipal" -}}
principal://iam.googleapis.com/projects/{{ .Values.gcp.projectNumber }}/locations/global/workloadIdentityPools/{{ .Values.gcp.projectId }}.svc.id.goog/subject/ns/{{ .Release.Namespace }}/sa/{{ include "orders-api.serviceAccountName" . }}
{{- end }}
Với mô hình B, chart thêm một IAMServiceAccount và liên kết KSA vào đó. memberFrom.serviceAccountRef cho phép object này tham chiếu tới object kia, nên không ai phải gõ email:
apiVersion: iam.cnrm.cloud.google.com/v1beta1
kind: IAMServiceAccount
metadata:
name: orders-api # account ID → orders-api@shop-prod.iam.gserviceaccount.com
spec:
displayName: orders-api (GKE)
---
apiVersion: iam.cnrm.cloud.google.com/v1beta1
kind: IAMPolicyMember
metadata:
name: orders-api-wi-user
spec:
member: serviceAccount:shop-prod.svc.id.goog[orders/orders-api]
role: roles/iam.workloadIdentityUser
resourceRef:
kind: IAMServiceAccount
name: orders-api
---
apiVersion: iam.cnrm.cloud.google.com/v1beta1
kind: IAMPolicyMember
metadata:
name: orders-api-reports-reader
spec:
memberFrom:
serviceAccountRef:
name: orders-api
role: roles/storage.objectViewer
resourceRef:
kind: StorageBucket
name: shop-prod-orders-reports
Nhiều team, nhiều project: namespaced mode
Nếu một danh tính controller duy nhất có quyền Editor trên mọi project, thì namespace nào cũng đụng được vào project nào. Namespaced mode cho mỗi namespace một controller Config Connector riêng với danh tính Google riêng:
apiVersion: core.cnrm.cloud.google.com/v1beta1
kind: ConfigConnector
metadata:
name: configconnector.core.cnrm.cloud.google.com
spec:
mode: namespaced
---
apiVersion: core.cnrm.cloud.google.com/v1beta1
kind: ConfigConnectorContext
metadata:
name: configconnectorcontext.core.cnrm.cloud.google.com
namespace: orders
spec:
googleServiceAccount: kcc-orders@shop-prod.iam.gserviceaccount.com
Controller riêng của namespace chạy dưới KSA cnrm-system/cnrm-controller-manager-orders, nên đó là KSA được cấp roles/iam.workloadIdentityUser trên kcc-orders. Chỉ cấp cho kcc-orders các role admin của những tài nguyên team sở hữu, không cấp resourcemanager.projectIamAdmin. Thêm luật Kyverno hoặc Gatekeeper để IAMPolicyMember trong orders chỉ được nêu principal thuộc …/ns/orders/….
Những cái bẫy của Config Connector
IAMPolicyMemberkhông sửa được. Đổimemberhayrolenghĩa là xoá object rồi tạo lại. Trong Argo CD, đặtReplace=truecho các resource này.- Đừng bao giờ dùng
IAMPolicycùngIAMPolicyMembertrên cùng một tài nguyên.IAMPolicymang tính áp đặt toàn bộ (authoritative) và sẽ xoá các binding bạn không khai báo. Làm vậy trên project có thể khoá tất cả mọi người ra ngoài. - Xoá object là xoá luôn tài nguyên trên cloud, trừ khi có annotation
cnrm.cloud.google.com/deletion-policy: abandon. Hãy gắn nó cho mọi bucket và database. BigQueryDatasetkhông dùng được vớiIAMPolicyMember. Quản lý quyền truy cập dataset quaspec.accesscủa chính dataset.- Tài nguyên được reconcile khoảng mỗi 10 phút, nên thay đổi bằng tay trên console sẽ bị ghi đè. Đó chính là mục đích, nhưng hãy báo cho team biết.
Những cái bẫy (của chính Workload Identity)
- Identity sameness. Pool tính theo project, không theo cluster.
orders/orders-apiở bất kỳ cluster nào trongshop-prodcũng là cùng một principal. Đừng đặt các cluster có mức tin cậy khác nhau (dev và prod, hay một cluster CI dùng chung) trong cùng project. Nếu buộc phải làm vậy, hãy cấp quyền cho principalSetkubernetes.cluster/…. - Pod
hostNetwork: truekhông dùng được Workload Identity, vì chúng nói chuyện thẳng với metadata của node. - Race lúc khởi động. GKE metadata server cần một chút thời gian sau khi pod khởi động. Ứng dụng lấy credentials ngay trong giây đầu tiên nên có cơ chế thử lại, và mọi thư viện client của Google đều đã làm sẵn việc này.
- IAM có độ trễ. Binding mới có thể mất vài phút mới có hiệu lực. Ngay sau khi deploy có thể gặp vài lỗi
403thoáng qua, đừng coi đó là thất bại. - Chưa phải API nào cũng nhận
principal://. Gặp API từ chối thì chuyển riêng workload đó sang mô hình B, các workload khác giữ nguyên. - Service account của node. Giữ nó tối giản. Workload Identity bảo vệ pod khỏi nó, nhưng pod
hostNetworkvà chính node vẫn dùng nó.
Kết luận
Trên GKE, Workload Identity chỉ là một thiết lập ở cluster cộng một IAM binding. Mặc định hãy cấp role thẳng cho principal của Kubernetes ServiceAccount, và chỉ dùng service account liên kết ở những chỗ API đòi hỏi. Sau đó đưa các binding ra khỏi việc bấm tay: với Config Connector, Helm chart của ứng dụng tự sở hữu bucket, danh tính và quyền của nó, được review trong cùng pull request với code dùng chúng.
Nguồn
- GKE Workload Identity Federation: khái niệm: https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity
- GKE Workload Identity Federation: hướng dẫn: https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity
- Định danh principal trong IAM: https://cloud.google.com/iam/docs/principal-identifiers
- Tổng quan Config Connector: https://cloud.google.com/config-connector/docs/overview
- Config Connector: namespace và project: https://cloud.google.com/config-connector/docs/concepts/namespaces-and-projects



Bình luận