Workload Identity Federation từ AWS sang Azure — không cần client secret

Cho workload trên EC2, Lambda, ECS và EKS gọi Azure bằng token ngắn hạn: AWS IAM outbound identity federation, federated credential của Entra trên managed identity, lối tắt cho EKS, và mô hình hub-and-spoke.

Read in English →

Một service trên AWS cần đọc Azure Storage hoặc lấy secret từ Key Vault. Lối tắt quen thuộc là tạo app registration trên Entra với một client secret, cất vào AWS Secrets Manager rồi inject vào ứng dụng qua AZURE_CLIENT_SECRET. Secret đó hết hạn theo một lịch không ai nhớ, bị copy vào CI, và dùng được từ bất kỳ đâu trên internet.

Workload identity federation thay secret bằng một quan hệ tin cậy. Workload trên AWS đưa ra một token đã ký, chứng minh danh tính AWS của nó, và Microsoft Entra ID đổi token đó lấy access token Azure ngắn hạn. Không có secret nào tồn tại, nên không có gì để lộ.

Đây là bài “anh em” với Workload Identity Federation từ AWS sang Google Cloud. Hai cloud làm việc này rất khác nhau, và sự khác biệt đó quyết định toàn bộ phần bên dưới.

Vì sao Azure khác GCP

Google có sẵn AWS provider: nó nhận một request GetCallerIdentity đã ký và hỏi AWS xem bên gọi là ai. Entra ID không có cơ chế như vậy. Federated identity credential chỉ nhận token OIDC (JWT) từ một issuer có công bố discovery document và khoá ký. Vậy câu hỏi là: workload trên AWS lấy JWT ở đâu?

  • Pod trên EKS thì luôn có sẵn: OIDC issuer của cluster ký token cho Kubernetes service account.
  • Mọi thứ khác (EC2, Lambda, ECS, Glue) trước đây phải đi đường vòng, ví dụ dùng Cognito developer identity hoặc tự dựng một token broker. Từ khi AWS ra mắt IAM outbound identity federation, mọi IAM principal đều gọi được sts:GetWebIdentityToken để nhận JWT do AWS ký, với issuer URL riêng cho từng account.

Cơ chế trao đổi token

Workload AWS lấy JWT từ AWS STS, gửi tới Microsoft Entra ID làm client assertion, Entra xác minh bằng khoá của issuer AWS và federated credential, rồi trả access token Azure
Entra không bao giờ gọi AWS STS. Nó chỉ kiểm tra chữ ký của JWT bằng khoá public của issuer, rồi so iss, sub, aud với federated credential.
  1. Workload gọi sts:GetWebIdentityToken với audience api://AzureADTokenExchange. AWS STS trả về JWT có sub là ARN của IAM role (arn:aws:iam::444444444444:role/orders-reporter) và iss là issuer của account bạn (https://<id>.tokens.sts.global.api.aws).
  2. Workload gửi JWT tới token endpoint của Entra dưới dạng client assertion (grant_type=client_credentials, client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer).
  3. Entra tải /.well-known/openid-configuration và jwks.json của issuer để xác minh chữ ký.
  4. Entra tìm một federated identity credential (FIC) trên managed identity đích có issuer, subject, audience khớp chính xác với token.
  5. Entra trả access token Azure của managed identity đó.
  6. Workload gọi Azure. Nó được làm gì là do các role assignment Azure RBAC thông thường quyết định.

Hai cách lấy token

Path A dùng AWS IAM outbound identity federation từ mọi loại compute AWS với subject là ARN của role; path B dùng OIDC issuer của EKS với subject là Kubernetes service account
Cả hai đường đều dẫn tới cùng một điểm: một JWT có issuer, subject và audience khớp với federated credential.
Path A: IAM outbound federation Path B: OIDC issuer của EKS
Chạy trên EC2, Lambda, ECS, Glue, EKS, mọi thứ có IAM role Chỉ pod trên EKS
Danh tính Azure nhìn thấy ARN của IAM role system:serviceaccount:<ns>:<name>
Thiết lập phía AWS Bật outbound federation một lần cho mỗi account Không cần (issuer có sẵn)
Thay đổi ứng dụng Một hàm callback lấy token, hoặc một sidecar nhỏ Không (Azure SDK đọc file token)
Claim bổ sung org_id, principal tags, ARN của Lambda/EC2 namespace, service account, pod
Số FIC cần tạo một cho mỗi account + role một cho mỗi cluster + service account

