Skip to content

IAM Binding Models — Cấp Quyền Cho Danh Tính Workload

Why this matters in production

File 03 kết thúc ở chỗ metadata server lấy về một federated access token đại diện danh tính KSA. Nhưng danh tính có quyền gì là một câu hỏi authorization hoàn toàn tách biệt — và đây là nơi Workload Identity có hai mô hình cấu hình khác nhau về bản chất, không chỉ về cú pháp. Chọn sai mô hình, hoặc trộn lẫn hai mô hình mà không hiểu, là nguồn của phần lớn sự cố "token lấy được nhưng vẫn 403" và "không hiểu vì sao workload có quyền nó không nên có".

Hai mô hình:

  1. Mô hình trực tiếp (direct resource access): bind role GCP thẳng cho principal KSA. Federated token gọi API luôn, không qua trung gian.
  2. Mô hình impersonation: KSA được phép mạo danh một IAM service account (GSA); federated token đổi tiếp lấy token của GSA; quyền là quyền của GSA.

Sự khác biệt không hàn lâm — nó quyết định ba thứ rất thực tế trong production:

  • Điểm điều tra khi debug 403. Mô hình trực tiếp: kiểm role cấp cho principal KSA. Mô hình impersonation: kiểm cả binding workloadIdentityUser role cấp cho GSA annotation đúng. Không biết mình ở mô hình nào nghĩa là không biết tìm lỗi ở đâu — và hai mô hình có thể cùng tồn tại trong một cluster, thậm chí một namespace.

  • Bề mặt quyền và blast radius. Mô hình impersonation thêm một lớp gián tiếp: nhiều KSA có thể cùng impersonate một GSA, nghĩa là một GSA quyền cao bị "chia sẻ" giữa nhiều workload. Một KSA bị chiếm có thể có toàn bộ quyền của GSA mà nó impersonate — thường rộng hơn nhiều so với nhu cầu thật.

  • Tính tương thích và migration. Tổ chức đã có hàng trăm GSA với quyền được chuẩn hóa thường chọn impersonation để tái dùng. Tổ chức mới hoặc đang đơn giản hóa chọn trực tiếp. Hiểu cả hai cho phép migration có kiểm soát thay vì viết lại từ đầu.

Tài liệu Google hiện khuyến nghị mô hình trực tiếp cho workload mới vì nó "eliminates the need for manual ServiceAccount annotation configuration" — ít thành phần, ít chỗ sai. Nhưng impersonation vẫn phổ biến và hợp lệ. File này mổ xẻ cả hai, ngữ nghĩa, và tiêu chí chọn.

Internal model: hai luồng authorization

Nhắc lại: federated token đại diện gì

Sau token exchange (file 03, bước 4), metadata server có một federated access token đại diện cho principal://...sa/KSA. Câu hỏi tiếp theo — token này gọi được API nào — phụ thuộc role IAM gắn với principal đó hoặc với GSA mà principal được phép impersonate. Hai mô hình phân nhánh chính ở đây.

Mô hình trực tiếp: bind role thẳng cho principal KSA

Ý tưởng: coi KSA như một principal IAM hạng nhất và cấp role GCP thẳng cho nó, dùng định danh principal:// (file 01, dạng 1). Không có GSA, không có annotation, không có bước impersonation.

bash
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

Luồng runtime: federated token (đại diện principal KSA) gọi thẳng storage.googleapis.com; backend verify token, thấy principal có roles/storage.objectViewer, cho phép. Ba bước token exchange của file 03 là toàn bộ chuỗi — không có bước thứ tư.

Ưu điểm: ít thành phần nhất (không GSA, không annotation), granularity per-KSA tự nhiên, dễ audit (đọc IAM policy thấy ngay KSA nào có role gì), dễ suy luận. Nhược điểm: role phải cấp ở mức resource hỗ trợ principal binding (project, bucket, secret, topic...); một số resource/role hiếm chưa hỗ trợ binding principal trực tiếp — khi đó phải dùng impersonation.

Mô hình impersonation: KSA mạo danh GSA

Ý tưởng: KSA không có quyền GCP trực tiếp; thay vào đó nó được phép mạo danh (impersonate) một IAM service account (GSA), và quyền thực tế là quyền của GSA. Cấu hình gồm hai phần:

Phần 1 — Annotate KSA trỏ tới GSA:

