Workload Identity trên Azure: Microsoft Entra Workload ID cho AKS, từ az CLI tới GitOps

Cho mỗi pod trên AKS một danh tính Azure riêng mà không cần client secret: cơ chế Entra Workload ID, triển khai bằng az CLI và Terraform, Key Vault CSI, vượt giới hạn 20 federated credential, và quản lý từ Helm chart của ứng dụng bằng Azure Service Operator.

Read in English →

Một pod trên AKS đọc Key Vault hay ghi vào Storage thì cần một danh tính Azure. Hai lối tắt quen thuộc là tạo app registration với client secret cất trong Kubernetes Secret, hoặc dùng kubelet managed identity của cluster mà mọi pod trên node dùng chung. Cách thứ nhất dễ lộ secret. Cách thứ hai khiến mọi workload có cùng một bộ quyền.

Microsoft Entra Workload ID cho mỗi Kubernetes ServiceAccount một danh tính Azure riêng, dựa trên OIDC federation chuẩn. Không có bí mật nào nằm trong cluster. Nó thay thế AAD Pod Identity cũ, vốn chặn lưu lượng IMDS bằng một daemon trên node và đã bị deprecate.

Bài này khép lại loạt bài: EKS Pod Identity vs IRSA cho AWS, Workload Identity trên GKE cho GCP, cùng hai bài liên cloud AWS → GCP và AWS → Azure.

Cơ chế hoạt động

Webhook của AKS inject biến môi trường và projected token do OIDC issuer của cluster ký; SDK đổi token ở Entra ID, Entra khớp federated credential trên managed identity rồi trả access token
Cluster đóng vai trò issuer phát token. Mỗi federated credential cho phép Entra ID tin issuer đó với đúng một cặp namespace và service account.
  1. Một mutating webhook thấy pod có label azure.workload.identity/use: "true" và inject các biến AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_AUTHORITY_HOST, AZURE_FEDERATED_TOKEN_FILE cùng một projected service account token.
  2. Token được OIDC issuer của cluster ký, với audience api://AzureADTokenExchange và subject system:serviceaccount:orders:orders-api.
  3. Azure Identity SDK gửi token đó tới Microsoft Entra ID. Entra tải khoá public của issuer và xác minh chữ ký.
  4. Entra tìm một federated identity credential (FIC) trên user-assigned managed identity có issuer, subject và audience khớp chính xác.
  5. Entra trả access token của identity đó, và Azure RBAC quyết định identity được làm gì.

So với GKE: trên GKE, chính Kubernetes ServiceAccount là principal được cấp role. Trên Azure thì luôn có một managed identity đứng giữa, cộng với một FIC cho mỗi cặp cluster và service account.

Bước 1: bật trên cluster

az aks update -g rg-aks-prod -n aks-weu-1 \
  --enable-oidc-issuer \
  --enable-workload-identity

AKS_OIDC_ISSUER=$(az aks show -g rg-aks-prod -n aks-weu-1 \
  --query "oidcIssuerProfile.issuerUrl" -o tsv)
# https://westeurope.oic.prod-aks.azure.com/<tenant-id>/<uuid>/

Cluster AKS Automatic đã bật sẵn cả hai, có thể bỏ qua bước này.

Bước 2: identity, federated credential, phân quyền

az identity create -g rg-orders-prod -n id-orders-api
CLIENT_ID=$(az identity show -g rg-orders-prod -n id-orders-api --query clientId -o tsv)
PRINCIPAL_ID=$(az identity show -g rg-orders-prod -n id-orders-api --query principalId -o tsv)

az identity federated-credential create \
  --name fic-aks-weu-1 \
  --identity-name id-orders-api \
  --resource-group rg-orders-prod \
  --issuer "$AKS_OIDC_ISSUER" \
  --subject system:serviceaccount:orders:orders-api \
  --audience api://AzureADTokenExchange

az role assignment create \
  --assignee-object-id "$PRINCIPAL_ID" --assignee-principal-type ServicePrincipal \
  --role "Key Vault Secrets User" \
  --scope "$(az keyvault show -n kv-orders-prod --query id -o tsv)"
  • Key Vault phải dùng mô hình phân quyền Azure RBAC (--enable-rbac-authorization) thì role assignment mới có tác dụng.
  • Tạo role assignment cần quyền Role Based Access Control Administrator, User Access Administrator hoặc Owner. Contributor không làm được.
  • FIC mới cần vài giây để lan truyền. RBAC assignment mới có thể mất tới 10 phút.