Path A: IAM outbound identity federation

1. Phía AWS: bật outbound federation và cấp quyền cho role

aws iam enable-outbound-web-identity-federation
# trả về issuer riêng của account, ví dụ https://abc123-….tokens.sts.global.api.aws
aws iam get-outbound-web-identity-federation-info   # xem lại sau này

Chỉ cho role của workload xin token dành cho Azure, chỉ ký bằng RS256, và chỉ token ngắn hạn:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "sts:GetWebIdentityToken",
    "Resource": "*",
    "Condition": {
      "ForAllValues:StringEquals": { "sts:IdentityTokenAudience": "api://AzureADTokenExchange" },
      "StringEquals": { "sts:SigningAlgorithm": "RS256" },
      "NumericLessThanEquals": { "sts:DurationSeconds": 3600 }
    }
  }]
}

Hãy dùng RS256. AWS có thêm ES384, nhưng tài liệu của Entra ghi RS256 là thuật toán duy nhất đã được kiểm thử cho workload identity federation.

2. Phía Azure: managed identity kèm federated credential

Dùng user-assigned managed identity. Đây là một tài nguyên Azure bình thường, quản lý được bằng Terraform và RBAC, không cần app registration.

az identity create -g rg-identity -n id-orders-reporter

az identity federated-credential create \
  --name aws-prod-orders-reporter \
  --identity-name id-orders-reporter \
  --resource-group rg-identity \
  --issuer "https://abc123-def456-ghi789-jkl012.tokens.sts.global.api.aws" \
  --subject "arn:aws:iam::444444444444:role/orders-reporter" \
  --audiences "api://AzureADTokenExchange"

PRINCIPAL_ID=$(az identity show -g rg-identity -n id-orders-reporter --query principalId -o tsv)
az role assignment create \
  --assignee-object-id "$PRINCIPAL_ID" --assignee-principal-type ServicePrincipal \
  --role "Storage Blob Data Reader" \
  --scope "/subscriptions/<sub-id>/resourceGroups/rg-analytics/providers/Microsoft.Storage/storageAccounts/analyticsprod"

Subject là ARN của role, không phải ARN của session. Giá trị này không đổi giữa các session, các EC2 instance hay các lần gọi Lambda, đúng thứ mà quy tắc khớp chính xác của Entra cần.

3. Phía ứng dụng: hàm callback lấy token

ClientAssertionCredential của thư viện Azure Identity nhận một hàm trả về JWT mới. Cài hàm đó bằng AWS SDK, vốn đã biết cách lấy credentials của role trên EC2, Lambda, ECS và EKS (Pod Identity hoặc IRSA):

import boto3
from azure.identity import ClientAssertionCredential
from azure.storage.blob import BlobServiceClient

# GetWebIdentityToken không có trên STS global endpoint: phải chỉ định region
sts = boto3.client("sts", region_name="eu-west-1")

def aws_jwt() -> str:
    resp = sts.get_web_identity_token(
        Audience=["api://AzureADTokenExchange"],
        SigningAlgorithm="RS256",
        DurationSeconds=300,
    )
    return resp["WebIdentityToken"]

credential = ClientAssertionCredential(
    tenant_id="<entra-tenant-id>",
    client_id="<client-id của id-orders-reporter>",
    func=aws_jwt,  # được gọi mỗi khi Azure cần access token mới
)

blobs = BlobServiceClient("https://analyticsprod.blob.core.windows.net", credential=credential)

Hàm callback chỉ chạy khi Azure SDK cần access token mới, nên JWT sống 5 phút là đủ.

Cách không sửa code: dùng file token

Nếu không sửa được code, hãy ghi JWT ra file và để WorkloadIdentityCredential chuẩn (một phần của DefaultAzureCredential) đọc file đó. Cách này cần một sidecar trên ECS/EKS, hoặc một systemd timer trên EC2:

