All posts

8 min readMicrosoft Entra · Managed Identity · Azure · Best Practices

Managed identities instead of client secrets: where they work and where they don't

Managed identities replace client secrets for workloads running in Azure. Which services support them, how to grant Microsoft Graph permissions with PowerShell, where only workload identity federation helps, and which secrets remain and still expire.

Read this post in German

A client secret is a password for an application. It sits in an app setting, a Key Vault or a pipeline variable, it expires, and somebody has to replace it in time. Managed identities remove that password, but only for workloads that run in Azure. This post shows where the switch works, what it looks like technically, and which credentials are left afterwards.

What a managed identity is

A managed identity is a service principal in Microsoft Entra ID whose credential Azure creates, stores and renews itself. There is no secret for you to copy, store or rotate. According to Microsoft the credential is certificate-based, valid for 90 days and rolled after 45 days. You never get to see it.

Unlike an app registration, a managed identity has no application object, only the service principal. That matters later when you grant permissions.

System-assigned or user-assigned

Both variants produce the same kind of token. The difference is the lifecycle:

  • System-assigned is enabled directly on a resource, for example a Function App. The identity carries the resource's name, belongs to it alone and is deleted with it. Fitting when a workload needs its own identity and you want the audit log to show which resource acted.
  • User-assigned is a standalone Azure resource that you attach to one or more resources. Roles and Graph permissions can be granted before the workload exists. Microsoft's best-practice guidance calls this the more efficient choice for most scenarios, because there are fewer identities and role assignments to maintain, for example when five VMs read the same storage account.

A resource can carry both at once: one system-assigned and several user-assigned identities.

Which Azure services support managed identities

Microsoft maintains the full list under Azure services that support managed identities. Relevant for typical integration workloads:

  • Azure App Service and Azure Functions (configured per deployment slot)
  • Virtual Machines and Virtual Machine Scale Sets
  • Azure Kubernetes Service, for pods through Microsoft Entra Workload ID
  • Azure Container Apps and Azure Container Instances
  • Azure Logic Apps
  • Azure Automation (runbooks)
  • Azure Data Factory and Azure Synapse
  • API Management
  • Azure Arc-enabled servers, meaning on-premises servers with the Connected Machine agent (system-assigned only)

The last item is the one exception to the rule "Azure only". A Windows or Linux server in your own data centre gets a system-assigned identity through Azure Arc, and the agent renews it.

How code obtains a token

The Azure Identity libraries (Azure.Identity for .NET, azure-identity for Python and Java, @azure/identity for JavaScript) have two relevant classes. ManagedIdentityCredential fetches a token for the managed identity specifically. DefaultAzureCredential walks a chain: environment variables, workload identity, managed identity and finally the Azure CLI sign-in on a developer machine. For production code in Azure, Microsoft recommends the explicit ManagedIdentityCredential so that no other credential is picked up by accident.

var credential = new ManagedIdentityCredential();
var token = await credential.GetTokenAsync(
    new TokenRequestContext(["https://graph.microsoft.com/.default"]));

Under the hood the library calls a local endpoint: on a VM the Instance Metadata Service at 169.254.169.254, on App Service, Functions and Container Apps the address from the IDENTITY_ENDPOINT environment variable. The token is issued on the resource itself; nothing is read from a secret store.

Granting Microsoft Graph permissions to a managed identity

This is where most people get stuck. For an app registration you open "API permissions" in the Entra admin center, pick Application.Read.All and grant admin consent. That page does not exist for a managed identity, because it has no application object. Microsoft states explicitly that the admin center currently offers no way to assign Graph permissions to a managed identity.

Instead you assign the app role directly to the service principal. The example gives a Function App the Application.Read.All permission on Microsoft Graph:

Connect-MgGraph -Scopes "Application.Read.All", "AppRoleAssignment.ReadWrite.All"

# Service principal of the managed identity (name = name of the Azure resource)
$mi = Get-MgServicePrincipal -Filter "displayName eq 'func-secret-report' and servicePrincipalType eq 'ManagedIdentity'"

# Service principal of Microsoft Graph and the app role we want
$graph = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
$role  = $graph.AppRoles | Where-Object { $_.Value -eq "Application.Read.All" }