bash
kubectl annotate serviceaccount KSA_NAME \
  --namespace NAMESPACE \
  iam.gke.io/gcp-service-account=GSA_NAME@GSA_PROJECT_ID.iam.gserviceaccount.com

Phần 2 — Cho phép KSA impersonate GSA (roles/iam.workloadIdentityUser):

bash
gcloud iam service-accounts add-iam-policy-binding \
  GSA_NAME@GSA_PROJECT_ID.iam.gserviceaccount.com \
  --role="roles/iam.workloadIdentityUser" \
  --member="serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME]"

Lưu ý định dạng member ở phần 2: serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME] — đây là cú pháp đặc thù của Workload Identity, dùng [NAMESPACE/KSA_NAME] trong ngoặc vuông, khác hoàn toàn với cú pháp principal:// dài. Đây là member mà GSA tin tưởng để cho mạo danh.

Quyền GCP thực tế (ví dụ đọc bucket) được cấp cho GSA, không cho KSA:

bash
gcloud projects add-iam-policy-binding PROJECT_ID \
  --role="roles/storage.objectViewer" \
  --member="serviceAccount:GSA_NAME@GSA_PROJECT_ID.iam.gserviceaccount.com"

Luồng runtime: federated token (principal KSA) → dùng để mạo danh GSA qua IAM Credentials → nhận access token của GSA → gọi API với quyền của GSA. Đây là bước thứ tư mà file 03 đề cập — chuỗi dài hơn một bước so với mô hình trực tiếp.

Cơ học của bước impersonation: generateAccessToken

Bước thứ tư không phải phép màu — nó là một lời gọi cụ thể tới IAM Service Account Credentials API, method generateAccessToken. Federated token (đại diện principal KSA) đóng vai caller; nó gọi:

POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/GSA_EMAIL:generateAccessToken
{ "scope": ["https://www.googleapis.com/auth/cloud-platform"], "lifetime": "3600s" }