# vòng lặp làm mới (sidecar): token sống 1 giờ, làm mới mỗi 30 phút
while true; do
  aws sts get-web-identity-token --region eu-west-1 \
    --audience api://AzureADTokenExchange --signing-algorithm RS256 --duration-seconds 3600 \
    --query WebIdentityToken --output text > /var/run/azure/token.tmp \
  && mv /var/run/azure/token.tmp /var/run/azure/token
  sleep 1800
done
# biến môi trường của container ứng dụng
AZURE_TENANT_ID=<entra-tenant-id>
AZURE_CLIENT_ID=<client-id của id-orders-reporter>
AZURE_FEDERATED_TOKEN_FILE=/var/run/azure/token

Thử nhanh bằng shell

JWT=$(aws sts get-web-identity-token --region eu-west-1 \
  --audience api://AzureADTokenExchange --signing-algorithm RS256 \
  --query WebIdentityToken --output text)

curl -s "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token" \
  -d client_id=<client-id> \
  -d scope=https://storage.azure.com/.default \
  -d grant_type=client_credentials \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
  -d client_assertion="$JWT" | jq -r '.access_token | length'

Ra một con số dương nghĩa là Azure đã chấp nhận danh tính AWS của bạn.

Path B: pod EKS dùng OIDC issuer của cluster

Với EKS, bạn không cần outbound federation. Chỉ cần tin issuer của cluster, và cấp cho pod một service account token có audience của Azure:

ISSUER=$(aws eks describe-cluster --name prod-eu-1 \
  --query "cluster.identity.oidc.issuer" --output text)

az identity federated-credential create \
  --name eks-prod-eu-1-orders-reporter \
  --identity-name id-orders-reporter \
  --resource-group rg-identity \
  --issuer "$ISSUER" \
  --subject "system:serviceaccount:orders:orders-reporter" \
  --audiences "api://AzureADTokenExchange"
spec:
  serviceAccountName: orders-reporter
  containers:
  - name: app
    image: …/orders-reporter:1.8.0
    env:
    - { name: AZURE_TENANT_ID, value: "<entra-tenant-id>" }
    - { name: AZURE_CLIENT_ID, value: "<client-id của id-orders-reporter>" }
    - { name: AZURE_FEDERATED_TOKEN_FILE, value: /var/run/secrets/azure/tokens/azure-identity-token }
    volumeMounts:
    - { name: azure-token, mountPath: /var/run/secrets/azure/tokens, readOnly: true }
  volumes:
  - name: azure-token
    projected:
      sources:
      - serviceAccountToken:
          audience: api://AzureADTokenExchange
          expirationSeconds: 3600
          path: azure-identity-token

Mọi Azure SDK dùng DefaultAzureCredential đều tự nhận cấu hình này, không phải sửa code. Nếu không muốn tự viết phần volume, mutating webhook mã nguồn mở Azure Workload Identity cũng chạy được trên cluster không phải AKS, và inject đúng các biến môi trường cùng token như trên.

Đánh đổi: mỗi cluster có issuer riêng, nên cần một FIC cho mỗi cặp cluster và service account, trong khi mỗi identity chỉ chứa tối đa 20 FIC.

Hub-and-spoke: một issuer cho cả tổ chức

Với Path A, mỗi AWS account có một issuer riêng. Có 20 account, mỗi account vài role, thì bạn phải quản lý hàng chục FIC và bật outbound federation ở khắp nơi. Mô hình gọn hơn là dùng một AWS identity account trung tâm:

Workload account assume role trong một AWS identity account trung tâm, account duy nhất bật outbound federation; managed identity trên Azure chỉ tin một issuer đó
Chỉ identity account phát token cho Azure. Workload account đi qua đó bằng role chaining thông thường, và Azure chỉ tin một issuer.
  • Chỉ bật outbound federation ở identity account (999999999999). Đây là issuer duy nhất Azure tin.
  • Với mỗi workload, tạo một role ở đó, ví dụ azure-orders-reporter, có trust policy chỉ cho phép đúng role workload tương ứng ở account prod. Chỉ role này được gọi GetWebIdentityToken, với audience của Azure.
  • Workload AssumeRole sang role đó, xin JWT rồi đổi ở Entra. Mỗi managed identity trên Azure chỉ cần một FIC: issuer của identity account cộng ARN của role trung tâm.
  • Dùng SCP để chặn sts:GetWebIdentityToken ở mọi account khác, để không account nào âm thầm trở thành issuer được tin.