Bước 3: ServiceAccount và pod

apiVersion: v1
kind: ServiceAccount
metadata:
  name: orders-api
  namespace: orders
  annotations:
    azure.workload.identity/client-id: "<CLIENT_ID>"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders-api
  namespace: orders
spec:
  selector:
    matchLabels: { app: orders-api }
  template:
    metadata:
      labels:
        app: orders-api
        azure.workload.identity/use: "true"   # bắt buộc, đặt trên POD template
    spec:
      serviceAccountName: orders-api
      containers:
      - name: app
        image: crordersprod.azurecr.io/orders-api:1.8.0

Label azure.workload.identity/use: "true" là bắt buộc và phải nằm trên pod template. Chỉ pod có label mới được webhook xử lý, gắn lên ServiceAccount không có tác dụng. Nếu sau này đổi annotation của ServiceAccount, hãy restart pod.

Các annotation hữu ích khác:

Đặt ở Annotation Tác dụng
ServiceAccount azure.workload.identity/tenant-id tenant của identity, nếu khác tenant của cluster
SA hoặc pod azure.workload.identity/service-account-token-expiration thời hạn projected token, 3600–86400 giây (mặc định 3600)
Pod azure.workload.identity/skip-containers c1;c2: các container không nhận token
Pod azure.workload.identity/inject-proxy-sidecar sidecar chặn IMDS, chỉ là cầu nối tạm cho ứng dụng còn dùng SDK cũ

Bước 4: code ứng dụng

Dùng DefaultAzureCredential, nó sẽ thử WorkloadIdentityCredential dựa trên các biến môi trường đã được inject. Không có dòng code nào dành riêng cho Kubernetes:

import os
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient

client = SecretClient(vault_url=os.environ["KEYVAULT_URL"], credential=DefaultAzureCredential())
db_password = client.get_secret("db-password").value

Phiên bản SDK tối thiểu: Python azure-identity 1.13.0, .NET Azure.Identity 1.9.0, Java 1.9.0, Go azidentity 1.3.0, Node @azure/identity 3.2.0. Hai chi tiết hay làm người ta vấp:

  • Xin scope theo dạng v2, https://management.azure.com/.default, không dùng resource URI thô.
  • Không hard-code đường dẫn token. Hãy đọc từ AZURE_FEDERATED_TOKEN_FILE, các SDK đã làm sẵn việc này.

Kiểm tra

kubectl -n orders run wi-test --rm -it --restart=Never \
  --image=ghcr.io/azure/azure-workload-identity/msal-go \
  --overrides='{"metadata":{"labels":{"azure.workload.identity/use":"true"}},"spec":{"serviceAccountName":"orders-api"}}' \
  --env KEYVAULT_URL=https://kv-orders-prod.vault.azure.net/ --env SECRET_NAME=db-password
# "successfully got secret"

Secret dạng file: Key Vault CSI driver

Nếu ứng dụng muốn nhận secret dưới dạng file hoặc biến môi trường thay vì tự gọi Key Vault, Secrets Store CSI driver dùng được chính workload identity đó:

az aks enable-addons -g rg-aks-prod -n aks-weu-1 --addons azure-keyvault-secrets-provider
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: orders-kv
  namespace: orders
spec:
  provider: azure
  parameters:
    usePodIdentity: "false"
    clientID: "<CLIENT_ID>"          # chính workload identity
    keyvaultName: kv-orders-prod
    tenantId: "<TENANT_ID>"
    objects: |
      array:
        - |
          objectName: db-password
          objectType: secret

Mount vào pod bằng một volume csi (driver: secrets-store.csi.k8s.io, volumeAttributes.secretProviderClass: orders-kv). Pod phải chạy với cùng ServiceAccount đó.

Terraform

resource "azurerm_kubernetes_cluster" "weu1" {
  # …
  oidc_issuer_enabled       = true
  workload_identity_enabled = true
}

resource "azurerm_user_assigned_identity" "orders_api" {
  name                = "id-orders-api"
  resource_group_name = "rg-orders-prod"
  location            = "westeurope"
}

