
If a CI pipeline or an Azure VM in your environment has a client secret sitting in a config file somewhere, there's a good chance it doesn't need to. Azure has a credential-free way for its own resources to authenticate, and the only reason most teams still store secrets is that they set things up before they knew the alternative existed, or they're solving a problem managed identities genuinely can't solve.
The core distinction
Both a managed identity and a service principal are, under the hood, the same underlying object in Microsoft Entra ID (formerly Azure AD): an identity that isn't a person, used by software to authenticate. The difference is who holds the credential and how it's rotated:
- Service principal — you create it, you manage its credential (a client secret or a certificate), and you're responsible for rotating that credential before it expires. It can be used from anywhere: another cloud, a laptop, an on-prem server, a third-party CI/CD system that isn't running inside Azure.
- Managed identity — Azure creates and manages the credential for you, automatically rotates it, and never exposes it to you at all. The catch: it only works for a resource running inside Azure — a VM, an App Service, an Azure Function, an AKS pod via workload identity, and so on. It can't authenticate from your laptop or from GitHub-hosted runners, because the credential is tied to that specific Azure resource's identity, issued through the Azure instance metadata service.
That's the entire decision in one sentence: if the thing authenticating runs inside Azure, use a managed identity; if it runs outside Azure, you need a service principal (or Workload Identity Federation, more on that below).
Two flavors of managed identity
There are two kinds, and picking wrong causes real operational pain later:
- System-assigned — tied 1:1 to a single resource's lifecycle. Delete the VM, the identity is deleted with it. Simple, but you can't share it across resources and can't pre-create it before the resource exists.
- User-assigned — created as its own standalone Azure resource with its own lifecycle. You can attach the same user-assigned identity to multiple VMs or Function Apps, which is usually what you want for anything beyond a single throwaway VM, because it survives the resource being rebuilt and lets you grant role assignments once instead of per-resource.
Setting up a system-assigned managed identity
# Enable a system-assigned identity on an existing VM
az vm identity assign --name my-app-vm --resource-group my-rg
# Grant it a role on a target resource -- e.g. read access to a storage account
principalId=$(az vm identity show --name my-app-vm --resource-group my-rg --query principalId -o tsv)
az role assignment create \
--assignee-object-id "$principalId" \
--assignee-principal-type ServicePrincipal \
--role "Storage Blob Data Reader" \
--scope "/subscriptions//resourceGroups/my-rg/providers/Microsoft.Storage/storageAccounts/myappstorage"Note that even a managed identity shows up as principal type ServicePrincipal in role assignments — in Entra ID's object model, managed identities are implemented as a special kind of service principal. The naming overlap is exactly why people confuse the two.
Setting up a user-assigned managed identity
# Create the identity as its own resource
az identity create --name shared-app-identity --resource-group my-rg
# Grab its IDs
identityId=$(az identity show --name shared-app-identity --resource-group my-rg --query id -o tsv)
principalId=$(az identity show --name shared-app-identity --resource-group my-rg --query principalId -o tsv)
# Attach it to a VM (can attach the same identity to many resources)
az vm identity assign --name my-app-vm --resource-group my-rg --identities "$identityId"
# Grant it a role, same as before
az role assignment create \
--assignee-object-id "$principalId" \
--assignee-principal-type ServicePrincipal \
--role "Key Vault Secrets User" \
--scope "/subscriptions//resourceGroups/my-rg/providers/Microsoft.KeyVault/vaults/my-vault"Using the identity from inside your code
Application code never handles a secret — it asks the Azure Instance Metadata Service for a token, which the Azure SDK does for you automatically:
from azure.identity import ManagedIdentityCredential
from azure.storage.blob import BlobServiceClient
# For a user-assigned identity, pass its client ID explicitly;
# omit client_id entirely for a system-assigned identity.
credential = ManagedIdentityCredential(client_id="")
blob_service = BlobServiceClient(
account_url="https://myappstorage.blob.core.windows.net",
credential=credential,
)In most real apps you'd actually use DefaultAzureCredential instead, which tries managed identity first and falls back to other methods (environment variables, Azure CLI login) — handy because the same code then works unchanged on your local machine during development and in Azure in production.
When you genuinely still need a service principal
Service principals aren't legacy cruft — they're the right tool for anything authenticating from outside Azure:
# Create a service principal scoped to a resource group, with a client secret
az ad sp create-for-rbac \
--name "github-actions-deploy-sp" \
--role "Contributor" \
--scopes "/subscriptions//resourceGroups/my-rg"That prints a client ID, tenant ID, and client secret — the secret you now have to store somewhere safe and rotate before it expires.
The better move: skip the stored secret entirely with federated credentials
If the outside caller is something like GitHub Actions or another OIDC-capable CI system, you don't actually need a stored secret at all. Workload Identity Federation lets you configure a service principal (or a user-assigned managed identity) to trust tokens issued by an external OIDC provider, so the CI job authenticates with a short-lived token instead of a long-lived secret:
az ad app federated-credential create \
--id \
--parameters '{
"name": "github-actions-main",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:myorg/myrepo:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"]
}'This is the Azure-side equivalent of keyless CI auth patterns you may already know from AWS (sts:AssumeRoleWithWebIdentity) or GCP (Workload Identity Federation) — no secret is ever stored in GitHub, Azure just trusts a signed token GitHub issues per run.
Migrating off a stored credential: a practical path
- Step 1 — Identify where the credential runs. If it's an Azure VM, Function App, or App Service, that's a managed identity candidate. If it's external CI, check whether that CI system supports OIDC (GitHub Actions, GitLab CI, and others do) — that's a federated credential candidate.
- Step 2 — Create the replacement identity (managed identity or federated credential) and grant it the same role assignments the old service principal has. Use
az role assignment list --assigneeto see exactly what to replicate. - Step 3 — Update the application or pipeline to request a token via the new identity, and test it end to end before touching the old credential.
- Step 4 — Once confirmed, delete the old client secret (
az ad app credential delete) rather than just leaving it unused — an unused but valid secret is still a live risk sitting in whatever vault or file it was stored in.
Common pitfalls
- Trying to use a managed identity from outside Azure. It will not work from your laptop, from on-prem, or from a CI runner that isn't an Azure resource — the identity token is only issuable to the specific Azure resource it's attached to.
- Using system-assigned identity on a resource you rebuild often. Every rebuild gets a brand-new identity with a new object ID, so role assignments silently stop applying. Use user-assigned for anything you expect to recreate.
- Forgetting role assignments take a few minutes to propagate. A fresh role assignment on a managed identity can take a short while to become effective; a request failing immediately after creating the assignment isn't necessarily a configuration error.
- Leaving the old service principal's secret active after migration. "We're not using it anymore" isn't the same as "it can't be used" — rotate or delete it.
Troubleshooting
If code using ManagedIdentityCredential fails with an authentication error, check first whether the resource actually has the identity enabled (az vm identity show) and whether the role assignment exists and targets the right scope. A common mistake is granting the role at the wrong resource group or subscription level, or granting it to the resource's VM identity when the code is actually asking for a specific user-assigned identity's client ID that was never attached.
Where to go next
Once you've moved CI and in-Azure workloads off stored secrets, the same keyless pattern extends to Key Vault access policies and RBAC for secrets, and to cross-tenant scenarios using Entra ID's app-to-app federation — worth a dedicated look once the basics here are solid.

Experienced DevOps Engineer proficient in Python, Docker, Kubernetes, CI/CD, and cloud technologies. Specialized in automating processes and building scalable infrastructure for seamless development and deployment.