IAM kiểm tra rằng caller (principal KSA) có roles/iam.workloadIdentityUser trên GSA mục tiêu — đây chính là binding ở Phần 2. Nếu đạt, API trả về một access token OAuth2 của GSA với lifetime yêu cầu (mặc định 1 giờ, trần mặc định cũng 1 giờ trừ khi tổ chức nới qua constraint iam.allowServiceAccountCredentialLifetimeExtension). Hai hệ quả thực hành quan trọng:

  • roles/iam.workloadIdentityUser chỉ cho phép đổi danh tính, không tự cấp quyền tài nguyên nào. Một KSA có quyền impersonate một GSA "trống quyền" thì vẫn không gọi được API gì — quyền nằm hoàn toàn ở các role gắn cho GSA. Đây là lý do mô hình impersonation luôn cần ba mảnh (annotation + workloadIdentityUser + role tài nguyên trên GSA), thiếu một mảnh là một kiểu 403 khác nhau (file 07).
  • Khác biệt với serviceAccountTokenCreator. roles/iam.serviceAccountTokenCreator là role tổng quát cho phép bất kỳ caller tạo token/ký dữ liệu thay GSA (dùng cho impersonation chain thủ công, ký blob, tạo ID token). roles/iam.workloadIdentityUser hẹp hơn: nó chỉ dành cho member kiểu federated identity (principal://, principalSet://, hoặc cú pháp [NS/KSA]). Đừng cấp serviceAccountTokenCreator cho KSA chỉ để impersonate — nó rộng hơn cần thiết.

Bảng so sánh hai mô hình

Tiêu chíTrực tiếp (principal binding)Impersonation (GSA)
Thành phầnChỉ KSA + IAM bindingKSA + annotation + GSA + 2 binding
Quyền cấp choprincipal://...sa/KSAGSA
Số bước token3 (dừng ở federated token)4 (thêm impersonate GSA)
Annotation cần?KhôngCó (gcp-service-account)
GranularityPer-KSA tự nhiênPhụ thuộc GSA (có thể chia sẻ)
Khuyến nghị choWorkload mớiTổ chức đã chuẩn hóa theo GSA
AuditĐọc IAM policy là đủPhải lần annotation → GSA → role

Production architecture patterns

Pattern: mặc định trực tiếp, impersonation khi có lý do

Khuyến nghị thực hành: mặc định mô hình trực tiếp cho mọi workload mới. Nó đơn giản, ít chỗ sai, audit dễ. Chỉ chuyển sang impersonation khi có một trong các lý do thật: (a) tổ chức đã có GSA với quyền chuẩn hóa và muốn tái dùng; (b) cần cấp một role/resource chưa hỗ trợ principal binding trực tiếp; (c) workload cần dùng tính năng chỉ GSA có (ví dụ một số luồng signing, hoặc cần một danh tính có "email" cố định cho hệ thống bên thứ ba). Ngoài các lý do đó, gián tiếp qua GSA chỉ thêm bề mặt lỗi.

Pattern: một GSA cho một workload (nếu dùng impersonation)

Anti-pattern phổ biến nhất của impersonation là nhiều KSA cùng impersonate một GSA quyền rộng — biến GSA thành một danh tính chia sẻ, phá vỡ least-privilege. Nếu chọn impersonation, giữ ánh xạ 1 KSA ↔ 1 GSA, mỗi GSA quyền tối thiểu cho đúng workload. Khi đó impersonation giữ được granularity tương đương mô hình trực tiếp, chỉ thêm một lớp gián tiếp. Cho phép nhiều KSA impersonate cùng GSA chỉ khi chúng thực sự là cùng một workload logic (ví dụ replica ở nhiều namespace).

Pattern: cross-project Workload Identity

Một tình huống production thường gặp: cluster ở project A, nhưng resource (bucket, secret) ở project B. Workload Identity hỗ trợ điều này vì pool, KSA, và GSA/role có thể ở project khác nhau. Hai cách:

  • Trực tiếp cross-project: cấp role cho principal://...PROJECT_A.svc.id.goog/...sa/KSA trên resource của project B. Pool thuộc project A (nơi cluster chạy), binding nằm ở project B (nơi resource ở).
  • Impersonation cross-project: KSA ở cluster project A impersonate một GSA ở project B; GSA project B có quyền trên resource project B.

Với GKE 1.24+, khi cross-project, cần chú ý quota project cho việc đếm quota API. Annotate KSA để chỉ định quota project:

bash
kubectl annotate serviceaccount KSA_NAME \
  --namespace NAMESPACE \
  iam.gke.io/credential-quota-project=QUOTA_PROJECT_ID

Annotation này quyết định project nào bị tính quota cho lời gọi API — quan trọng khi resource và cluster ở project khác nhau và bạn muốn quota tính đúng chỗ.

Pattern: cấp quyền cả namespace bằng principalSet

Đôi khi đơn vị quyền tự nhiên không phải một KSA mà là một namespace — ví dụ mọi workload trong namespace team-a được đọc một bucket chung của team. Thay vì lặp binding cho từng KSA, dùng định danh principalSet:// cấp namespace (file 01, dạng 3):

bash
gcloud storage buckets add-iam-policy-binding gs://team-a-shared \
  --role="roles/storage.objectViewer" \
  --member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/namespace/team-a"

Binding này áp cho mọi KSA trong namespace team-a. Sức mạnh đi kèm rủi ro: nó cũng cấp quyền cho các KSA tương lai tạo trong namespace đó, kể cả KSA mà người tạo không ý thức được sẽ thừa hưởng quyền này. Vì vậy principalSet cấp namespace chỉ nên dùng cho quyền thực sự dùng chung toàn namespaceít nhạy cảm; quyền nhạy cảm vẫn nên bind per-KSA để giữ ranh giới rõ ràng. Cùng nguyên tắc áp cho định danh cấp cluster (dạng 4) — nó rộng hơn nữa và hiếm khi đáng dùng ngoài các trường hợp hạ tầng đặc thù.

Pattern: thu hẹp binding bằng IAM Conditions

Cả hai mô hình đều có thể siết thêm bằng IAM Conditions — biểu thức CEL gắn vào binding, chỉ cho phép quyền có hiệu lực khi điều kiện đạt. Ví dụ giới hạn một principal KSA chỉ truy cập object trong một bucket có tiền tố tên nhất định, hoặc chỉ trong khung giờ làm việc:

bash
gcloud projects add-iam-policy-binding PROJECT_ID \
  --role="roles/storage.objectViewer" \
  --member="principal://iam.googleapis.com/.../subject/ns/NS/sa/KSA" \
  --condition='expression=resource.name.startsWith("projects/_/buckets/reports-"),title=only-reports-buckets'

Điều kiện này biến một role tưởng rộng (objectViewer toàn project) thành quyền hẹp đúng nhóm tài nguyên cần. Đây là công cụ mạnh để đạt least-privilege khi role có sẵn ở mức tài nguyên thô hơn mong muốn — đặc biệt hữu ích cho mô hình trực tiếp, nơi bạn muốn giữ ít thành phần nhưng vẫn cần granularity. Lưu ý IAM Conditions không hỗ trợ mọi cặp role/service như nhau; kiểm tài liệu Resource conditions trước khi dựa vào nó cho một ràng buộc bảo mật quan trọng.

Pattern: migration từ impersonation sang trực tiếp (không downtime)

Tổ chức muốn rời mô hình impersonation về mô hình trực tiếp (đơn giản hơn) có thể làm dual-binding để không gián đoạn:

  1. Thêm binding trực tiếp cho principal://...sa/KSA với đúng role tài nguyên — song song với cấu hình impersonation đang chạy. Lúc này KSA có quyền theo cả hai đường, nhưng runtime vẫn dùng GSA (vì annotation còn đó).
  2. Gỡ annotation iam.gke.io/gcp-service-account khỏi KSA và rollout lại workload. Từ giờ runtime dùng federated token trực tiếp (3 bước), quyền đến từ binding principal:// vừa thêm.
  3. Quan sát một chu kỳ (logs, error rate) xác nhận không còn 403.
  4. Dọn dẹp: gỡ binding workloadIdentityUser trên GSA, và nếu GSA không còn ai dùng, các role tài nguyên của nó. Cuối cùng xóa GSA nếu không còn mục đích.

Thứ tự này quan trọng: thêm quyền trước, đổi đường sau, dọn cuối — đảo thứ tự sẽ tạo cửa sổ workload mất quyền. Chiều ngược lại (trực tiếp → impersonation) hiếm khi đáng làm trừ khi gặp một role/tài nguyên không hỗ trợ principal binding trực tiếp.

Pattern: Workload Identity trên Autopilot

Trên GKE Autopilot, Workload Identity "is always enabled" — luôn bật, không thể tắt, và không có node pool nào để cấu hình GKE_METADATA (Google quản lý toàn bộ). Hệ quả thiết kế: trên Autopilot bạn không bao giờ phải lo "node pool còn ở GCE_METADATA" hay "Pod lấy token node SA" — vì không có node SA lộ ra cho workload. Autopilot enforce sẵn mô hình bảo mật mà Standard đòi bạn tự cấu hình đúng. Với workload mới ưu tiên bảo mật, đây là một lý do mạnh chọn Autopilot. Việc cấp quyền (principal binding hay impersonation) hoạt động y hệt Standard.

Annotation return-principal-id-as-email

Một chi tiết tích hợp: khi dùng impersonation, mặc định metadata server có thể trả về danh tính ở định dạng liên quan tới GSA. Để buộc trả về principal identifier thay vì email-style, thêm annotation:

yaml
metadata:
  annotations:
    iam.gke.io/return-principal-id-as-email: "true"

Cần khi hệ thống downstream kỳ vọng một định dạng danh tính cụ thể; phần lớn workload không cần đụng tới nó, nhưng biết nó tồn tại giúp giải thích các khác biệt định dạng danh tính trả về.

Common mistakes / anti-patterns

1. Trộn hai mô hình mà không biết. Annotate KSA trỏ GSA cũng bind role trực tiếp cho principal KSA. Vì sao xảy ra: copy cấu hình từ hai nguồn khác nhau. Hệ quả: quyền thực tế khó suy luận (có thể là hợp của cả hai), debug 403 rối loạn. Phòng tránh: chọn dứt khoát một mô hình cho mỗi KSA; tài liệu hóa mô hình chuẩn của tổ chức.

2. Nhiều KSA impersonate một GSA quyền rộng. Cấp workloadIdentityUser cho nhiều KSA trên cùng một GSA roles/editor. Vì sao xảy ra: tiện, "dùng chung một GSA cho cả namespace". Hệ quả: mọi workload đó có quyền editor; một workload bị chiếm = quyền editor toàn project; granularity biến mất. Phòng tránh: 1 KSA ↔ 1 GSA quyền tối thiểu, hoặc dùng mô hình trực tiếp.

3. Quên binding workloadIdentityUser (chỉ annotate). Annotate KSA trỏ GSA nhưng quên cấp roles/iam.workloadIdentityUser. Vì sao xảy ra: làm thiếu một trong hai phần của mô hình impersonation. Hệ quả: token lấy được nhưng impersonate thất bại → 403. Phòng tránh: nhớ impersonation cần cả hai phần — annotation và binding; kiểm cả hai khi debug.

4. Sai cú pháp member trong binding workloadIdentityUser. Dùng principal://... thay vì serviceAccount:PROJECT_ID.svc.id.goog[NS/KSA] cho binding workloadIdentityUser. Vì sao xảy ra: lẫn hai định dạng định danh. Hệ quả: binding không khớp, impersonate thất bại. Phòng tránh: nhớ binding workloadIdentityUser dùng cú pháp ngoặc vuông [NS/KSA]; binding role resource trực tiếp dùng principal://.

5. Cấp role cho KSA trong mô hình impersonation. Trong mô hình impersonation, cấp role resource (ví dụ storage.objectViewer) cho principal KSA thay vì cho GSA. Vì sao xảy ra: lẫn hai mô hình. Hệ quả: vì runtime dùng token của GSA (không phải principal KSA), role cấp cho KSA không có tác dụng → 403. Phòng tránh: trong impersonation, role resource cấp cho GSA; trong trực tiếp, cấp cho principal KSA.

GCP-native implementation guidance

bash
# ── Mô hình TRỰC TIẾP (khuyến nghị) ──
gcloud projects add-iam-policy-binding PROJECT_ID \
  --role="roles/secretmanager.secretAccessor" \
  --member="principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/subject/ns/NAMESPACE/sa/KSA_NAME" \
  --condition=None

# ── Mô hình IMPERSONATION ──
# 1. KSA annotate trỏ GSA
kubectl annotate serviceaccount KSA_NAME -n NAMESPACE \
  iam.gke.io/gcp-service-account=GSA@PROJECT_ID.iam.gserviceaccount.com
# 2. GSA tin KSA mạo danh
gcloud iam service-accounts add-iam-policy-binding GSA@PROJECT_ID.iam.gserviceaccount.com \
  --role="roles/iam.workloadIdentityUser" \
  --member="serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME]"
# 3. GSA có quyền resource thật
gcloud projects add-iam-policy-binding PROJECT_ID \
  --role="roles/secretmanager.secretAccessor" \
  --member="serviceAccount:GSA@PROJECT_ID.iam.gserviceaccount.com"

# Pod phải khai báo serviceAccountName = KSA (cả hai mô hình)
# spec.serviceAccountName: KSA_NAME

Kiểm tra binding của một GSA (mô hình impersonation) — xác nhận đúng KSA được phép mạo danh:

bash
gcloud iam service-accounts get-iam-policy GSA@PROJECT_ID.iam.gserviceaccount.com \
  --format=json | jq '.bindings[] | select(.role=="roles/iam.workloadIdentityUser")'

Operational implications

Hai mô hình IAM binding phản ánh một sự đánh đổi vận hành sâu hơn giữa đơn giảntập trung hóa quản lý quyền. Mô hình trực tiếp tối ưu cho đơn giản và minh bạch: quyền của một workload đọc được ngay từ IAM policy của resource, không cần lần qua annotation và GSA. Nhưng nó phân tán việc quản lý quyền ra theo từng KSA. Mô hình impersonation tối ưu cho tập trung hóa: quyền gom vào GSA, đội platform quản lý một tập GSA chuẩn, team chỉ cần "xin impersonate GSA X" — phù hợp tổ chức lớn có ranh giới rõ giữa platform team và app team. Đổi lại là thêm một lớp gián tiếp phải vận hành và audit.

Không có lựa chọn đúng tuyệt đối — có lựa chọn đúng cho ngữ cảnh tổ chức. Điều quan trọng nhất về mặt vận hành là nhất quán: chọn một mô hình làm chuẩn, tài liệu hóa, và enforce qua review/policy, để mọi kỹ sư debug theo cùng một mental model thay vì phải đoán mỗi KSA dùng mô hình nào. Một cluster trộn lẫn ngẫu nhiên hai mô hình là một cluster mà mọi sự cố 403 đều bắt đầu bằng câu hỏi "khoan, KSA này dùng mô hình nào" — chính xác sự mơ hồ mà file 07 (debugging) dạy cách loại trừ có hệ thống.

File 05 mở rộng cùng mental model authorization này ra ngoài GKE: khi danh tính không phải KSA mà là một GitHub repo, một AWS role, một Azure workload — Workload Identity Federation cho external IdP dùng cùng STS, cùng principal/principalSet, chỉ thay nguồn JWT.

References