resource "azurerm_federated_identity_credential" "orders_api_weu1" {
  name                = "fic-aks-weu-1"
  resource_group_name = "rg-orders-prod"
  parent_id           = azurerm_user_assigned_identity.orders_api.id
  issuer              = azurerm_kubernetes_cluster.weu1.oidc_issuer_url
  subject             = "system:serviceaccount:orders:orders-api"
  audience            = ["api://AzureADTokenExchange"]
}

resource "azurerm_role_assignment" "orders_api_kv" {
  scope                = azurerm_key_vault.orders.id
  role_definition_name = "Key Vault Secrets User"
  principal_id         = azurerm_user_assigned_identity.orders_api.principal_id
}

Nhiều cluster: giới hạn 20 FIC

Mỗi cluster có issuer riêng, nên cùng một ứng dụng chạy trên nhiều cluster cần một FIC cho mỗi cluster trên identity của nó, trong khi mỗi identity chỉ chứa tối đa 20 FIC.

Direct federation cần một federated credential cho mỗi cluster, tối đa 20; identity bindings (preview) cho nhiều cluster dùng chung một identity qua một federated credential
Direct federation đã GA và dùng được ở mọi nơi. Identity bindings bỏ được giới hạn theo cluster, nhưng chỉ có trên AKS và vẫn đang preview.
  • Mỗi ứng dụng chạy trên dưới khoảng 20 cluster: mỗi cluster một FIC, tự động hoá bằng Terraform. Tạo FIC tuần tự, vì ghi song song vào cùng một identity sẽ bị 409 Conflict.
  • Fleet lớn: cân nhắc identity bindings (preview). Nhiều cluster dùng chung một managed identity qua một FIC duy nhất. Token của binding dùng audience api://AKSIdentityBinding, nên đừng dùng lại file token đó cho direct federation, nếu không sẽ gặp lỗi AADSTS700212.
  • Cần cô lập chặt: mỗi cluster hoặc mỗi môi trường một identity cũng là lựa chọn hợp lý, giúp giới hạn phạm vi ảnh hưởng khi có sự cố.

GitOps: để chart của ứng dụng sở hữu danh tính Azure

Tới giờ mọi thứ đều là lệnh az hoặc một repo Terraform riêng. Azure Service Operator v2 (ASO) là phiên bản Azure của ACK và Config Connector. Nó quản lý tài nguyên Azure dưới dạng object Kubernetes, nhờ đó Helm chart của ứng dụng có thể tự khai báo identity, FIC và role assignment của mình.

Helm chart khai báo UserAssignedIdentity, FederatedIdentityCredential và RoleAssignment; ASO ghi clientId và principalId vào ConfigMap, RoleAssignment và Deployment đọc từ ConfigMap đó
ASO ghi ID của identity vào ConfigMap. Role assignment và pod đọc từ đó, nên không ai phải dán GUID vào values.yaml.

Bản thân ASO cũng xác thực bằng workload identity: cài với --set useWorkloadIdentityAuth=true (cần có cert-manager), và cấp cho identity của ASO quyền trên các resource group nó quản lý. Sau đó chart chứa:

# templates/azure/identity.yaml
apiVersion: managedidentity.azure.com/v1api20230131
kind: UserAssignedIdentity
metadata:
  name: {{ include "orders-api.fullname" . }}
spec:
  location: {{ .Values.azure.location }}
  owner:
    armId: /subscriptions/{{ .Values.azure.subscriptionId }}/resourceGroups/{{ .Values.azure.resourceGroup }}
  operatorSpec:
    configMaps:
      clientId:
        name: {{ include "orders-api.fullname" . }}-identity
        key: clientId
      principalId:
        name: {{ include "orders-api.fullname" . }}-identity
        key: principalId
---
apiVersion: managedidentity.azure.com/v1api20230131
kind: FederatedIdentityCredential
metadata:
  name: {{ include "orders-api.fullname" . }}-{{ .Values.azure.clusterName }}
spec:
  owner:
    name: {{ include "orders-api.fullname" . }}
  issuer: {{ .Values.azure.oidcIssuer }}
  subject: system:serviceaccount:{{ .Release.Namespace }}:{{ include "orders-api.serviceAccountName" . }}
  audiences:
  - api://AzureADTokenExchange
---
apiVersion: authorization.azure.com/v1api20220401
kind: RoleAssignment
metadata:
  name: {{ include "orders-api.fullname" . }}-blob-reader
