Azure managed identities explained: connect services without secrets
Azure managed identities explained for people who have to make one work: system-assigned or user-assigned, how the SDK gets a token, what to do on your laptop where there is no managed identity, the role assignment in Bicep that doesn't fail on the first deploy, and the errors with their fixes.
Identity & securityFix a problem8 min read
A managed identity is an identity Azure creates for one of your resources and looks after for you: no password, no key, nothing to put in configuration or rotate. Your code asks the platform for a token, Entra ID issues it, and the service you call checks the identity's Azure roles like anyone else's. It's the right way for a Function, web app, container, VM, Logic App or pipeline to reach a storage account, a vault, a database or a model. This guide covers the decision everyone has to make first, the code, the laptop problem, a deployment pattern that doesn't break on day one, and the errors.
System-assigned or user-assigned
| System-assigned | User-assigned | |
|---|---|---|
| Created | With the resource; switched on in its Identity settings | As its own Azure resource (Microsoft.ManagedIdentity/userAssignedIdentities) |
| Lifetime | Deleted with the resource | Independent; you delete it |
| Shared | No, one resource only | Yes, assigned to as many resources as you like |
| Role assignments | Can only be made once the resource exists | Can be made in advance, so deployments don't race |
| Name in the directory | The resource's name (a slot is <app>/slots/<slot>) | Whatever you called it |
| Microsoft's recommendation | When each resource must have a distinct identity, or you want permissions to vanish with the resource, or you need the audit log to name the resource | For most other cases |
Why user-assigned wins by default: several resources that do the same job share one identity and one set of role assignments; the identity and its roles can exist before the resource that uses it, so a Bicep deployment doesn't fail because the creator can't assign roles; deleting and recreating a resource doesn't orphan the roles; and creating many resources at once doesn't hit the directory's object-creation rate limit (each system-assigned identity is a new service principal, and deleted ones count against your quota for 30 days). A resource can have one system-assigned and several user-assigned identities at once, and choose which to use per call.
One security point: whoever can run code on a resource can use every identity assigned to it, and whoever can assign a user-assigned identity can give its permissions to any resource they control. Treat "can deploy to this app" and "Managed Identity Operator" as privileged roles.
The three steps
-
Create the identity (user-assigned shown):
bash az identity create --name id-orders-api --resource-group rg-orders az identity show --name id-orders-api --resource-group rg-orders --query "{clientId:clientId, principalId:principalId}" az webapp identity assign --name app-orders-api --resource-group rg-orders \ --identities /subscriptions/<sub>/resourceGroups/rg-orders/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id-orders-apiTwo IDs matter: the principal (object) ID is what role assignments point at; the client ID is what your code passes to pick a user-assigned identity.
-
Give it a data-plane role on the target, at the narrowest scope that works. A control-plane role such as Contributor doesn't read data; see our 403 after assigning a role guide for the role per service.
bash az role assignment create --role "Storage Blob Data Reader" \ --assignee-object-id <principalId> --assignee-principal-type ServicePrincipal \ --scope /subscriptions/<sub>/resourceGroups/rg-orders/providers/Microsoft.Storage/storageAccounts/storders -
Use it in code. The Azure Identity library (
Azure.Identity,azure-identity,@azure/identity) handles the token:csharp var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions { ManagedIdentityClientId = "<client id of id-orders-api>" // omit for a system-assigned identity }); var blobs = new BlobServiceClient(new Uri("https://storders.blob.core.windows.net"), credential);The Azure CLI can use the identity too, on a VM or a hosted pipeline agent:
az login --identity(add--client-idfor user-assigned). Some services call the identity for you: a Logic App connection, a Data Factory linked service, an App Service Key Vault reference.
A useful second use: a managed identity can be a federated credential on an app registration, so an app that must be an Entra application (for Graph permissions, say) still has no secret. There's a limit of 20 federated credentials per app.
On your laptop there is no managed identity
DefaultAzureCredential tries a chain of credentials in order: environment variables (a service principal), workload identity, managed identity, Visual Studio, VS Code, Azure CLI, Azure PowerShell, Azure Developer CLI, then an interactive browser if you allow it. In Azure the managed identity answers; on your machine it fails and the chain falls through to your az login. The same code runs in both places, provided your own account has the same data-plane role on the same resource. Test with that account, not with a subscription Owner, which hides planes problems.
Microsoft's advice for production is to replace DefaultAzureCredential with the specific credential once you know what the app uses (ManagedIdentityCredential), or set AZURE_TOKEN_CREDENTIALS=prod to restrict the chain: it's faster (no failed attempts), easier to debug, and immune to a stray AZURE_CLIENT_SECRET environment variable on the host changing who the app is. The chain also honours AZURE_USERNAME and AZURE_PASSWORD, a username-and-password flow that mandatory MFA has made unusable; remove them if they're set anywhere.
Deploy it with Bicep
The pattern that survives redeploys: a user-assigned identity, the role assignment named deterministically and with principalType set, and the app referencing the identity.
resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: 'id-orders-api'
location: location
}
// Storage Blob Data Reader, by ID so a rename of the role never breaks it
var blobReader = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1')
resource read 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(storage.id, identity.properties.principalId, blobReader)
scope: storage
properties: {
roleDefinitionId: blobReader
principalId: identity.properties.principalId
principalType: 'ServicePrincipal' // avoids the PrincipalNotFound replication race
}
}
The guid(scope, principalId, roleDefinitionId) name changes if the principal changes, which is what you want: a role assignment's principal can't be updated, so a reused name with a new identity fails with RoleAssignmentUpdateNotPermitted. A system-assigned identity's principal ID changes every time the resource is recreated, which is the other reason user-assigned deploys more smoothly.
Tidy up the other way too: deleting an identity doesn't delete its role assignments, which linger as Identity not found and count against the subscription's assignment limit. Get-AzRoleAssignment | Where-Object ObjectType -eq "Unknown" | Remove-AzRoleAssignment clears them.
The errors
| Error | Cause | Fix |
|---|---|---|
ManagedIdentityCredential authentication unavailable / No Managed Identity endpoint found | The code isn't running where a managed identity exists (your laptop, a container without one), or the identity isn't enabled on the resource | Enable the identity; locally, let the chain fall through to the CLI |
The requested identity hasn't been assigned to this resource (400) | A user-assigned identity's client ID is wrong, or it isn't assigned to this resource | Check the client ID and the resource's Identity page |
IMDS timeout, 502 or 504 | A proxy or network rule between the VM and 169.254.169.254, or a non-Azure host | Exclude the metadata address from proxies; allow outbound to it |
403 after the token is issued | The identity has no data-plane role on the target, the wrong identity got the token, or the role hasn't propagated | Assign the role to the right principal; wait up to 10 minutes; enable SDK logging to see which credential answered |
403 long after you fixed a group membership | Managed identity tokens are cached per resource for about 24 hours | Assign roles to the identity directly rather than through groups, or wait |
AADSTS70021 with a managed identity as a federated credential | Issuer, subject or audience doesn't match | Compare the federated credential with the token's claims; wait a few minutes after creating it |
| "Azure resource … does not have access to identity …" | A rare assignment-state fault | Remove and reassign the user-assigned identity; disable and re-enable a system-assigned one |
| A Logic App, Data Factory or Automation runbook can't authenticate | Its managed identity isn't enabled, or has no role; Automation's Connect-AzAccount -Identity needs the system identity to exist | Enable the identity on the resource and assign the role |
Sources
- What is managed identities for Azure resources?, Microsoft Learn
- Managed identity best practice recommendations, Microsoft Learn
- Credential chains in the Azure Identity library for .NET, Microsoft Learn (
DefaultAzureCredentialorder and production guidance) - Troubleshoot Azure-hosted application authentication, Microsoft Learn (the
ManagedIdentityCredentialerrors) - Troubleshoot Azure RBAC, Microsoft Learn (
PrincipalNotFound,RoleAssignmentUpdateNotPermitted, identity not found) - Sign in to Azure with a managed identity using Azure CLI, Microsoft Learn
- Known issues with managed identities, Microsoft Learn
Did this fix it?
Comments
No comments yet. Share what worked for you.
Sign in to comment. Comments show under a display name you choose, never your email.