In this blog post Securely use Managed Identity in Production and Azure CLI Locally we will show a simple, safe way to authenticate apps in Azure without storing secrets, while keeping local development fast.
The idea is straightforward. In production, your app uses Azure Managed Identity to get tokens automatically from Azure Active Directory (Entra ID). Locally, developers sign in with the Azure CLI, and the same code picks up those credentials. One code path. Two environments. No secrets.
This pattern keeps operations secure and predictable, and it keeps developers unblocked. Let’s walk through the why, the tech behind it, and concrete steps to implement it.
The technology behind it
Managed Identity
Managed Identity (MI) is a first-class identity for Azure resources. Azure hosts and rotates its credentials. Your code requests an access token for a resource (like Key Vault or Storage) and Azure returns it if the identity has the right role. There are two types:
- System-assigned: Tied to a single resource (VM, App Service, Function, Container App). Lifecycle follows the resource.
- User-assigned: A reusable identity you can attach to multiple resources. Good for multi-app scenarios or AKS with Workload Identity.
OAuth 2.0 and tokens
Under the hood, Azure AD issues OAuth 2.0 access tokens for resource-specific scopes (for example, https://vault.azure.net/.default for Key Vault). The Azure SDKs handle token acquisition and refresh.
DefaultAzureCredential
The Azure SDKs offer DefaultAzureCredential, which tries multiple credential sources in order. In Azure, it finds Managed Identity. On a developer machine, it falls back to the Azure CLI (after checking environment variables and other developer tools). This lets the same code work everywhere.
Azure CLI for local development
Developers run az login to authenticate. The Azure SDK reads the CLI’s cached token via the AzureCliCredential step inside DefaultAzureCredential. No secrets, no config files with passwords.
Architecture at a glance
- Production: App with Managed Identity → Azure AD issues tokens → App calls Azure services (Key Vault, Storage, SQL, etc.).
- Local: Developer logs in with Azure CLI → Same code uses DefaultAzureCredential → App calls the same Azure services.
Production setup with Managed Identity
1) Choose identity type
- Small, single app: system-assigned MI is simplest.
- Multiple apps or AKS: user-assigned MI gives you reuse and separation of duties.
2) Create a user-assigned identity (optional)
az identity create -g <rg> -n <mi-name> -l <region>
az identity show -g <rg> -n <mi-name> --query "{clientId:clientId, principalId:principalId, id:id}"
Note the clientId (for code) and principalId (for role assignments).
3) Enable Managed Identity on your compute
- App Service (system-assigned):
az webapp identity assign -g <rg> -n <app-name> - App Service (user-assigned):
az webapp identity assign -g <rg> -n <app-name> --identities <mi-resource-id> - VM or VMSS:
az vm identity assign -g <rg> -n <vm-name> - Azure Container Apps:
az containerapp identity assign -g <rg> -n <app-name> --system-assigned - AKS workloads: Use Azure AD Workload Identity with a user-assigned MI (recommended over deprecated AAD Pod Identity). Configure ServiceAccount, federated identity, and binding to the MI.
4) Assign least-privilege roles
Give the identity data-plane roles on the target resources. Examples:
- Key Vault: Key Vault Secrets User
- Storage (Blobs): Storage Blob Data Contributor
# Scope to a specific resource for least privilege
az role assignment create \
--assignee-object-id <principalId-of-mi> \
--assignee-principal-type ServicePrincipal \
--role "Key Vault Secrets User" \
--scope <key-vault-resource-id>
az role assignment create \
--assignee-object-id <principalId-of-mi> \
--assignee-principal-type ServicePrincipal \
--role "Storage Blob Data Contributor" \
--scope <storage-account-resource-id>
Prefer RBAC over legacy Key Vault access policies for new deployments.
Local development with Azure CLI
1) Sign in and select the right subscription
az login
az account list --output table
az account set --subscription <subscription-id-or-name>
az account show --output table
If you work across tenants, add --tenant <tenant-id> to az login.
2) Run the app with DefaultAzureCredential
DefaultAzureCredential will try Managed Identity in Azure. Locally, it will use your Azure CLI sign-in (unless you explicitly exclude it). That means the code below is the same in both places.
Code examples using DefaultAzureCredential
.NET (C#) — Key Vault secret and Blob listing
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Azure.Storage.Blobs;
var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions {
// Good practice to disable CLI in production
ExcludeAzureCliCredential = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production"
});
// If using user-assigned MI, specify client ID (or set AZURE_CLIENT_ID)
// var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions {
// ManagedIdentityClientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID")
// });
// Key Vault
var kvUri = new Uri(Environment.GetEnvironmentVariable("KEY_VAULT_URI"));
var secretClient = new SecretClient(kvUri, credential);
var secret = await secretClient.GetSecretAsync("app-connection-string");
Console.WriteLine($"Secret length: {secret.Value.Value.Length}");
// Storage
var blobServiceClient = new BlobServiceClient(new Uri(Environment.GetEnvironmentVariable("BLOB_ENDPOINT")), credential);
await foreach (var container in blobServiceClient.GetBlobContainersAsync())
{
Console.WriteLine(container.Name);
}
Python — Key Vault secret
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
import os
env = os.getenv("ENVIRONMENT", "Dev")
credential = DefaultAzureCredential(exclude_cli_credential=(env == "Production"))
# For user-assigned MI: DefaultAzureCredential(managed_identity_client_id=os.getenv("AZURE_CLIENT_ID"))
kv_uri = os.getenv("KEY_VAULT_URI")
secret_client = SecretClient(vault_url=kv_uri, credential=credential)
secret = secret_client.get_secret("app-connection-string")
print(len(secret.value))
Node.js — Blob listing
const { DefaultAzureCredential } = require("@azure/identity");
const { BlobServiceClient } = require("@azure/storage-blob");
const env = process.env.NODE_ENV || "development";
const credential = new DefaultAzureCredential({
excludeAzureCliCredential: env === "production",
// For user-assigned MI: managedIdentityClientId: process.env.AZURE_CLIENT_ID
});
const blobEndpoint = process.env.BLOB_ENDPOINT; // e.g. https://<account>.blob.core.windows.net
const client = new BlobServiceClient(blobEndpoint, credential);
for await (const c of client.listContainers()) {
console.log(c.name);
}
Environment-specific controls
- Force-disable CLI in prod: use the SDK options shown above (ExcludeAzureCliCredential/exclude_cli_credential).
- User-assigned MI selection: set
AZURE_CLIENT_IDor pass the client ID via SDK options. - Configuration: store endpoints (Key Vault URI, Storage endpoint) in app settings or Azure App Configuration, not in code.
Step-by-step checklist
- Pick identity type (system-assigned or user-assigned).
- Enable MI on your compute.
- Assign least-privilege roles on each Azure resource your app needs.
- Use
DefaultAzureCredentialin code. No secrets, no connection strings. - Locally:
az login, set the right subscription, run the app. - In production: optionally exclude CLI credential and pin user-assigned MI via client ID.
- Observe logs and verify access; adjust roles if you see 403s.
Common pitfalls and how to fix them
- 403 Forbidden to a service: The MI lacks the data-plane role on that resource. Assign the correct role at the smallest scope.
- It works locally but fails in prod: You may be using your personal CLI permissions. Exclude CLI in prod and ensure the MI has roles.
- Multiple identities on one resource: Provide the user-assigned MI’s client ID to the SDK so it knows which one to use.
- Key Vault using access policies: Prefer RBAC for new deployments to match how other services authorize.
- Wrong subscription: Check
az account showand set the intended subscription before testing locally. - AKS pods need identity: Use Azure AD Workload Identity with a user-assigned MI mapped to a Kubernetes ServiceAccount.
Security and operations tips
- Least privilege everywhere. Avoid broad roles like Contributor for app identities.
- Separate identities per app or boundary. This simplifies audits and incident response.
- Log token usage paths. The Azure SDKs can emit helpful telemetry and request IDs.
- Automate role assignments in IaC (Bicep/Terraform) to keep environments consistent.
- Rotate nothing: one of the benefits of MI is Azure rotates credentials for you.
Putting it all together
This pattern keeps production secure and local development smooth. In production, Managed Identity eliminates secrets and centralizes authorization in Azure AD. Locally, Azure CLI lets developers authenticate with their own accounts while reusing the same code path. The glue is DefaultAzureCredential.
At CloudProinc.com.au we recommend adopting this approach as your default for new services. Start small: enable MI on one app, grant a single data-plane role, and switch the SDK to DefaultAzureCredential. You’ll get immediate security wins with minimal code changes.
Discover more from CPI Consulting -Specialist Azure Consultancy
Subscribe to get the latest posts sent to your email.