LowCodeStacks™
Sign in

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

Checked against Microsoft Learn · 7 sourcesPosted by the Maker DeskHow we write guides

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-assignedUser-assigned
CreatedWith the resource; switched on in its Identity settingsAs its own Azure resource (Microsoft.ManagedIdentity/userAssignedIdentities)
LifetimeDeleted with the resourceIndependent; you delete it
SharedNo, one resource onlyYes, assigned to as many resources as you like
Role assignmentsCan only be made once the resource existsCan be made in advance, so deployments don't race
Name in the directoryThe resource's name (a slot is <app>/slots/<slot>)Whatever you called it
Microsoft's recommendationWhen 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 resourceFor 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

  1. 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-api
    

    Two 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.

  2. 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
    
  3. 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-id for 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.

bicep
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

ErrorCauseFix
ManagedIdentityCredential authentication unavailable / No Managed Identity endpoint foundThe code isn't running where a managed identity exists (your laptop, a container without one), or the identity isn't enabled on the resourceEnable 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 resourceCheck the client ID and the resource's Identity page
IMDS timeout, 502 or 504A proxy or network rule between the VM and 169.254.169.254, or a non-Azure hostExclude the metadata address from proxies; allow outbound to it
403 after the token is issuedThe identity has no data-plane role on the target, the wrong identity got the token, or the role hasn't propagatedAssign 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 membershipManaged identity tokens are cached per resource for about 24 hoursAssign roles to the identity directly rather than through groups, or wait
AADSTS70021 with a managed identity as a federated credentialIssuer, subject or audience doesn't matchCompare 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 faultRemove and reassign the user-assigned identity; disable and re-enable a system-assigned one
A Logic App, Data Factory or Automation runbook can't authenticateIts managed identity isn't enabled, or has no role; Automation's Connect-AzAccount -Identity needs the system identity to existEnable the identity on the resource and assign the role

Sources

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.