Workload Identity Federation from AWS to Azure — no client secrets
Let EC2, Lambda, ECS and EKS workloads call Azure with short-lived tokens: AWS IAM outbound identity federation, Entra federated credentials on managed identities, the EKS shortcut, and a hub-and-spoke design.

A service on AWS needs to read an Azure Storage account or a Key Vault secret. The usual shortcut is an Entra app registration with a client secret, stored in AWS Secrets Manager and injected as AZURE_CLIENT_SECRET. That secret expires on a schedule nobody remembers, gets copied into CI, and is valid from anywhere on the internet.
Workload identity federation replaces the secret with a trust relationship. The AWS workload presents a signed token that proves its AWS identity, and Microsoft Entra ID exchanges it for a short-lived Azure access token. No secret exists, so none can leak.
This is the Azure counterpart of Workload Identity Federation from AWS to Google Cloud. The two clouds work quite differently, and that difference shapes everything below.
Why Azure is different from GCP
Google has a native AWS provider: it accepts a signed GetCallerIdentity request and asks AWS who the caller is. Entra ID has no such thing. A federated identity credential only accepts an OIDC token (JWT) from an issuer that publishes a discovery document and signing keys. So the question becomes: where does an AWS workload get a JWT?
- EKS pods have always had one: the cluster’s OIDC issuer signs Kubernetes service account tokens.
- Everything else (EC2, Lambda, ECS, Glue) used to need workarounds, such as Cognito developer identities or a home-grown token broker. Since AWS launched IAM outbound identity federation, any IAM principal can call
sts:GetWebIdentityTokenand receive a JWT signed by AWS, with an issuer URL unique to your account.
How the exchange works
iss, sub and aud with a federated credential.- The workload calls
sts:GetWebIdentityTokenwith audienceapi://AzureADTokenExchange. AWS STS returns a JWT whosesubis the IAM role ARN (arn:aws:iam::444444444444:role/orders-reporter) and whoseissis your account’s issuer (https://<id>.tokens.sts.global.api.aws). - It sends the JWT to Entra’s token endpoint as a client assertion (
grant_type=client_credentials,client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer). - Entra fetches the issuer’s
/.well-known/openid-configurationandjwks.jsonand verifies the signature. - Entra looks for a federated identity credential (FIC) on the target managed identity whose
issuer,subjectandaudiencematch the token exactly. - Entra returns an Azure access token for that managed identity.
- The workload calls Azure. What it may do is decided by normal Azure RBAC role assignments.
Two ways to get the token
| Path A: IAM outbound federation | Path B: EKS OIDC issuer | |
|---|---|---|
| Works on | EC2, Lambda, ECS, Glue, EKS, anything with an IAM role | EKS pods only |
| Identity Azure sees | IAM role ARN | system:serviceaccount:<ns>:<name> |
| AWS setup | Enable outbound federation once per account | Nothing (the issuer already exists) |
| App change | A token callback, or a small sidecar | None (Azure SDK reads a token file) |
| Extra claims | org_id, principal tags, Lambda/EC2 ARN |
namespace, service account, pod |
| FIC granularity | one per account + role | one per cluster + service account |
Path A: IAM outbound identity federation
1. AWS: enable outbound federation and allow the role
aws iam enable-outbound-web-identity-federation
# returns the account-specific issuer, e.g. https://abc123-….tokens.sts.global.api.aws
aws iam get-outbound-web-identity-federation-info # read it again later
Allow the workload’s role to request tokens only for Azure, only RS256, and only short-lived ones:
{
"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 }
}
}]
}
Use RS256. AWS also offers ES384, but Entra documents RS256 as the only tested algorithm for workload identity federation.
2. Azure: a managed identity with a federated credential
Use a user-assigned managed identity. It’s an ordinary Azure resource, manageable with Terraform and RBAC, and needs no 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"
The subject is the role ARN, not a session ARN. It’s stable across sessions, EC2 instances and Lambda invocations, which is exactly what Entra’s exact-match rule needs.
3. The application: a token callback
The Azure Identity library’s ClientAssertionCredential accepts a function that returns a fresh JWT. Back it with the AWS SDK, which already knows how to get role credentials on EC2, Lambda, ECS and EKS (Pod Identity or IRSA):
import boto3
from azure.identity import ClientAssertionCredential
from azure.storage.blob import BlobServiceClient
# GetWebIdentityToken is not available on the global STS endpoint: use a 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 of id-orders-reporter>",
func=aws_jwt, # called whenever Azure needs a new access token
)
blobs = BlobServiceClient("https://analyticsprod.blob.core.windows.net", credential=credential)
The callback runs only when the Azure SDK needs a new access token, so a 5-minute JWT lifetime is plenty.
No-code alternative: a token file
If you can’t touch the code, write the JWT to a file and let the standard WorkloadIdentityCredential, which is part of DefaultAzureCredential, read it. This needs a sidecar container on ECS or EKS, or a systemd timer on EC2:
# refresh loop (sidecar): token valid 1h, refreshed every 30 min
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
# app container environment
AZURE_TENANT_ID=<entra-tenant-id>
AZURE_CLIENT_ID=<client-id of id-orders-reporter>
AZURE_FEDERATED_TOKEN_FILE=/var/run/azure/token
Test it from a 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'
A positive number means Azure accepted your AWS identity.
Path B: EKS pods with the cluster’s OIDC issuer
For EKS you don’t need outbound federation at all. Trust the cluster’s issuer and give the pod a service account token whose audience is Azure’s:
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 of 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
Any Azure SDK using DefaultAzureCredential picks this up with no code changes. If you’d rather not write the volume by hand, the open-source Azure Workload Identity mutating webhook also runs on non-AKS clusters and injects the same env vars and token.
The trade-off: every cluster has its own issuer, so you need one FIC per cluster and service account, and an identity holds at most 20 FICs.
Hub and spoke: one issuer for the whole organisation
With Path A, every AWS account has its own issuer. With twenty accounts and a few roles each, you’d be managing dozens of FICs and enabling outbound federation everywhere. A cleaner pattern is a central AWS identity account:
- Enable outbound federation only in the identity account (
999999999999). It becomes the single issuer Azure trusts. - For each workload, create a role there, such as
azure-orders-reporter, whose trust policy allows only the matching workload role in the prod account. Only that role may callGetWebIdentityToken, with the Azure audience. - Workloads
AssumeRoleinto it, request the JWT, and exchange it at Entra. Each Azure managed identity has one FIC: the identity account’s issuer plus the hub role’s ARN. - Use an SCP to deny
sts:GetWebIdentityTokeneverywhere else, so no other account can quietly become a trusted issuer.
You get one issuer to trust, a central CloudTrail record of every token handed to Azure, and an AWS-side kill switch: delete the hub role and the Azure access is gone.
The cost is one extra AssumeRole hop. Role chaining caps the hub session at one hour, which is fine because the SDK refreshes it.
Terraform (Azure side)
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
}
Gotchas
- Exact match, no wildcards.
issuer,subjectandaudiencemust match the token character for character. A wrong subject creates the FIC happily and fails only at token exchange time. - 20 FICs per identity. Plan the hub, or split identities per workload.
- Propagation delay. A fresh FIC can return
AADSTS70021: No matching federated identity record foundfor a few minutes. Add retries to the first exchange. - Create FICs sequentially. Parallel writes to the same identity return
409 Conflict. Use a recentazurermprovider, which serialises them. - Regional STS endpoint.
GetWebIdentityTokenisn’t served by the global STS endpoint. Always set a region. - RS256 for Azure. Enforce it with the
sts:SigningAlgorithmcondition. - Short tokens. Keep
DurationSecondslow for the callback path. Use ≤ 3600 only for the file path, and enforce the ceiling withsts:DurationSeconds.
Troubleshooting
| Error | Likely cause |
|---|---|
AADSTS70021: No matching federated identity record found |
iss/sub/aud mismatch, or a freshly created FIC still propagating |
AADSTS700211: No matching federated identity record found for presented assertion issuer |
Issuer URL differs, for example a trailing slash or the wrong account |
AADSTS700024: Client assertion is not within its valid time range |
JWT expired. Fetch a fresh one per exchange, or refresh the file more often. |
AccessDenied on sts:GetWebIdentityToken |
Outbound federation not enabled, wrong audience or algorithm in the IAM condition, or an SCP deny |
403 AuthorizationPermissionMismatch from Storage |
Token exchange worked, but the RBAC role assignment is missing or still propagating |
Wrap-up
For Azure, federation always comes down to one thing: an OIDC token whose issuer, subject and audience match a federated credential. EKS pods can use the cluster’s issuer with zero code. Everything else on AWS can now get that token from sts:GetWebIdentityToken, with the IAM role ARN as a stable subject. At organisation scale, issue tokens from a single AWS identity account and keep Azure trusting exactly one issuer.
Sources
- Federating AWS identities to external services: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_outbound.html
- Getting started with outbound identity federation: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_outbound_getting_started.html
- Outbound federation token claims: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_outbound_token_claims.html
- STS GetWebIdentityToken API: https://docs.aws.amazon.com/STS/latest/APIReference/API_GetWebIdentityToken.html
- Workload identity federation (Entra): https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation
- Create a federated identity credential: https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation-create-trust
- Federated identity credential considerations and limits: https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation-considerations
- OAuth 2.0 client credentials with a federated credential: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow



Comments