TL;DR
Workload Identity Federation (WIF) cho phép workload chạy trên AWS (EC2, Lambda, EKS, CodeBuild) xác thực với Google Cloud mà không cần Service Account Key JSON. AWS STS token được trao đổi qua Google STS để lấy access token ngắn hạn impersonate một Google Service Account, toàn bộ chuỗi có thời hạn tối đa 1 giờ.
Dùng WIF khi:
- Workload đa đám mây cần gọi BigQuery, GCS, Cloud Logging, Compute Engine, Vertex AI từ AWS.
- Bạn muốn loại bỏ hoàn toàn secret dài hạn khỏi image, filesystem, secret store CI/CD.
- Bạn cần audit trail truy vết được ARN gốc phía AWS, không chỉ Service Account phía GCP.
Cạm bẫy chính: Subject mismatch khi EC2 instance bị thay (Instance ID đổi) và attribute mapping quá rộng (mapping cả assumed-role/ROLE/* thay vì ARN cụ thể), cái sau biến WIF thành cửa sau nếu role đó dùng chung giữa nhiều workload.
Code mẫu và Terraform đầy đủ: github.com/vanhoangkha/workload-identity-federation-guide.
Dành cho ai
- Đối tượng: kỹ sư bảo mật cloud, kỹ sư nền tảng, SRE vận hành multi-cloud (AWS + GCP), hoặc đội data engineering đang chạy pipeline AWS → BigQuery.
- Cần biết trước: AWS IAM (Role, STS, Instance Metadata), khái niệm OIDC/OAuth cơ bản, gcloud CLI.
- Sau khi đọc bạn sẽ:
- Hiểu chính xác luồng token giữa AWS STS ↔ Google STS ↔ Service Account.
- Biết viết attribute mapping và condition để giới hạn chính xác ai được impersonate.
- Tránh được 5 lỗi phổ biến nhất khi thiết lập WIF lần đầu.
- Có sẵn Terraform tham chiếu để áp dụng trong production.
Bài này dài (~5500 từ). Nếu chỉ cần hướng dẫn nhanh, xem README_VI của repo.
Khái niệm
Phần đi sâu bắt đầu bằng từ vựng. WIF là câu chuyện ghép 3 hệ thống danh tính lại với nhau, lệch thuật ngữ là lệch mô hình tư duy.
- Service Account Key: Tệp JSON chứa RSA private key, cấp cho một Google Service Account. Vĩnh viễn cho đến khi thu hồi. Đây là cái WIF thay thế.
- Workload Identity Pool: Container logic trên Google Cloud đại diện cho một tập hợp identity bên ngoài được tin cậy. Mỗi cloud provider hoặc IdP bên ngoài thường là một pool.
- Workload Identity Pool Provider: Cấu hình cụ thể kiểu “tin AWS account 123456789012” hoặc “tin OIDC issuer https://token.actions.githubusercontent.com”. Một pool có thể có nhiều provider.
- STS (Security Token Service): Dịch vụ trao đổi token. AWS STS cấp token cho EC2/Lambda; Google STS (
sts.googleapis.com) nhận token ngoài và trả về Google Federated Token. - Federated Token: Access token ngắn hạn do Google STS cấp, gắn với identity bên ngoài, chưa phải Service Account. Dùng Federated Token để bước tiếp theo mới gọi
iamcredentials.generateAccessTokenimpersonate Service Account. - Service Account Impersonation: Cơ chế cho phép một principal (identity bên ngoài, user, hoặc SA khác) mạo danh một Service Account, nhận access token của SA đó có thời hạn ≤1h.
- Attribute mapping: Biểu thức (ngôn ngữ CEL) ánh xạ claims trong token ngoài (ví dụ
assertion.arncủa AWS) sang attribute Google dùng để định danh (google.subject,attribute.aws_role, v.v.). - Attribute condition: Biểu thức lọc ai được phép vào pool. Ví dụ chỉ cho ARN chứa prefix
assumed-role/prod-vào pool production. - Principal identifier: Chuỗi kiểu
principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL/subject/ARN_ĐẦY_ĐỦ. Đây là thứ bạn bind vào IAM policy của Service Account. - IMDSv2: Instance Metadata Service v2 của AWS (dựa trên session token). WIF hỗ trợ cả IMDSv1 và v2; dùng v2 mặc định.
Bối cảnh: vì sao Service Account Key là anti-pattern
Trước WIF, cách chuẩn để workload AWS gọi GCP API là tạo Google Service Account, generate key JSON, scp vào instance hoặc nhồi vào secret manager, rồi set GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json.
Cách này hỏng ở nhiều tầng:
1. Thông tin xác thực dài hạn. Key JSON không hết hạn. Thu hồi thủ công. Nếu lộ, kẻ tấn công có thời gian không giới hạn để khai thác, cho đến khi ai đó để ý và thu hồi.
2. Bề mặt tấn công chuỗi cung ứng. Key JSON lang thang qua rất nhiều nơi: laptop dev, artifact CI, layer Docker image, git commit bị đẩy nhầm, backup, Slack DM. Mỗi điểm là một bề mặt tấn công. Search GitHub public “BEGIN PRIVATE KEY” service_account, bạn sẽ thấy hàng trăm key bị lộ ngay trong 60 giây.
3. Audit trail đứt. Cloud Audit Logs của GCP ghi Service Account đã gọi API, nhưng không biết workload nào đã dùng key đó. Khi 3 EC2 instance cùng mount một key, ứng phó sự cố mù.
4. Không đạt chuẩn tuân thủ hiện đại. SOC2 Type II, PCI-DSS 4.0, ISO 27001:2022 đều ưu tiên thông tin xác thực ngắn hạn và rotation tự động. Key JSON vĩnh viễn kéo tụt posture.
5. Rotation là nỗi đau. Quy trình rotate key chuẩn: tạo key mới, triển khai song song, xác minh, thu hồi key cũ. Làm đúng mỗi quý. Thực tế: lần cuối nhóm bạn rotate là khi nào?
WIF xóa bỏ hoàn toàn lớp này. Không có key để rotate. Không có file để rò rỉ. Identity được suy ra từ metadata của chính workload (EC2 Instance Role, Lambda Execution Role, EKS ServiceAccount), không phải từ secret.
Đổi lại, bạn cần hiểu luồng token kỹ hơn, vì khi nó hỏng, thông điệp lỗi không hiển nhiên.
Kiến trúc
Luồng token end-to-end gồm 6 bước, đi qua cả AWS STS lẫn Google STS:
Chi tiết quan trọng từng bước:
- Bước 1-2 (AWS ký request): Workload gọi chính AWS STS endpoint với request
GetCallerIdentity, rồi ký request đó bằng AWS Signature V4. Kết quả là một signed request: chưa phải token. Signed request này được gửi sang Google STS. - Bước 2 (Google xác minh AWS): Google STS tự gọi lại AWS STS
GetCallerIdentitybằng chính signed request đó. Nếu AWS trả về ARN hợp lệ, Google coi như xác minh xong. Đây là lý do provider cần biếtAWS Account ID: để xác thực ARN trả về thuộc đúng account được tin cậy. - Bước 3 (Federated Token): Token này đại diện cho identity bên ngoài đã được ánh xạ, KHÔNG phải Service Account. Nó có ít quyền: chủ yếu chỉ đủ để gọi
iamcredentials.generateAccessToken. - Bước 4-5 (impersonation): Federated Token được dùng để impersonate SA cụ thể. Nếu identity bên ngoài không có IAM binding
workloadIdentityUser+serviceAccountTokenCreatortrên SA đó, bước này fail. - Bước 6: SA access token hành xử giống hệt khi app chạy trong GCP: mọi IAM policy của SA đều áp dụng.
Lưu ý kiến trúc: Google không bao giờ thấy AWS secret access key của bạn. Nó chỉ thấy signed request, và chỉ AWS STS mới mở khoá được identity. Đây là điểm mấu chốt khiến WIF bảo mật, Google có thể xác minh bạn là ai mà không cần chia sẻ secret material.
Thành phần cốt lõi
| Thành phần | Mục đích | Phạm vi quản lý |
|---|---|---|
| Workload Identity Pool | Namespace logic cho identity bên ngoài từ cùng nhóm (AWS account, GitHub org…) | Cấp project, GCP |
| Pool Provider | Cấu hình trust cho 1 loại IdP ngoài: AWS, OIDC, SAML | Trong phạm vi pool, GCP |
| Attribute Mapping | Ánh xạ claim ngoài → attribute Google dùng để phân quyền | Trong phạm vi provider |
| Attribute Condition | Lọc ai được vào pool (biểu thức CEL) | Trong phạm vi provider |
| Google Service Account | Identity thực gọi Google API, chủ sở hữu quyền | Cấp project |
IAM Binding workloadIdentityUser | Principal bên ngoài được phép impersonate SA | Service Account resource |
IAM Binding serviceAccountTokenCreator | Principal bên ngoài được phép cấp SA token | Service Account resource |
| AWS IAM Role | Identity workload AWS phía gốc, gắn với EC2 instance / Lambda / pod | AWS account |
| Credential Config JSON | File không phải secret trỏ workload tới đúng pool/provider/SA | Phân phối cùng workload |
Điểm dễ nhầm nhất: workloadIdentityUser và serviceAccountTokenCreator phải cấp cả hai, và cả hai đều bind trên Service Account, không phải project. Nhiều hướng dẫn trên internet chỉ cấp một, khiến request fail với “Permission denied” khó gỡ lỗi.
Triển khai từng bước
Mục này cô đặc lại 9 bước cần thiết. Mỗi bước đều có điểm cần lưu ý đó là giá trị so với tài liệu gốc.
Bước 1: Lấy thông tin AWS
Trên workload AWS (SSH vào EC2 hoặc chạy trong Lambda):
aws sts get-caller-identity
Output mẫu:
{
"UserId": "AROAXXXXXXXXXXXXXXXXX:i-0abc123def456",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/prod-data-sync-role/i-0abc123def456"
}
Lưu ý: ARN trên là assumed-role ARN (có Instance ID ở cuối), không phải IAM Role ARN gốc. Bạn cần ARN này, nguyên văn, để ánh xạ vào Google. Nhưng cũng vì vậy, khi EC2 instance bị thay, Instance ID đổi, ARN đổi, binding sẽ sai. Pattern đúng cho production là ánh xạ theo role (bỏ Instance ID), xem mục bảo mật ở dưới.
Bước 2: Bật API trên GCP
gcloud services enable \
iam.googleapis.com \
sts.googleapis.com \
iamcredentials.googleapis.com \
--project=PROJECT_ID
Thêm API của dịch vụ đích (BigQuery, GCS, Logging…) nếu workload sẽ dùng.
Lưu ý: iamcredentials.googleapis.com thường bị bỏ sót vì tên không trực quan. Không bật nó, bước impersonation (4-5 trong sơ đồ) sẽ fail.
Bước 3: Tạo Workload Identity Pool
gcloud iam workload-identity-pools create aws-pool \
--project=PROJECT_ID \
--location=global \
--display-name="AWS Workload Pool" \
--description="Pool for AWS-side workloads"
Lưu ý: --location luôn là global cho Workload Identity Pool ở thời điểm hiện tại. Đừng thử dùng region.
Bước 4: Tạo AWS Provider với attribute mapping chặt
gcloud iam workload-identity-pools providers create-aws aws-provider \
--project=PROJECT_ID \
--location=global \
--workload-identity-pool=aws-pool \
--account-id=123456789012 \
--attribute-mapping="google.subject=assertion.arn,attribute.aws_role=assertion.arn.extract('assumed-role/{role}/'),attribute.aws_account=assertion.account" \
--attribute-condition="assertion.arn.startsWith('arn:aws:sts::123456789012:assumed-role/prod-')"
Bóc tách:
google.subject=assertion.arn→ dùng ARN đầy đủ làm subject. Đây là cái bạn bind IAM.attribute.aws_role=assertion.arn.extract('assumed-role/{role}/')→ trích tên role để có thể bind theo role thay vì theo instance.attribute.aws_account=assertion.account→ tiện cho logging/analytics.attribute-condition→ chỉ cho ARN có prefixprod-vào pool. Đây là tầng phòng thủ đầu tiên.
Lưu ý: Nếu attribute condition fail, Google STS trả về lỗi unauthorized_client chung chung, không nói claim nào không khớp. Gỡ lỗi bằng cách log raw AWS ARN và thử condition trên playground CEL.
Bước 5: Tạo Google Service Account
gcloud iam service-accounts create aws-bigquery-reader \
--project=PROJECT_ID \
--display-name="BigQuery reader for AWS workloads"
Nguyên tắc: một Service Account cho một trường hợp sử dụng. Đừng dùng chung 1 SA cho cả đọc BigQuery, ghi GCS, và gọi Vertex AI. Khi bị xâm nhập, bạn muốn phạm vi ảnh hưởng nhỏ nhất có thể.
Bước 6: Cấp impersonation cho principal bên ngoài
Cấp theo role, không theo ARN đầy đủ:
PROJECT_NUMBER=$(gcloud projects describe PROJECT_ID --format='value(projectNumber)')
MEMBER="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/aws-pool/attribute.aws_role/prod-data-sync-role"
gcloud iam service-accounts add-iam-policy-binding \
aws-bigquery-reader@PROJECT_ID.iam.gserviceaccount.com \
--project=PROJECT_ID \
--role=roles/iam.workloadIdentityUser \
--member="${MEMBER}"
gcloud iam service-accounts add-iam-policy-binding \
aws-bigquery-reader@PROJECT_ID.iam.gserviceaccount.com \
--project=PROJECT_ID \
--role=roles/iam.serviceAccountTokenCreator \
--member="${MEMBER}"
Lưu ý 1: principalSet:// với attribute.aws_role/ bind theo role, nên khi EC2 instance bị thay, ARN mới (cùng role, Instance ID khác) vẫn được nhận. Nếu bind bằng principal:// với subject/ (ARN đầy đủ), bạn sẽ phải cập nhật binding mỗi lần autoscale, không khả thi.
Lưu ý 2: PROJECT_NUMBER là số 12 chữ số, không phải PROJECT_ID dạng chuỗi. Nhầm 2 cái này là lỗi số 1 khi thiết lập.
Lưu ý 3: IAM propagation mất 30-60 giây. Thử ngay sau khi bind thường fail với “Permission denied” do eventual consistency.
Bước 7: Cấp role cho Service Account trên tài nguyên đích
gcloud projects add-iam-policy-binding PROJECT_ID \
--role=roles/bigquery.dataViewer \
--member="serviceAccount:aws-bigquery-reader@PROJECT_ID.iam.gserviceaccount.com"
gcloud projects add-iam-policy-binding PROJECT_ID \
--role=roles/bigquery.jobUser \
--member="serviceAccount:aws-bigquery-reader@PROJECT_ID.iam.gserviceaccount.com"
Ưu tiên binding cấp dataset hoặc cấp table thay vì cấp project khi có thể. Ít quyền nhất áp cho cả Service Account, không chỉ identity bên ngoài.
Bước 8: Tạo credential config
gcloud iam workload-identity-pools create-cred-config \
projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/aws-pool/providers/aws-provider \
--service-account=aws-bigquery-reader@PROJECT_ID.iam.gserviceaccount.com \
--aws \
--enable-imdsv2 \
--output-file=gcp-credentials.json
File sinh ra có dạng:
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/aws-pool/providers/aws-provider",
"subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
"service_account_impersonation_url": "...",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"environment_id": "aws1",
"region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone",
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials",
"regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
"imdsv2_session_token_url": "http://169.254.169.254/latest/api/token"
}
}
File này không phải secret. Không có key material. Có thể commit vào git, nhồi vào Docker image, đưa vào config public. Xâm nhập file này một mình không làm gì được, kẻ tấn công vẫn cần chạy trong workload AWS có đúng IAM Role mới lấy được AWS STS token.
Bước 9: Kết nối vào workload
Đặt biến môi trường và cài thư viện:
export GOOGLE_APPLICATION_CREDENTIALS=/opt/gcp/gcp-credentials.json
pip install google-cloud-bigquery
Test cơ bản:
from google.cloud import bigquery
client = bigquery.Client(project="PROJECT_ID")
query = "SELECT corpus, COUNT(*) c FROM `bigquery-public-data.samples.shakespeare` GROUP BY corpus ORDER BY c DESC LIMIT 3"
for row in client.query(query).result():
print(row.corpus, row.c)
Nếu pass, bạn đã qua giai đoạn khó nhất.
Cấu hình tham khảo (Terraform)
Thiết lập cấp production nên đi qua Terraform. Đây là module cơ bản:
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
}
variable "project_id" { type = string }
variable "aws_account_id" { type = string }
variable "allowed_role_prefix" {
type = string
default = "prod-"
}
data "google_project" "current" {
project_id = var.project_id
}
resource "google_project_service" "required" {
for_each = toset([
"iam.googleapis.com",
"sts.googleapis.com",
"iamcredentials.googleapis.com",
"bigquery.googleapis.com",
])
project = var.project_id
service = each.value
}
resource "google_iam_workload_identity_pool" "aws" {
project = var.project_id
workload_identity_pool_id = "aws-pool"
display_name = "AWS Workload Pool"
description = "External identities from AWS account ${var.aws_account_id}"
}
resource "google_iam_workload_identity_pool_provider" "aws" {
project = var.project_id
workload_identity_pool_id = google_iam_workload_identity_pool.aws.workload_identity_pool_id
workload_identity_pool_provider_id = "aws-provider"
display_name = "AWS ${var.aws_account_id}"
aws {
account_id = var.aws_account_id
}
attribute_mapping = {
"google.subject" = "assertion.arn"
"attribute.aws_role" = "assertion.arn.extract('assumed-role/{role}/')"
"attribute.aws_account" = "assertion.account"
}
attribute_condition = "assertion.arn.startsWith('arn:aws:sts::${var.aws_account_id}:assumed-role/${var.allowed_role_prefix}')"
}
resource "google_service_account" "aws_bigquery_reader" {
project = var.project_id
account_id = "aws-bigquery-reader"
display_name = "BigQuery reader for AWS workloads"
}
locals {
principal_set = "principalSet://iam.googleapis.com/projects/${data.google_project.current.number}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.aws.workload_identity_pool_id}/attribute.aws_role/${var.allowed_role_prefix}data-sync-role"
}
resource "google_service_account_iam_binding" "workload_identity_user" {
service_account_id = google_service_account.aws_bigquery_reader.name
role = "roles/iam.workloadIdentityUser"
members = [local.principal_set]
}
resource "google_service_account_iam_binding" "token_creator" {
service_account_id = google_service_account.aws_bigquery_reader.name
role = "roles/iam.serviceAccountTokenCreator"
members = [local.principal_set]
}
resource "google_project_iam_member" "bq_data_viewer" {
project = var.project_id
role = "roles/bigquery.dataViewer"
member = "serviceAccount:${google_service_account.aws_bigquery_reader.email}"
}
resource "google_project_iam_member" "bq_job_user" {
project = var.project_id
role = "roles/bigquery.jobUser"
member = "serviceAccount:${google_service_account.aws_bigquery_reader.email}"
}
output "credential_config_command" {
value = <<-EOT
gcloud iam workload-identity-pools create-cred-config \
projects/${data.google_project.current.number}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.aws.workload_identity_pool_id}/providers/${google_iam_workload_identity_pool_provider.aws.workload_identity_pool_provider_id} \
--service-account=${google_service_account.aws_bigquery_reader.email} \
--aws \
--enable-imdsv2 \
--output-file=gcp-credentials.json
EOT
}
Chạy:
terraform apply
eval "$(terraform output -raw credential_config_command)"
Bạn sẽ có gcp-credentials.json sẵn sàng đóng gói theo workload.
Trường hợp sử dụng ngắn: 3 kịch bản đáng chú ý
README của repo bao phủ 8 kịch bản. Ba cái dưới đây đáng gọi tên riêng vì đều có trường hợp biên:
1. AWS Lambda → BigQuery. Lambda không có IMDS nhưng runtime tự tiêm thông tin xác thực qua biến môi trường AWS_ACCESS_KEY_ID/AWS_SESSION_TOKEN. Credential config JSON được đóng gói cùng package triển khai. Lưu ý: độ trễ cold-start thêm ~300-600ms do chuỗi 3 HTTP call (AWS STS → Google STS → IAM Credentials). Dùng ADC token cache để phân bổ đều chi phí.
2. Terraform trên AWS quản lý GCP. Đây là pattern vàng, pipeline CI/CD chạy trên AWS CodeBuild, dùng WIF để terraform apply tài nguyên GCP. Không cần lưu Google key trong Secrets Manager. IAM Role của CodeBuild được bind với SA có role roles/editor (hoặc chặt hơn), CodeBuild bị xâm nhập không làm lộ thông tin xác thực GCP vĩnh viễn.
3. EKS Pod → GCS. Đây là trường hợp sử dụng khó nhất. Pod có thể dùng:
- IRSA (IAM Roles for Service Accounts) của AWS để nhận AWS STS token, rồi nối chuỗi qua WIF sang GCP: pattern “double federation”.
- Hoặc dùng GKE Workload Identity Federation for GKE (tên tương tự nhưng khác hẳn) nếu chuyển sang GKE.
Với EKS + WIF, mount gcp-credentials.json qua Secret (file không phải secret nhưng mount theo cơ chế quen thuộc), đặt GOOGLE_APPLICATION_CREDENTIALS, xong.
Code đầy đủ cho cả 8 trường hợp sử dụng ở examples/ trong repo.
Cân nhắc bảo mật
WIF loại bỏ một lớp rủi ro (key dài hạn) nhưng mở ra lớp khác (cấu hình sai). Đây là những điểm cần chốt trong threat model.
1. Attribute condition là ranh giới tin cậy, không phải IAM binding
IAM binding chỉ chạy sau khi token đã được cấp. Nếu attribute condition quá rộng, hàng nghìn ARN khác trong cùng AWS account có thể đi qua Google STS thành công, rồi mới bị chặn ở tầng IAM. Cái này tạo nhiễu trong audit log và khiến phát hiện khó hơn.
Pattern tốt:
attribute.aws_role.startsWith("prod-") &&
attribute.aws_account == "123456789012"
Pattern xấu:
# Không có condition → bất kỳ ARN nào từ account 123456789012 cũng qua
2. Mapping theo role, bind theo role
Như đã nêu ở Bước 6: bind principalSet://.../attribute.aws_role/ROLE_NAME thay vì principal://.../subject/ARN_ĐẦY_ĐỦ. Lợi:
- EC2 tự động thay không làm vỡ binding.
- Bớt xáo trộn Terraform.
- Audit log vẫn có ARN đầy đủ (log ghi
subject), nên truy vết instance vẫn được.
3. Một SA cho mỗi trường hợp sử dụng
Nếu SA aws-bigquery-reader bị xâm nhập, kẻ tấn công chỉ đọc được BigQuery. Nếu bạn dùng chung SA cho cả BigQuery + GCS + Secret Manager, bạn mất nhiều hơn. Dùng quy ước đặt tên <source>-<service>-<verb>: aws-bq-reader, aws-gcs-writer, aws-logging-writer.
4. Kiểm toán cả hai cloud
Cấu hình:
- AWS CloudTrail: log call
sts:GetCallerIdentityphát sinh từ IP của Google (pattern: IP thuộc AS15169). Đây là bước 2 trong luồng, xác minh request. - GCP Cloud Audit Logs: bật Data Access logs cho Workload Identity Pool. Mỗi lần trao đổi token được log với AWS ARN đầy đủ.
- Cảnh báo: trao đổi token từ ARN không mong đợi, hoặc từ region không mong đợi (ánh xạ
assertion.region).
Công thức cơ bản: log sink Audit Logs đổ vào BigQuery dataset, join với bảng “expected ARNs”, cảnh báo khi khác.
5. Không lạm dụng IMDSv1
IMDSv2 có session token, chặn đánh cắp token qua SSRF. Credential config có cờ --enable-imdsv2, giữ nó. IMDSv1 chỉ nên dùng khi có lý do legacy rõ ràng (AMI cũ không hỗ trợ v2).
6. Cái Google không lo, bạn vẫn phải lo
| Google lo | Bạn lo |
|---|---|
| Xác minh AWS signature | IAM Role trên AWS không bị gán bừa |
| Cấp Federated Token ngắn hạn | Attribute mapping đúng với threat model |
| Áp dụng IAM binding trên SA | Cấp role tối thiểu cho SA |
| Log trao đổi token | Cảnh báo trên log, rà soát định kỳ |
| IMDS của AWS vẫn hoạt động | IMDSv2 bật trên mọi EC2 |
7. Phạm vi ảnh hưởng nếu AWS Role bị xâm nhập
Giả sử kẻ tấn công xâm nhập prod-data-sync-role (ví dụ SSRF kéo thông tin xác thực từ IMDS).
- Kẻ tấn công có tối đa 1 giờ trong mỗi cửa sổ token.
- Kẻ tấn công chỉ tiếp cận được những SA được bind cho role đó.
- Kẻ tấn công chỉ có quyền của SA đó, không phải full
roles/owner. - Audit log ghi đầy đủ ARN → nhóm ứng phó biết chính xác bắt đầu từ đâu.
So với SA key bị rò rỉ: không giới hạn thời gian, không giới hạn nguồn, không truy vết được caller. Khác biệt là đáng kể.
Vận hành và giám sát
Triển khai thành công chỉ là bước 1. Vận hành lâu dài cần mấy thứ sau:
Dashboard tối thiểu (Grafana hoặc Cloud Monitoring):
- Tỉ lệ trao đổi token theo provider, theo role.
- Tỉ lệ failure (
sts.googleapis.com/token.error). - Độ trễ p50/p95/p99 của chuỗi (đo phía client).
- Số AWS ARN duy nhất trong 24h: tăng đột biến = dấu hiệu sai binding hoặc bị tấn công.
Cảnh báo cần có:
attribute_condition_failedtăng bất thường: điều kiện đang chặn traffic đáng lẽ được cho qua.permission_deniedtăng bất thường trêniam.serviceAccounts.getAccessToken: thiếu binding hoặc role đã bị gỡ.- Trao đổi token từ ARN không nằm trong allowlist (join với kiểm kê).
Runbook 2h sáng:
- Query BigQuery fail với
401trên workload AWS. - Kiểm tra CloudTrail: có call
GetCallerIdentitytừ IP Google gần đây không? Nếu không, IMDS hoặc IAM Role AWS hỏng. - Kiểm tra Cloud Audit Logs
Token exchangecho pool: có entry không? Nếu không, file credential config hỏng. - Kiểm tra IAM binding trên SA: còn
workloadIdentityUser+serviceAccountTokenCreatorkhông? Nếu thiếu, apply lại Terraform. - Chờ 60s cho IAM propagation, retry.
Rà soát định kỳ:
- Hằng quý: rà SA nào không có access call trong 90 ngày → gỡ hoặc giảm quyền.
- Hằng quý: rà attribute condition: prefix role có drift không (nhóm đổi quy ước đặt tên)?
- Hằng năm: rotate Workload Identity Pool (tạo pool mới, chuyển đổi, xoá pool cũ): không bắt buộc nhưng tốt cho posture.
Truy nguyên thường gặp
Lỗi: “Permission ‘iam.serviceAccounts.getAccessToken’ denied on resource”
Nguyên nhân: Thiếu roles/iam.serviceAccountTokenCreator trên SA, hoặc IAM binding chưa propagate.
Kiểm tra:
gcloud iam service-accounts get-iam-policy \
aws-bigquery-reader@PROJECT_ID.iam.gserviceaccount.com
Phải thấy cả workloadIdentityUser và serviceAccountTokenCreator.
Sửa: Apply lại binding, chờ 60s.
Lỗi: “Invalid token: Unable to parse AWS token”
Nguyên nhân: Credential config dùng endpoint IMDSv1 nhưng EC2 đang áp dụng IMDSv2, hoặc ngược lại.
Kiểm tra:
# Kiểm tra IMDS hop limit và yêu cầu token
aws ec2 describe-instance-attribute \
--instance-id i-0abc \
--attribute metadataOptions
Sửa: Tạo lại credential config đúng với thiết lập IMDS của instance.
Lỗi: “Unauthorized client”
Nguyên nhân: Attribute condition fail. Thông điệp không nói rõ claim nào fail.
Kiểm tra: Dump aws sts get-caller-identity của workload → đối chiếu ARN với condition. Thử biểu thức CEL trên CEL playground.
Sửa: Chỉnh condition, hoặc đổi tên AWS role cho đúng quy ước.
Lỗi: “The caller does not have permission” khi query BigQuery
Nguyên nhân: SA đã cấp được token nhưng không có role bigquery.dataViewer/bigquery.jobUser trên dataset/project.
Kiểm tra:
gcloud projects get-iam-policy PROJECT_ID \
--flatten="bindings[].members" \
--filter="bindings.members:aws-bigquery-reader@"
Sửa: Cấp đúng role ở phạm vi đúng (dataset > project nếu có thể).
Lỗi: Subject mismatch sau sự kiện autoscaling
Nguyên nhân: Binding dùng principal://.../subject/<ARN_đầy_đủ> với Instance ID cụ thể. EC2 mới có Instance ID khác.
Sửa: Chuyển sang principalSet://.../attribute.aws_role/<role-name>. Xem Bước 6.
Đánh đổi và quyết định thiết kế
| Quyết định | Phương án A | Phương án B | Khuyến nghị |
|---|---|---|---|
| Phạm vi bind | ARN đầy đủ (subject) | Role (attribute.aws_role) | Role, sống chung với autoscaling |
| Số SA | 1 SA cho nhiều trường hợp sử dụng | 1 SA cho mỗi trường hợp sử dụng | Mỗi trường hợp một SA, phạm vi ảnh hưởng nhỏ |
| Attribute condition | Không có | Lọc theo prefix role | Có condition, phòng thủ theo chiều sâu |
| IMDS | v1 | v2 | v2, chặn SSRF |
| Vị trí credential config | Nhúng vào image | Mount runtime qua Secret | Mount runtime, xoay config không cần rebuild |
| Identity pool | 1 pool cho tất cả AWS account | 1 pool cho mỗi AWS account | Mỗi account một pool, cô lập rõ ràng |
| Thay thế | WIF | SA Key JSON | WIF, không có lý do để dùng key mới nữa |
Cân nhắc về hiệu năng: chuỗi WIF thêm 2-3 HTTP round-trip lúc lấy token lần đầu (~300-800ms tùy region). Google SDK cache token ~1h, nên chi phí phân bổ đều gần như bằng 0. Lambda cold-start là nơi duy nhất cảm nhận được, dùng provisioned concurrency hoặc khởi tạo token trong pha init.
Cân nhắc về multi-region: Workload Identity Pool luôn global. Call AWS STS được route tới endpoint regional (sts.ap-southeast-1.amazonaws.com) để giảm độ trễ. Credential config sinh bởi gcloud đã có pattern sts.{region}.amazonaws.com, không cần chỉnh.
Khi nào KHÔNG dùng WIF
- Workload chạy trong Google Cloud: dùng Workload Identity native của GKE, hoặc Service Account gắn vào GCE/Cloud Run. Không cần WIF.
- Service-to-service trong AWS: dùng IAM Role + AssumeRole, không cần nối chuỗi qua Google.
- Xác thực hướng người dùng: dùng OAuth/OIDC bình thường, WIF dành cho workload.
- Workload burst cực ngắn với yêu cầu độ trễ <50ms lần đầu: nếu cache token 1h không đủ phân bổ đều, cân nhắc kiến trúc khác (proxy tập trung chẳng hạn).
Tài liệu tham khảo
- Repo gốc với code và Terraform đầy đủ: github.com/vanhoangkha/workload-identity-federation-guide
- Google Cloud, Workload Identity Federation with other clouds
- Google Cloud, Best practices for using Workload Identity Federation
- Google Cloud, Manage attribute mappings and conditions
- AWS, IAM Roles for Amazon EC2
- AWS, Instance Metadata Service v2
- RFC 8693, OAuth 2.0 Token Exchange
- Bài liên quan:
- CSPM trên nhiều AWS Landing Zone: bổ sung góc độ multi-account AWS posture.
- Service tokens và mTLS cho non-human client: pattern non-human auth bên Cloudflare.