Bạn được một issuer duy nhất để tin, một nơi ghi CloudTrail cho mọi token cấp cho Azure, và một “công tắc” ngắt ngay từ phía AWS: xoá role trung tâm là quyền truy cập Azure mất ngay.

Cái giá là thêm một bước AssumeRole. Role chaining giới hạn session ở role trung tâm tối đa một giờ, nhưng không sao vì SDK tự làm mới.

Terraform (phía Azure)

resource "azurerm_user_assigned_identity" "orders_reporter" {
  name                = "id-orders-reporter"
  resource_group_name = "rg-identity"
  location            = "westeurope"
}

resource "azurerm_federated_identity_credential" "aws_hub" {
  name                = "aws-hub-orders-reporter"
  resource_group_name = "rg-identity"
  parent_id           = azurerm_user_assigned_identity.orders_reporter.id
  issuer              = "https://abc123-def456-ghi789-jkl012.tokens.sts.global.api.aws"
  subject             = "arn:aws:iam::999999999999:role/azure-orders-reporter"
  audience            = ["api://AzureADTokenExchange"]
}

resource "azurerm_role_assignment" "reports_reader" {
  scope                = azurerm_storage_account.analytics.id
  role_definition_name = "Storage Blob Data Reader"
  principal_id         = azurerm_user_assigned_identity.orders_reporter.principal_id
}

Những cái bẫy

  • Khớp chính xác, không có wildcard. issuer, subject, audience phải giống token từng ký tự. Subject sai vẫn tạo FIC thành công, và chỉ báo lỗi lúc đổi token.
  • Tối đa 20 FIC mỗi identity. Hãy dùng mô hình hub, hoặc tách identity theo từng workload.
  • Độ trễ lan truyền. FIC vừa tạo có thể trả lỗi AADSTS70021: No matching federated identity record found trong vài phút. Thêm cơ chế thử lại cho lần đổi token đầu tiên.
  • Tạo FIC tuần tự. Ghi song song vào cùng một identity sẽ bị 409 Conflict. Dùng provider azurerm bản mới, vì nó tự tạo tuần tự.
  • STS endpoint theo region. GetWebIdentityToken không có trên STS global endpoint. Luôn chỉ định region.
  • RS256 cho Azure. Ép buộc bằng condition sts:SigningAlgorithm.
  • Token ngắn hạn. Giữ DurationSeconds thấp khi dùng callback. Chỉ dùng tới 3600 cho cách dùng file token, và chặn trần bằng condition sts:DurationSeconds.

Xử lý sự cố

Lỗi Nguyên nhân thường gặp
AADSTS70021: No matching federated identity record found iss/sub/aud không khớp, hoặc FIC mới tạo chưa lan truyền xong
AADSTS700211: No matching federated identity record found for presented assertion issuer Issuer URL khác, ví dụ thừa dấu / cuối hoặc sai account
AADSTS700024: Client assertion is not within its valid time range JWT đã hết hạn. Lấy JWT mới cho mỗi lần đổi, hoặc làm mới file thường xuyên hơn.
AccessDenied khi gọi sts:GetWebIdentityToken Chưa bật outbound federation, sai audience hoặc thuật toán trong IAM condition, hoặc bị SCP chặn
Storage trả 403 AuthorizationPermissionMismatch Đổi token thành công nhưng thiếu role assignment RBAC, hoặc role assignment chưa có hiệu lực

Kết luận

Với Azure, federation luôn quy về một điều: một token OIDC có issuer, subject và audience khớp với federated credential. Pod trên EKS dùng được issuer của cluster mà không phải viết dòng code nào. Mọi thứ khác trên AWS giờ có thể lấy token đó từ sts:GetWebIdentityToken, với ARN của IAM role làm subject ổn định. Ở quy mô tổ chức, hãy phát token từ một AWS identity account duy nhất, để Azure chỉ phải tin đúng một issuer.

Nguồn

Bình luận