Most teams running workloads on AKS need to access Azure resources — Key Vault, Storage, Service Bus, SQL. The common shortcut is to create a service principal, export a client secret, and shove it into a Kubernetes secret. It works, but it introduces credentials that expire, need rotation, and can leak.
Microsoft Entra Workload Identity eliminates all of that. Your pod gets a short-lived, automatically rotated token — no secrets stored anywhere. Under the hood it uses OIDC federation: AKS acts as an identity provider, Entra ID trusts it, and your pod exchanges a projected service account token for an Azure access token at runtime.
This guide walks through the full setup — enabling the feature on AKS, wiring up a managed identity, and deploying a workload that reads from Azure Key Vault without a single stored credential.
How It Works
The flow is straightforward:
- AKS exposes an OIDC issuer URL — a public endpoint that signs Kubernetes service account tokens
- You create an Azure User-Assigned Managed Identity and a federated credential that says “trust tokens from this AKS cluster issued for this service account”
- Your pod’s service account is annotated with the managed identity’s client ID
- The Workload Identity webhook injects two things into your pod: a projected token volume and environment variables (
AZURE_CLIENT_ID,AZURE_TENANT_ID,AZURE_FEDERATED_TOKEN_FILE) - Azure SDK automatically picks up these variables and exchanges the token for an access token — zero code changes needed
Prerequisites
- AKS cluster (version 1.24+)
- Azure CLI authenticated (
az login) kubectlconfigured against your cluster- Helm 3.x installed
Step 1 — Enable OIDC Issuer and Workload Identity on AKS
If you’re creating a new cluster:
az aks create \
--resource-group <rg> \
--name <cluster> \
--enable-oidc-issuer \
--enable-workload-identity \
--generate-ssh-keys
For an existing cluster:
az aks update \
--resource-group <rg> \
--name <cluster> \
--enable-oidc-issuer \
--enable-workload-identity
Retrieve and store the OIDC issuer URL — you’ll need it when creating the federated credential:
OIDC_ISSUER=$(az aks show \
--resource-group <rg> \
--name <cluster> \
--query "oidcIssuerProfile.issuerUrl" -o tsv)
echo $OIDC_ISSUER
# https://<region>.oic.prod-aks.azure.com/<tenant-id>/<cluster-id>/
Step 2 — Create a User-Assigned Managed Identity
az identity create \
--resource-group <rg> \
--name nerdsolv-workload-identity
# Store the client ID and principal ID for later steps
CLIENT_ID=$(az identity show \
--resource-group <rg> \
--name nerdsolv-workload-identity \
--query clientId -o tsv)
PRINCIPAL_ID=$(az identity show \
--resource-group <rg> \
--name nerdsolv-workload-identity \
--query principalId -o tsv)
Step 3 — Create a Kubernetes Service Account
Create the namespace and service account. The annotation links the service account to the managed identity.
kubectl create namespace app-workloads
# serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: nerdsolv-sa
namespace: app-workloads
annotations:
azure.workload.identity/client-id: "<CLIENT_ID>"
labels:
azure.workload.identity/use: "true"
kubectl apply -f serviceaccount.yaml
Step 4 — Create the Federated Credential
This tells Entra ID: “trust tokens issued by this AKS OIDC endpoint for the nerdsolv-sa service account in the app-workloads namespace.”
az identity federated-credential create \
--name nerdsolv-fed-credential \
--identity-name nerdsolv-workload-identity \
--resource-group <rg> \
--issuer $OIDC_ISSUER \
--subject "system:serviceaccount:app-workloads:nerdsolv-sa" \
--audiences api://AzureADTokenExchange
Subject format:
system:serviceaccount:<namespace>:<service-account-name>— must match exactly what’s in Kubernetes.
Step 5 — Grant the Identity Access to Azure Resources
For this example we’ll give the identity read access to an Azure Key Vault.
KEY_VAULT_ID=$(az keyvault show \
--name <your-keyvault> \
--resource-group <rg> \
--query id -o tsv)
az role assignment create \
--role "Key Vault Secrets User" \
--assignee-object-id $PRINCIPAL_ID \
--assignee-principal-type ServicePrincipal \
--scope $KEY_VAULT_ID
Use
Key Vault Secrets Userfor read-only access. UseKey Vault Secrets Officerif the workload needs to write secrets.
Step 6 — Deploy a Workload
The key requirement on the pod: set azure.workload.identity/use: "true" on the pod spec and reference the annotated service account. The webhook handles the rest.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nerdsolv-app
namespace: app-workloads
spec:
replicas: 1
selector:
matchLabels:
app: nerdsolv-app
template:
metadata:
labels:
app: nerdsolv-app
azure.workload.identity/use: "true" # required
spec:
serviceAccountName: nerdsolv-sa # the annotated SA
containers:
- name: app
image: mcr.microsoft.com/azure-cli:latest
command: ["sleep", "infinity"]
env:
- name: KEYVAULT_URL
value: "https://<your-keyvault>.vault.azure.net"
kubectl apply -f deployment.yaml
Verify the webhook injected the identity environment variables:
kubectl exec -n app-workloads deploy/nerdsolv-app -- env | grep AZURE
# AZURE_CLIENT_ID=<client-id>
# AZURE_TENANT_ID=<tenant-id>
# AZURE_FEDERATED_TOKEN_FILE=/var/run/secrets/azure/tokens/azure-identity-token
# AZURE_AUTHORITY_HOST=https://login.microsoftonline.com/
Test Key Vault access from inside the pod:
kubectl exec -n app-workloads deploy/nerdsolv-app -- \
az keyvault secret show \
--vault-name <your-keyvault> \
--name my-secret \
--query value -o tsv
No login, no credentials — it just works.
Step 7 — Using the Identity in Application Code
Any Azure SDK that supports DefaultAzureCredential picks up the injected variables automatically. No code changes needed when moving between local development (where you use az login) and the cluster (where Workload Identity takes over).
Python:
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
credential = DefaultAzureCredential()
client = SecretClient(vault_url=os.environ["KEYVAULT_URL"], credential=credential)
secret = client.get_secret("my-secret")
Node.js:
import { DefaultAzureCredential } from "@azure/identity";
import { SecretClient } from "@azure/keyvault-secrets";
const credential = new DefaultAzureCredential();
const client = new SecretClient(process.env.KEYVAULT_URL, credential);
const secret = await client.getSecret("my-secret");
Go:
cred, _ := azidentity.NewDefaultAzureCredential(nil)
client, _ := azsecrets.NewClient(os.Getenv("KEYVAULT_URL"), cred, nil)
resp, _ := client.GetSecret(ctx, "my-secret", "", nil)
Troubleshooting
Pod not getting AZURE_ environment variables*
Check the webhook is running:
kubectl get pods -n kube-system | grep azure-wi-webhook
If missing, the cluster may not have --enable-workload-identity applied — run the az aks update command from Step 1.
Token exchange fails with AADSTS70021
The federated credential subject doesn’t match. Double-check: namespace and service account name in the --subject flag must match exactly what’s in Kubernetes.
az identity federated-credential create returns 409
A federated credential with that name already exists. Either delete it first or use a different name.
Summary
Workload Identity is the right way to give AKS workloads access to Azure resources. The setup is a one-time five-step process per identity: enable OIDC on AKS, create a managed identity, annotate a service account, create a federated credential, assign RBAC. After that, any pod using that service account gets automatic, secretless Azure authentication — and any Azure SDK using DefaultAzureCredential just works.
No secrets to rotate. No credentials to leak. No expiry surprises at 2am.
Was this article helpful?
Need help setting this up for your team?
nerdSolv designs and deploys cloud-native infrastructure on Azure, AWS, and GCP.
Talk to us →