Workload Identity Architecture — Cluster Như Một OIDC Provider
Why this matters in production
Để debug, thiết kế, và bảo mật Workload Identity, bạn phải trả lời được một câu hỏi nền tảng mà hầu hết kỹ sư không bao giờ đặt ra: khi một Pod nói "tôi là service account payment trong namespace prod", ai làm chứng cho lời tuyên bố đó, và Google tin nó dựa trên cơ sở gì? Câu trả lời định hình toàn bộ phần còn lại của chương: chính cluster GKE đóng vai trò một OpenID Connect (OIDC) provider, nó ký một JWT xác nhận danh tính KSA, và Google IAM được cấu hình để tin tưởng OIDC provider đó thông qua một cấu trúc gọi là Workload Identity Pool.
Đây không phải chi tiết học thuật. Trong production, hiểu sai mô hình niềm tin này dẫn tới ba lớp sự cố thực tế và tốn kém:
Sự cố isolation đa cluster. Một đội ngũ tạo hai cluster trong cùng một project —
stagingvàprod— và đặt cùng tên KSAapptrong namespacedefaultở cả hai. Họ cấp cho KSA đó quyền đọc một bucket production nhạy cảm, nghĩ rằng chỉ Pod ở clusterprodmới dùng được. Thực tế, Pod ởstagingcó chính xác cùng danh tính IAM và đọc được cùng bucket đó. Đây là hệ quả trực tiếp của thuộc tính identity sameness mà phần sau giải phẫu — và nó là một trong những lỗ hổng isolation hay bị bỏ sót nhất trên GKE.Sự cố cấp quyền sai granularity. Đội ngũ cấp quyền GCP cho
principalSet://...namespace/prodđể "cho cả namespace dùng được", rồi sáu tháng sau một workload mới hoàn toàn vô can được deploy vào namespace đó và bỗng nhiên thừa hưởng quyền truy cập database production. Hiểu rõ bốn dạng định danh — và dạng nào ánh xạ tới một KSA đơn lẻ vs một tập KSA — là điều kiện để không tạo ra loại quyền dư thừa lan rộng này.Sự cố "không hiểu vì sao nó hoạt động/không hoạt động". Khi một IAM binding với principal string dài 200 ký tự báo lỗi
does not existhoặcinvalid principal, kỹ sư không có mental model về cấu trúc định danh sẽ chỉ thử-và-sai. Nắm được mỗi thành phần trongprincipal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/subject/ns/NAMESPACE/sa/KSA_NAMEnghĩa là gì cho phép bạn đọc lỗi như đọc một địa chỉ sai chính tả, không phải một bí ẩn.
Theo tài liệu Workload Identity Federation for GKE, Workload Identity Federation là "the recommended way for your workloads running on Google Kubernetes Engine (GKE) to access Google Cloud services in a secure and manageable way". Từ khóa là manageable — và tính quản lý được đó bắt nguồn trực tiếp từ kiến trúc niềm tin mà file này mô tả.
Internal model: cluster là một OIDC provider
OpenID Connect — nền tảng khái niệm
Toàn bộ Workload Identity là OIDC ở mọi tầng, nên cần nói rõ mental model OIDC trước. OIDC là một lớp xác thực xây trên OAuth 2.0, trong đó một identity provider (IdP) phát hành một ID token dạng JWT (JSON Web Token) ký số, chứa các claim về danh tính. Một bên thứ ba (relying party) có thể verify token đó mà không cần liên hệ trực tiếp với IdP, bằng cách:
- Đọc claim
iss(issuer) trong token để biết IdP nào phát hành. - Tải tài liệu cấu hình của IdP tại
ISSUER_URL/.well-known/openid-configuration. - Từ đó lấy URL của JWKS (JSON Web Key Set) — tập public key của IdP.
- Dùng public key tương ứng (chọn theo
kidtrong header JWT) để verify chữ ký RS256 của token. - Kiểm tra các claim
aud(audience — token này dành cho ai),exp(hết hạn),sub(subject — danh tính).
Điểm tinh tế quan trọng: verify là offline đối với IdP. Relying party chỉ cần JWKS public key (cache được), không gọi ngược về IdP mỗi lần. Đây là lý do Workload Identity scale được tới hàng nghìn token/giây mà không biến cluster thành bottleneck.
Mỗi GKE cluster là một OIDC issuer độc lập
Khi bạn bật Workload Identity, mỗi cluster GKE trở thành một OIDC provider với issuer URL riêng, theo định dạng:
https://container.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/clusters/CLUSTER_NAMECluster công bố cấu hình OIDC của mình tại ISSUER_URL/.well-known/openid-configuration, và từ đó là JWKS chứa public key mà cluster dùng để ký token KSA. Bạn có thể tự lấy issuer URL từ bên trong cluster:
kubectl get --raw /.well-known/openid-configuration | jq -r .issuerĐiều cốt lõi cần ghim: private key ký token nằm trong control plane của cluster (do Google quản lý), public key được công bố qua JWKS, và Google Security Token Service dùng public key đó để verify. Cluster ký, STS verify — đó là toàn bộ quan hệ tin cậy ở tầng mật mã. File 02 đào sâu cấu trúc token và JWKS; ở đây ta dừng ở mức kiến trúc: cluster là một IdP.
Vì mỗi cluster có issuer riêng, hai cluster khác nhau ký token bằng key khác nhau, và issuer URL nhúng CLUSTER_NAME + LOCATION. Đây là cơ sở kỹ thuật cho phép — về nguyên tắc — phân biệt token đến từ cluster nào. Nhưng như phần identity sameness sẽ chỉ ra, cơ chế binding mặc định của Workload Identity không tận dụng sự phân biệt này, và đó là nguồn của một lớp lỗi isolation.
Workload Identity Pool — cây cầu để IAM hiểu Kubernetes
IAM không hiểu khái niệm "Kubernetes ServiceAccount". IAM hiểu principal — user, service account, group, hoặc một danh tính federated. Để IAM có thể tham chiếu một KSA như một principal hợp lệ, GKE tạo một Workload Identity Pool cố định cho project, với tên bất biến:
PROJECT_ID.svc.id.googTheo tài liệu, pool này "provides a naming format that allows IAM to understand and trust Kubernetes credentials". Có vài đặc tính của pool này mà production cần nhớ:
- Pool là cấp project, không phải cấp cluster. Một project có đúng một pool
PROJECT_ID.svc.id.goog, dùng chung cho mọi cluster trong project. Đây là gốc rễ của identity sameness. - Pool tồn tại ngay cả khi không có cluster nào. Tài liệu nêu rõ pool này "remains even if all clusters are deleted from the project". Pool là một thuộc tính của project, không phải artifact của cluster.
- Pool ánh xạ issuer của mọi cluster trong project về cùng một không gian danh tính. Mọi cluster trong project đều federate vào cùng pool này; danh tính được biểu diễn không phải theo cluster mà theo
(namespace, serviceaccount).
Hãy hình dung pool như một namespace danh tính mà IAM dành riêng cho Kubernetes của project bạn. Mọi KSA trong mọi cluster của project được biểu diễn như một địa chỉ trong không gian này.
Bốn dạng định danh: principal và principalSet
Đây là phần kỹ thuật quan trọng nhất của file. IAM cho phép tham chiếu danh tính Kubernetes ở bốn mức độ chi tiết (granularity) khác nhau, qua hai loại tiền tố: principal:// cho một danh tính đơn, và principalSet:// cho một tập danh tính. Hiểu sai bốn dạng này là nguồn gốc của hầu hết lỗi cấp quyền sai phạm vi.
Dạng 1 — Một KSA cụ thể theo tên (principal)
principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/subject/ns/NAMESPACE/sa/SERVICEACCOUNTĐây là dạng granular nhất và là dạng nên dùng mặc định. Nó trỏ tới đúng một KSA, xác định bởi cặp (namespace, serviceaccount name). Một IAM binding với principal này cấp quyền GCP cho chính xác KSA đó — không hơn. Khi áp dụng nguyên tắc KSA-per-workload (mỗi workload một KSA riêng), dạng này cho granularity đúng bằng workload.
Lưu ý cấu trúc: PROJECT_NUMBER (số, không phải PROJECT_ID) định danh project chứa pool; PROJECT_ID.svc.id.goog là tên pool; subject/ns/NAMESPACE/sa/SERVICEACCOUNT là phần subject mô tả KSA. Phần subject này tương ứng trực tiếp với claim sub trong projected JWT mà file 02 sẽ mổ xẻ.
Dạng 2 — Một KSA cụ thể theo UID (principal)
principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/kubernetes.serviceaccount.uid/SERVICEACCOUNT_UIDDạng này trỏ tới một KSA theo UID (định danh duy nhất Kubernetes gán khi tạo object) thay vì theo tên. Khác biệt ngữ nghĩa rất quan trọng:
- Theo tên (dạng 1): nếu bạn xóa KSA
apprồi tạo lại một KSAappmới, principal string không đổi — danh tính IAM "kế thừa". Tốt cho cấu hình khai báo ổn định, nhưng có nghĩa một KSA bị xóa-tạo-lại (kể cả bởi người khác) vẫn thừa hưởng quyền cũ. - Theo UID (dạng 2): mỗi lần tạo KSA sinh UID mới, nên binding gắn UID chỉ áp dụng cho đúng một instance của KSA. Xóa-tạo-lại = mất quyền. An toàn hơn trước tấn công "tái tạo KSA cùng tên để chiếm quyền", nhưng kém tiện cho GitOps khai báo.
Trong thực tế, dạng theo tên phổ biến hơn nhiều vì hợp với mô hình khai báo; dạng UID dùng khi cần khóa chặt một instance cụ thể.
Dạng 3 — Cả một namespace (principalSet)
principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/namespace/NAMESPACETiền tố đổi thành principalSet:// — đây là một tập danh tính, không phải một danh tính. Binding với dạng này cấp quyền cho mọi KSA trong namespace đó, kể cả KSA default, kể cả mọi workload tương lai được deploy vào namespace. Đây là con dao hai lưỡi: tiện cho "cấp một lần cho cả team namespace", nhưng phá vỡ granularity per-workload và là nguồn của quyền dư thừa lan theo thời gian. Dùng có chủ đích, không dùng vì lười.
Dạng 4 — Cả một cluster (principalSet)
principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/kubernetes.cluster/https://container.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/clusters/CLUSTER_NAMEDạng này cấp quyền cho mọi KSA trong mọi namespace của một cluster cụ thể. Điểm đáng chú ý: nó nhúng đầy đủ issuer URL của cluster (https://container.googleapis.com/v1/projects/.../clusters/CLUSTER_NAME). Đây chính là dạng định danh duy nhất phân biệt được cluster — và do đó là công cụ để khắc phục vấn đề identity sameness (xem phần sau). Phạm vi của nó rất rộng nên hiếm khi dùng để cấp quyền trực tiếp; giá trị chính của nó là làm thành phần trong IAM condition để giới hạn một binding khác chỉ áp dụng cho một cluster.
Bảng so sánh bốn dạng
| Dạng | Tiền tố | Phạm vi | Phân biệt cluster? | Dùng khi |
|---|---|---|---|---|
| 1. KSA theo tên | principal:// | Một KSA (ns, sa) | Không | Mặc định, KSA-per-workload |
| 2. KSA theo UID | principal:// | Một instance KSA cụ thể | Không | Khóa chặt instance, chống tái tạo |
| 3. Namespace | principalSet:// | Mọi KSA trong namespace | Không | Cấp theo namespace có chủ đích |
| 4. Cluster | principalSet:// | Mọi KSA trong cluster | Có | IAM condition giới hạn cluster |
Identity sameness: thuộc tính nguy hiểm nhất cần hiểu
Đây là điểm mà mọi kiến trúc sư GKE phải nội hóa. Vì pool PROJECT_ID.svc.id.goog là cấp project và dạng định danh mặc định (dạng 1) chỉ chứa (namespace, serviceaccount) mà không chứa thông tin cluster, nên:
Hai KSA có cùng tên, trong cùng namespace, ở hai cluster khác nhau nhưng cùng project, được IAM nhìn nhận là cùng một danh tính.
Tài liệu Google nêu thẳng: workload dùng cùng tên KSA across clusters trong cùng project "receive the same identity—IAM cannot distinguish between clusters". Hệ quả thực tế:
- Quyền GCP cấp cho
principal://...ns/prod/sa/appáp dụng cho Pod dùng KSAprod/appở mọi cluster trong project —dev,staging,prod, cluster thử nghiệm của một kỹ sư, tất cả. - Cấp quyền nhạy cảm cho một KSA nghĩa là tin tưởng mọi cluster trong project. Nếu một cluster dev kém bảo mật hơn, nó trở thành đường vòng để lấy quyền của workload prod.
Có hai cách xử lý, theo đúng tài liệu — "requires trusting all clusters in a workload identity pool or using conditional IAM policies to scope access to specific clusters":
Tách project theo môi trường.
prodvàstagingở hai project khác nhau → hai pool khác nhau → không có identity sameness giữa chúng. Đây là cách sạch nhất và phù hợp nguyên tắc resource hierarchy (xem Chương 1): isolation môi trường nên ở cấp project, không cấp cluster.IAM condition giới hạn cluster. Nếu buộc phải nhiều cluster cùng project, thêm một IAM condition vào binding dùng dạng định danh cấp cluster (dạng 4) để chỉ cho phép cluster cụ thể. Phức tạp hơn, dễ sai hơn, nhưng khả thi khi không thể tách project.
Nguyên tắc kiến trúc rút ra: dùng project làm ranh giới isolation cho Workload Identity, không dùng cluster. Nhiều cluster trong một project chia sẻ một không gian danh tính phẳng — hãy thiết kế với giả định đó.
Production architecture patterns
Pattern: một project — một môi trường — một pool
Mô hình sạch nhất là ánh xạ một-một giữa project và môi trường: project acme-prod, acme-staging, acme-dev, mỗi cái có pool riêng acme-prod.svc.id.goog, v.v. Workload Identity binding trong project prod hoàn toàn cô lập khỏi staging. Identity sameness chỉ còn áp dụng trong một môi trường — nơi mà nó ít gây hại hơn (các cluster cùng môi trường thường cùng mức tin cậy). Đây là pattern mặc định nên áp dụng trừ khi có lý do mạnh để làm khác.
Pattern: KSA-per-workload với principal dạng 1
Mỗi workload (Deployment/StatefulSet) có một KSA riêng, đặt tên theo workload (payment, order-processor, image-resizer), và mỗi KSA được cấp quyền qua principal dạng 1 trỏ đúng nó. Tuyệt đối tránh dùng KSA default của namespace cho workload có quyền GCP — vì default được mọi Pod không khai báo serviceAccountName dùng chung, biến nó thành một danh tính chia sẻ ngầm. Granularity per-workload là điều kiện để khi một workload bị chiếm, quyền lộ ra đúng bằng nhu cầu của nó.
Pattern: Fleet Workload Identity cho multi-cluster app
Với ứng dụng trải nhiều cluster được quản lý qua GKE Fleet (Anthos), có một Fleet Workload Identity pool ở cấp fleet (FLEET_PROJECT_ID.svc.id.goog) cho phép workload across nhiều cluster trong fleet chia sẻ danh tính một cách có chủ đích và nhất quán. Đây là biến thể "có chủ đích" của identity sameness — bạn muốn các bản sao của cùng một service ở nhiều cluster có cùng danh tính, và Fleet WI cung cấp điều đó như một tính năng thay vì một tác dụng phụ. Chi tiết Fleet vượt phạm vi file này, nhưng nguyên tắc kiến trúc giống hệt: federation vào một pool chung.
Common mistakes / anti-patterns
1. Giả định cluster là ranh giới isolation của danh tính. Tạo nhiều cluster cùng project cho các môi trường khác nhau, cấp quyền KSA tưởng là tách biệt. Vì sao xảy ra: tư duy "cluster = ranh giới" đúng với hầu hết thứ khác trong Kubernetes, nhưng sai với Workload Identity. Hệ quả ở scale: workload kém tin cậy ở một cluster lấy được quyền của workload nhạy cảm ở cluster khác cùng project. Phòng tránh: tách project theo môi trường; nếu không, dùng IAM condition cấp cluster (dạng 4).
2. Dùng principalSet namespace/cluster vì "tiện". Bind role cho principalSet://...namespace/NS thay vì từng KSA. Vì sao xảy ra: ít dòng binding hơn, "cả team dùng được". Hệ quả: mọi workload hiện tại và tương lai trong namespace thừa hưởng quyền; quyền tích lũy âm thầm. Phòng tránh: mặc định dùng principal dạng 1 per-KSA; chỉ dùng principalSet khi thực sự muốn cấp cho cả tập và đã cân nhắc.
3. Nhầm PROJECT_ID với PROJECT_NUMBER trong principal string. Phần đầu định danh dùng PROJECT_NUMBER (số), phần pool dùng PROJECT_ID (chuỗi). Vì sao xảy ra: hai thứ trông giống nhau về vai trò. Hệ quả: binding lỗi invalid principal hoặc cấp cho project sai. Phòng tránh: lấy đúng gcloud projects describe PROJECT_ID --format='value(projectNumber)'.
4. Cấp quyền cho KSA default. Cấp quyền GCP cho default/ns rồi mọi Pod không đặt serviceAccountName đều có quyền đó. Vì sao xảy ra: quên đặt KSA riêng, copy YAML thiếu serviceAccountName. Hệ quả: danh tính chia sẻ ngầm, không audit được Pod nào thực sự cần quyền. Phòng tránh: luôn tạo KSA có tên cho workload cần quyền GCP; coi default là "không có quyền GCP".
5. Quên rằng pool tồn tại độc lập với cluster. Xóa cluster rồi nghĩ Workload Identity binding "tự dọn". Pool và các binding principal:// vẫn còn. Hệ quả: binding mồ côi trỏ tới KSA không còn tồn tại, gây nhiễu khi audit IAM. Phòng tránh: dọn IAM binding như một bước riêng khi decommission, không trông cậy việc xóa cluster.
GCP-native implementation guidance
# Lấy PROJECT_NUMBER để dựng principal string
gcloud projects describe PROJECT_ID --format='value(projectNumber)'
# Lấy OIDC issuer URL của cluster (chạy với kubeconfig trỏ vào cluster)
kubectl get --raw /.well-known/openid-configuration | jq -r .issuer
# -> https://container.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/clusters/CLUSTER_NAME
# Cấp quyền cho ĐÚNG một KSA (principal dạng 1 — khuyến nghị)
gcloud projects add-iam-policy-binding PROJECT_ID \
--role="roles/storage.objectViewer" \
--member="principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/subject/ns/NAMESPACE/sa/KSA_NAME" \
--condition=None
# Cấp cho cả namespace (principalSet dạng 3 — cân nhắc kỹ phạm vi)
gcloud projects add-iam-policy-binding PROJECT_ID \
--role="roles/storage.objectViewer" \
--member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/namespace/NAMESPACE"Để giới hạn một binding chỉ áp dụng cho một cluster (khắc phục identity sameness khi nhiều cluster cùng project), kết hợp principal dạng 1 với một IAM condition tham chiếu định danh cấp cluster — đây là cách "scope access to specific clusters" mà tài liệu đề cập, dùng khi không thể tách project theo môi trường.
Operational implications
Kiến trúc niềm tin này thay đổi cách bạn tư duy về "quản lý danh tính workload". Không còn câu hỏi "credential ở đâu, ai giữ, khi nào rotate"; thay vào đó là ba câu hỏi khai báo, audit được: (1) KSA nào tồn tại trong cluster nào? — trả lời bằng kubectl get sa -A; (2) mỗi KSA principal được cấp role GCP gì? — trả lời bằng đọc IAM policy; (3) có identity sameness nào tạo ra quyền ngoài ý muốn không? — trả lời bằng kiểm xem có nhiều cluster cùng project chia sẻ tên KSA không.
Hệ quả tinh tế cho audit và forensics: vì danh tính được biểu diễn ở (project, namespace, serviceaccount) chứ không ở cluster, một dòng IAM policy principal://...ns/prod/sa/payment cho bạn biết chính xác workload nào có quyền — nhưng không cho biết Pod đó chạy ở cluster nào (trừ khi dùng định danh cấp cluster). Khi điều tra "ai đã đọc bucket này", bạn cần kết hợp IAM principal với Cloud Audit Log và thông tin cluster để định vị Pod cụ thể. Đây là lý do thiết kế danh tính nên đi cùng thiết kế audit ngay từ đầu (xem Chương 12, file 10 về audit logging).
Mental model để mang sang các file sau: cluster ký, pool đặt tên, IAM tin tưởng. File 02 mổ xẻ chính token mà cluster ký; file 03 theo dấu token đó qua metadata server và STS; file 04 dùng đúng các định danh principal ở đây để cấp quyền. Toàn bộ phần còn lại của chương đứng trên kiến trúc niềm tin OIDC mà file này vừa dựng.
References
- Workload Identity Federation for GKE — concepts
- Authenticate to Google Cloud APIs from GKE workloads
- Workload Identity Federation (external IdP) — overview
- Use external identity providers to authenticate to GKE (OIDC)
- Fleet Workload Identity
- OpenID Connect specification
- Resource hierarchy — project as isolation boundary