New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $mi.Id `
  -PrincipalId $mi.Id -ResourceId $graph.Id -AppRoleId $role.Id

Afterwards the assignment is visible in the Entra admin center under "Enterprise applications", filter "Application type: Managed Identities", on the "Permissions" tab. One practical note: managed identity tokens are cached, so a new role can take a while to show up in the token.

Where managed identities do not work

Managed identities exist only for resources Azure knows about. The limits follow from that:

  • On-premises servers without Azure Arc, servers at other hosters, AWS or Google Cloud: no IMDS endpoint, no managed identity.
  • Third-party SaaS connecting to your tenant (backup, ticketing, monitoring): the vendor authenticates with its own app registration and its own credential.
  • Developer laptops: locally, DefaultAzureCredential falls back to the Azure CLI or Visual Studio sign-in. That is a user identity, not a managed identity.
  • Multi-tenant apps that customers approve via admin consent: managed identities do not work across tenants. If the app itself runs in Azure, the app registration can now accept a managed identity as a federated credential and drop its secret. The customer-side consent is unaffected.
  • SAML SSO in enterprise apps: the signing certificate has nothing to do with managed identities and keeps expiring.

Workload identity federation for CI/CD and Kubernetes outside Azure

For GitHub Actions, Azure DevOps, Kubernetes in your own data centre or workloads in other clouds there is a second secretless route: workload identity federation. You add a federated identity credential to the app registration that trusts an external OIDC issuer. In the admin center it lives under "Certificates & secrets" on the "Federated credentials" tab.

Three values define the trust: issuer (for GitHub https://token.actions.githubusercontent.com), subject (for example repo:my-org/my-repo:environment:Production) and audience (recommended api://AzureADTokenExchange). The workflow exchanges its short-lived GitHub token for an Entra token; no secret is stored anywhere. An app registration or user-assigned managed identity can hold at most 20 federated credentials. A common mistake: if the subject does not match exactly, the exchange fails without a useful error.

Decision table

Where the workload runsRecommendationWhat still needs monitoring
App Service, Functions, Container Apps, VMManaged identity, user-assigned when several resources need the same rightsnothing
AKS podsWorkload ID with a user-assigned identity and a federated credentialnothing
GitHub Actions, Azure DevOps, other CIWorkload identity federation on the app registrationnothing, but document the subject rules
Kubernetes outside AzureWorkload identity federation with the cluster's OIDC issuernothing
On-premises server with Azure Arcsystem-assigned managed identity of the Arc servernothing
On-premises server without Arc, other cloudsCertificate, or a client secret on the app registration if you mustexpiry date
Third-party SaaS with its own appThe vendor's credential, usually a secretexpiry date
Multi-tenant app for customersCertificate or secret; in Azure alternatively a managed identity as federated credentialexpiry date
SAML SSO in enterprise appsSigning certificateexpiry date

Migration in four steps

  1. Inventory. List every app registration with its secrets and certificates, by script or tool. How to do that with Get-MgApplication is in Entra app registration secrets expire.
  2. Classify by runtime location. For each credential, record who uses it and where that workload runs: Azure resource, CI pipeline, external service, laptop, customer. Without a description and owner on the secret, this is the expensive step.
  3. Migrate Azure-hosted workloads first. Enable the identity, assign the Graph role or RBAC, switch the code to ManagedIdentityCredential, test alongside the old secret. Then move CI/CD and external Kubernetes clusters to federated credentials.
  4. Delete the old secrets. Only once the app registration's sign-in log shows no more sign-ins with that secret. An unused secret that still exists is attack surface without benefit.

What remains after migration

In our experience a third to a half of the credentials in a mid-sized tenant stay: backup and archiving services with their own app, monitoring tools, connectors to ERP or CRM, the multi-tenant app of your own product, SAML certificates for SSO. Each of them has an expiry date, and Microsoft still does not warn you. Migration shrinks the problem, it does not remove it.

How SecretExpiry helps

SecretExpiry monitors the remaining client secrets and certificates of app registrations across many tenants, using the read-only Application.Read.All permission and without ever seeing the secret values. Alerts arrive by e-mail per event or as a weekly digest, in a Teams or Slack channel via webhook, and as a calendar subscription (ICS). Each tenant can have its own notification address, individual credentials can be snoozed, and the dashboard shows an urgent list. The trial is free for 14 days, after that from 10 € per tenant per month. Setup details are in the documentation.