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.
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,
DefaultAzureCredentialfalls 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 runs | Recommendation | What still needs monitoring |
|---|---|---|
| App Service, Functions, Container Apps, VM | Managed identity, user-assigned when several resources need the same rights | nothing |
| AKS pods | Workload ID with a user-assigned identity and a federated credential | nothing |
| GitHub Actions, Azure DevOps, other CI | Workload identity federation on the app registration | nothing, but document the subject rules |
| Kubernetes outside Azure | Workload identity federation with the cluster's OIDC issuer | nothing |
| On-premises server with Azure Arc | system-assigned managed identity of the Arc server | nothing |
| On-premises server without Arc, other clouds | Certificate, or a client secret on the app registration if you must | expiry date |
| Third-party SaaS with its own app | The vendor's credential, usually a secret | expiry date |
| Multi-tenant app for customers | Certificate or secret; in Azure alternatively a managed identity as federated credential | expiry date |
| SAML SSO in enterprise apps | Signing certificate | expiry date |
Migration in four steps
- Inventory. List every app registration with its secrets and certificates, by script or tool. How to do that with
Get-MgApplicationis in Entra app registration secrets expire. - 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.
- 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. - 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.