spec:
  owner:
    armId: {{ .Values.azure.storageAccountId }}    # phạm vi của assignment
  principalIdFromConfig:
    name: {{ include "orders-api.fullname" . }}-identity
    key: principalId
  roleDefinitionReference:
    wellKnownName: Storage Blob Data Reader

Deployment đọc client ID từ cùng ConfigMap đó. Webhook chỉ thêm những biến môi trường chưa có, nên ServiceAccount thậm chí không cần annotation client-id:

# templates/deployment.yaml (trích)
template:
  metadata:
    labels:
      azure.workload.identity/use: "true"
  spec:
    serviceAccountName: {{ include "orders-api.serviceAccountName" . }}
    containers:
    - name: app
      env:
      - name: AZURE_CLIENT_ID
        valueFrom:
          configMapKeyRef:
            name: {{ include "orders-api.fullname" . }}-identity
            key: clientId

Ở lần deploy đầu, pod chờ ở trạng thái CreateContainerConfigError cho tới khi ASO tạo xong identity và ghi ConfigMap, rồi tự khởi động. Đây đúng là pattern trong ví dụ DATABASE_URL ở bài ACK. Muốn giữ lại identity khi release bị xoá, gắn annotation serviceoperator.azure.com/reconcile-policy: detach-on-delete.

Những cái bẫy

  • Label đặt trên pod, không phải ServiceAccount. Pod template thiếu azure.workload.identity/use: "true" thì không có gì được inject.
  • Đổi annotation phải restart pod. Webhook chỉ xử lý lúc pod được tạo.
  • Khớp chính xác. Issuer (kể cả dấu / ở cuối), system:serviceaccount:<ns>:<name> và audience phải giống FIC từng ký tự. Không hỗ trợ wildcard.
  • Độ trễ lan truyền. FIC mới mất vài giây, RBAC assignment mới có thể mất tới 10 phút. Hãy có cơ chế thử lại lúc khởi động thay vì coi các lỗi 403 ban đầu là thất bại.
  • Đừng dùng kubelet identity cho ứng dụng. Giữ identity của node ở mức tối thiểu, và cho mỗi workload một managed identity riêng.
  • Không hỗ trợ virtual nodes, và ở một vài region không tạo được FIC trên identity. Hãy kiểm tra trước khi thiết kế dựa trên chúng.
  • Token Entra sống tới 24 giờ và không phụ thuộc vào thời hạn của token Kubernetes. Muốn thu hồi quyền thì xoá RBAC assignment hoặc FIC, đừng chờ projected token hết hạn.

Xử lý sự cố

Lỗi Nguyên nhân thường gặp
AADSTS70021: No matching federated identity record found Subject hoặc audience không khớp, hoặc FIC mới tạo chưa lan truyền xong
AADSTS700211: … presented assertion issuer Issuer không khớp: URL của cluster khác, hoặc thiếu dấu / ở cuối
AADSTS700212: … presented assertion audience Audience của token không phải api://AzureADTokenExchange, ví dụ token của identity binding
Pod không có các biến môi trường Pod template thiếu label azure.workload.identity/use, hoặc webhook chưa được bật
Key Vault hoặc Storage trả 403 Chưa có RBAC assignment, Key Vault vẫn dùng access policy, hoặc đang có độ trễ lan truyền

Kết luận: cùng một ý tưởng trên ba cloud

AWS (EKS) GCP (GKE) Azure (AKS)
Cơ chế gốc Pod Identity / IRSA Workload Identity Federation for GKE Entra Workload ID
Ai được cấp quyền IAM role chính principal của KSA (hoặc GSA liên kết) user-assigned managed identity
Liên kết với KSA Pod Identity association không cần (trực tiếp) / annotation trên KSA FIC + client-id + label trên pod
IaC kiểu Kubernetes ACK Config Connector Azure Service Operator

Trên AKS, công thức là: bật OIDC issuer và Workload ID, cho mỗi workload một user-assigned managed identity với một federated credential cho mỗi cluster, gắn label cho pod, và để DefaultAzureCredential lo phần còn lại. Khi đã chạy ổn, hãy đưa identity, FIC và role assignment vào Helm chart của ứng dụng bằng Azure Service Operator, để quyền truy cập được review, đánh version và xoá cùng với đoạn code cần nó.

Nguồn

Bình luận