AKS Microsoft Entra Workload ID
For new AKS workloads, use Microsoft Entra Workload ID. Do not use the deprecated Microsoft Entra pod-managed identity for new workloads.
Required components
- AKS OIDC issuer enabled.
- User-assigned managed identity for the workload.
- Federated identity credential on the managed identity.
- Kubernetes service account annotated with the managed identity client ID.
- Pod uses the annotated service account.
- Azure SDK uses
WorkloadIdentityCredentialor a constrained credential chain that includes workload identity.
Federated credential
oidc_issuer=$(az aks show \
--resource-group "$aks_resource_group" \
--name "$aks_name" \
--query "oidcIssuerProfile.issuerUrl" -o tsv)
az identity federated-credential create \
--name "fic-${namespace}-${service_account_name}" \
--identity-name "$identity_name" \
--resource-group "$identity_resource_group" \
--issuer "$oidc_issuer" \
--subject "system:serviceaccount:${namespace}:${service_account_name}" \
--audiences "api://AzureADTokenExchange"Kubernetes service account
apiVersion: v1
kind: ServiceAccount
metadata:
name: api
namespace: production
annotations:
azure.workload.identity/client-id: "<managed-identity-client-id>"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: production
spec:
template:
metadata:
labels:
azure.workload.identity/use: "true"
spec:
serviceAccountName: api
containers:
- name: api
image: <image>Bicep federated credential shape
resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' existing = {
name: identity_name
}
resource federated_credential 'Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials@2024-11-30' = {
name: 'fic-${namespace}-${service_account_name}'
parent: identity
properties: {
issuer: oidc_issuer_url
subject: 'system:serviceaccount:${namespace}:${service_account_name}'
audiences: [
'api://AzureADTokenExchange'
]
}
}Code
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
var credential = new WorkloadIdentityCredential();
var client = new SecretClient(new Uri("https://<vault>.vault.azure.net"), credential);Setup ordering (silent-failure trap)
Workload Identity token injection happens at pod admission via a mutating webhook. The order of operations matters, getting it wrong produces pods with no AZURE_CLIENT_ID / AZURE_FEDERATED_TOKEN_FILE env vars and they silently fall back to the kubelet identity (or fail with 403). Correct sequence:
- Enable OIDC issuer on the cluster (
--enable-oidc-issuer). - Enable Workload Identity on the cluster (
--enable-workload-identity). - Create the federated identity credential with subject
system:serviceaccount:<ns>:<sa>matching the EXACT namespace + service-account name. - Annotate the ServiceAccount (
azure.workload.identity/client-id) AND add the pod template label (azure.workload.identity/use: "true"). - Only then start or restart the pods, the webhook injects the projected token volume and env vars at admission.
If pods are already running before step 4, they will NOT pick up the annotation retroactively. You must restart them (kubectl rollout restart deployment/<d> -n <ns>). This is the most common silent failure when retrofitting Workload Identity onto an already-deployed workload, including operator-installed platforms like Drasi.
Common failures
| Failure | Likely cause | Fix |
|---|---|---|
| Token exchange fails | Issuer, subject, or audience mismatch | Compare FIC values to AKS OIDC issuer and service account |
| SDK uses managed identity IMDS path | Old SDK or wrong credential | Update SDK and use WorkloadIdentityCredential |
| Pod has no injected token | Missing label, service account annotation, or webhook | Check workload identity webhook and pod labels |
Azure 403 Forbidden |
Federation works but target role missing | Assign target data-plane role to managed identity principal |
Bicep error: principalId cannot be used in role assignment name |
principalId is a runtime value, not available at deployment start |
Use identity.id (known at plan time) in guid() for the role assignment name: guid(resource.id, identity.id, roleDefinitionId). Use identity.properties.principalId only in properties.principalId. |
| PostgreSQL "access token has invalid format" | Plain password sent to a server requiring Entra auth, or managed identity not registered as a PostgreSQL Entra admin | Add the managed identity as a PostgreSQL Flexible Server Entra admin: az postgres flexible-server microsoft-entra-admin create --resource-group <rg> --server-name <server> --type ServicePrincipal --object-id <principal-id> --display-name <display-name>. Set user in the source to the identity's display name; omit password. |
Drasi for Kubernetes source pods have no AZURE_CLIENT_ID env var |
Wrong service account annotated. Drasi creates a per-source SA named source.<source-name> (e.g. source.cosmos-matches), NOT default |
kubectl annotate sa source.<source-name> -n <drasi-namespace> azure.workload.identity/client-id=<clientId> --overwrite and kubectl label sa source.<source-name> -n <drasi-namespace> azure.workload.identity/use=true --overwrite. The federated credential subject must be system:serviceaccount:<drasi-namespace>:source.<source-name>. The SA does not exist until after drasi apply creates the source — annotate after apply, then restart source pods. Existing pods need a restart to pick up the change. |
Drasi EventHub source: ResourceNotFound for hub name |
Drasi uses Cypher node labels as Event Hub entity names | The Event Hub name MUST match the label in MATCH (m:Label). If queries use MATCH (m:Match), the hub must be named Match, not match-events. |
Drasi drasi init points at wrong cluster |
Stale environment config from a previous cluster | Run drasi env kube to register the current kubectl context as the Drasi environment before drasi init. |
| AKS Bicep kubelet identity AcrPull role fails | Role assignment targets wrong identity property | Use aksCluster.properties.identityProfile.kubeletIdentity.objectId for the role assignment principalId, NOT aksCluster.identity.principalId (which is the control-plane identity, not the kubelet). |