Identity Veteran Patterns
Hard-won managed identity and workload identity patterns.
Workload Identity Federation: the #1 silent failure
The ordering trap
When setting up Workload Identity on AKS or ACA, the sequence matters:
1. Enable OIDC issuer on cluster
2. Create managed identity
3. Create federated credential (subject = system:serviceaccount:<ns>:<sa>)
4. Annotate service account with client-id
5. Label service account with use=true
6. Deploy workload (pod picks up identity)Common mistakes:
| Mistake | Symptom | Fix |
|---|---|---|
| Annotate SA before OIDC enabled | Pod gets kubelet identity | Enable OIDC issuer first |
| Wrong SA name in federated credential | AADSTS700213: No matching federated identity |
Verify system:serviceaccount:<ns>:<sa> matches exactly |
Missing azure.workload.identity/use=true label |
Pod doesn't get workload identity env vars | Add label to SA |
| SA doesn't exist when creating FIC | NotFound error |
Create SA first (e.g., deploy a pod that uses it) |
The AKS per-resource SA gotcha
Drasi and other operators create per-resource service accounts:
source.<name>(notdefault)reaction.<name>(notdefault)
The federated credential subject must match the actual SA name, not default.
# Verify SA exists before creating FIC
kubectl get sa -n <namespace> source.<name> -o yaml
# Then create FIC with exact subject
az identity federated-credential create \
--name <name> \
--identity-name <mi-name> \
--resource-group <rg> \
--issuer <oidc-issuer-url> \
--subject "system:serviceaccount:<namespace>:source.<name>"Managed identity assignment order
Wrong order (fails silently)
# Pod starts before MI is assigned → gets kubelet identity
kubectl apply -f pod.yaml
az identity assign --name my-identity --resource-group my-rg --scope <scope>Right order
# 1. Create identity
az identity create --name my-identity --resource-group my-rg
# 2. Assign roles
az role assignment create --assignee <principal-id> --role "Key Vault Secrets User" --scope <vault>
# 3. Deploy workload (pod now gets correct identity)
kubectl apply -f pod.yamlCredential rotation patterns
Entra ID secret rotation
# Create new secret
az ad app credential reset --id <app-id> --append --credential-description "new-secret"
# Verify new secret works
az account get-access-token --client-id <client-id> --client-secret <new-secret>
# Delete old secret (after verification window)
az ad app credential delete --id <app-id> --key-id <old-key-id>Managed identity doesn't rotate (that's the point)
Managed identities eliminate credential rotation entirely:
- System-assigned: lifecycle tied to resource
- User-assigned: lifecycle independent, but no secret to rotate
- Prefer user-assigned when the identity must be shared across resources or survive resource recreation; for single-resource workloads, follow the default decision table in SKILL.md (system-assigned)
SQL connection patterns
Azure SQL + Entra ID
// BAD: SQL auth (connection string with password)
var conn = new SqlConnection("Server=...;Database=...;User Id=...;Password=...");
// GOOD: Managed identity (no password in connection string)
var conn = new SqlConnection("Server=...;Database=...;Authentication=Active Directory Managed Identity;");
conn.AccessToken = await credential.GetTokenAsync(new TokenRequestContext(
new[] { "https://database.windows.net/.default" }));Connection pooling with identity
// Connection pool key includes access token
// When token expires, old connections are invalid
// Solution: Refresh token before expiry
private static async Task<string> GetAccessTokenAsync()
{
var token = await credential.GetTokenAsync(new TokenRequestContext(
new[] { "https://database.windows.net/.default" }));
return token.Token;
}GitHub Actions OIDC
The token audience gotcha
# WRONG: Missing audience — token rejected by Azure
- uses: azure/login@v3
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
# RIGHT: Explicit audience
- uses: azure/login@v3
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
audience: api://AzureADTokenExchangeFederated credential subject for GitHub Actions
# Repo-level
repo:<owner>/<repo>:ref:refs/heads/<branch>
# PR-level
repo:<owner>/<repo>:pull_request
# Environment-level
repo:<owner>/<repo>:environment:<env-name>Rule: Scope the subject as narrowly as possible. repo:<owner>/<repo>:ref:refs/heads/main is better than repo:<owner>/<repo>:.
Azure DevOps WIF issuer deprecation (July 2026)
Breaking change: Azure DevOps issuer (https://vstoken.dev.azure.com) is deprecated as of July 1, 2026. Retirement: July 1, 2027. Microsoft Entra issuer (https://login.microsoftonline.com/) is now the standard.
Migration:
- New service connections already use Microsoft Entra issuer by default
- Existing connections: Azure DevOps shows warning in pipeline runs and config UI
- Convert via the "Update" button in service connection settings
- No downtime during conversion, existing credential continues working until new one is verified
Scope: Only applies to single-tenant Entra applications in Azure public cloud. Multi-tenant apps and non-public clouds are excluded.
azure-workload-identity v1.6.0 token path change (June 2026)
Breaking change: Projected service account token mount path moved:
| Before | After |
|---|---|
/var/run/secrets/azure/tokens |
/var/run/secrets/azure/wi/token |
Full path: .../tokens/azure-identity-token |
Full path: .../wi/token/azure-identity-token |
Applications using DefaultAzureCredential / WorkloadIdentityCredential work without changes, they read AZURE_FEDERATED_TOKEN_FILE which is updated by the webhook.
Workaround for apps that hardcode the old path:
initContainers:
- name: symlink-token
image: busybox:latest
command: ["sh", "-c", "mkdir -p /var/run/secrets/azure/tokens && ln -sf /var/run/secrets/azure/wi/token/azure-identity-token /var/run/secrets/azure/tokens/azure-identity-token"]
volumeMounts:
- { name: azure-tokens-compat, mountPath: /var/run/secrets/azure/tokens }Rule: Never hardcode the token file path. Always read AZURE_FEDERATED_TOKEN_FILE, the webhook owns this path and may change it again.
Key Vault access patterns
RBAC vs access policies
| Mode | When to use | Limitation |
|---|---|---|
| RBAC (recommended) | New deployments, least-privilege | Requires role assignments per vault |
| Access policies (legacy) | Existing deployments | Coarse-grained, hard to audit |
# RBAC: Grant Key Vault Secrets User role
az role assignment create \
--assignee <principal-id> \
--role "Key Vault Secrets User" \
--scope /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.KeyVault/vaults/<vault>Secret naming conventions
# Use structured names for discoverability
database/connection-string
database/password
api/stripe-secret-key
certificates/tls-cert
# Use labels for environment separation
database/connection-string#prod
database/connection-string#dev