System-Assigned Managed Identities
Managed identities are the answer to the question "how does this Azure resource authenticate to other Azure resources without secrets in the code?" System-assigned is the simpler version: the identity is created when you enable it on the resource, and deleted when the resource is deleted. One resource, one identity, no credential management.
System-assigned or user-assigned?
My rule of thumb:
| Feature | System-Assigned | User-Assigned |
|---|---|---|
| Lifecycle | Tied to resource | Independent resource |
| Sharing | Cannot share across resources | Can assign to multiple resources |
| Use Case | Single resource needs identity | Multiple resources need same identity |
| Management | Auto-created/deleted | Manual lifecycle management |
| When to Use | Most common scenarios | Shared identity scenarios |
Enabling managed identity
Enable on Azure VM
Using Azure CLI
# Enable system-assigned identity on existing VM
az vm identity assign \
--resource-group rg-compute \
--name vm-app-01
# Enable during VM creation
az vm create \
--resource-group rg-compute \
--name vm-app-02 \
--image UbuntuLTS \
--assign-identity \
--admin-username azureuser \
--generate-ssh-keys
# Get the principal ID (for role assignments)
PRINCIPAL_ID=$(az vm identity show \
--resource-group rg-compute \
--name vm-app-01 \
--query principalId \
--output tsv)
echo $PRINCIPAL_ID
Enable on App Service / Function App
# Enable system-assigned identity
az webapp identity assign \
--resource-group rg-app \
--name app-mywebapp-prod
# Get principal ID
az webapp identity show \
--resource-group rg-app \
--name app-mywebapp-prod \
--query principalId -o tsv
Enable on Azure Container Instance
az container create \
--resource-group rg-containers \
--name mycontainer \
--image mcr.microsoft.com/azuredocs/aci-helloworld \
--assign-identity \
--location eastus
Grant Permissions to Managed Identity
Once enabled, grant the identity access to Azure resources using RBAC:
# Grant Storage Blob Data Contributor role to VM's identity
az role assignment create \
--role \"Storage Blob Data Contributor\" \
--assignee $PRINCIPAL_ID \
--scope /subscriptions/{subscription-id}/resourceGroups/rg-storage/providers/Microsoft.Storage/storageAccounts/mystorageaccount
# Grant Key Vault Secrets User role
az role assignment create \
--role \"Key Vault Secrets User\" \
--assignee $PRINCIPAL_ID \
--scope /subscriptions/{subscription-id}/resourceGroups/rg-security/providers/Microsoft.KeyVault/vaults/kv-app-prod
# Grant SQL DB Contributor role
az role assignment create \
--role \"SQL DB Contributor\" \
--assignee $PRINCIPAL_ID \
--scope /subscriptions/{subscription-id}/resourceGroups/rg-data/providers/Microsoft.Sql/servers/sql-prod-server
Terraform Example
# Virtual Machine with System-Assigned Identity
resource \"azurerm_linux_virtual_machine\" \"app\" {\n name = \"vm-app-prod-01\"
resource_group_name = azurerm_resource_group.compute.name
location = azurerm_resource_group.compute.location
size = \"Standard_D2s_v3\"
# Enable system-assigned managed identity
identity {
type = \"SystemAssigned\"
}
# ... other VM configuration ...
admin_username = \"azureuser\"
admin_ssh_key {
username = \"azureuser\"
public_key = file(\"~/.ssh/id_rsa.pub\")
}
os_disk {
caching = \"ReadWrite\"
storage_account_type = \"Premium_LRS\"
}
source_image_reference {
publisher = \"Canonical\"
offer = \"0001-com-ubuntu-server-jammy\"
sku = \"22_04-lts-gen2\"
version = \"latest\"
}
}
# Grant access to Key Vault
resource \"azurerm_role_assignment\" \"vm_keyvault\" {\n scope = azurerm_key_vault.app.id
role_definition_name = \"Key Vault Secrets User\"
principal_id = azurerm_linux_virtual_machine.app.identity[0].principal_id
}
# Grant access to Storage Account
resource \"azurerm_role_assignment\" \"vm_storage\" {\n scope = azurerm_storage_account.app.id
role_definition_name = \"Storage Blob Data Contributor\"
principal_id = azurerm_linux_virtual_machine.app.identity[0].principal_id
}
# App Service with System-Assigned Identity
resource \"azurerm_linux_web_app\" \"api\" {\n name = \"app-api-prod\"
resource_group_name = azurerm_resource_group.app.name
location = azurerm_resource_group.app.location
service_plan_id = azurerm_service_plan.app.id
# Enable system-assigned identity
identity {
type = \"SystemAssigned\"
}
site_config {
always_on = true
}
app_settings = {
\"WEBSITE_RUN_FROM_PACKAGE\" = \"1\"
}
}
# Grant App Service access to SQL Database
resource \"azurerm_role_assignment\" \"app_sql\" {\n scope = azurerm_mssql_server.main.id
role_definition_name = \"SQL DB Contributor\"
principal_id = azurerm_linux_web_app.api.identity[0].principal_id
}
# Azure Function with System-Assigned Identity
resource \"azurerm_linux_function_app\" \"processor\" {\n name = \"func-processor-prod\"
resource_group_name = azurerm_resource_group.app.name
location = azurerm_resource_group.app.location
service_plan_id = azurerm_service_plan.functions.id
storage_account_name = azurerm_storage_account.functions.name
storage_account_access_key = azurerm_storage_account.functions.primary_access_key
identity {
type = \"SystemAssigned\"
}
site_config {
application_stack {
python_version = \"3.11\"
}
}
}
Using Managed Identity in Application Code
.NET Example (Accessing Key Vault)
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
// DefaultAzureCredential automatically uses managed identity when running in Azure
var client = new SecretClient(
new Uri(\"https://kv-app-prod.vault.azure.net/\"),
new DefaultAzureCredential());
KeyVaultSecret secret = await client.GetSecretAsync(\"db-connection-string\");
string connectionString = secret.Value;
Python Example (Accessing Storage)
from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
# DefaultAzureCredential uses managed identity automatically
credential = DefaultAzureCredential()
blob_service_client = BlobServiceClient(
account_url=\"https://mystorageaccount.blob.core.windows.net\",
credential=credential
)
# Upload blob
blob_client = blob_service_client.get_blob_client(
container=\"mycontainer\",
blob=\"myfile.txt\"
)
blob_client.upload_blob(\"Hello, World!\")
Node.js Example (Accessing Cosmos DB)
const { DefaultAzureCredential } = require(\"@azure/identity\");
const { CosmosClient } = require(\"@azure/cosmos\");
const credential = new DefaultAzureCredential();
const endpoint = \"https://mycosmosdb.documents.azure.com:443/\";
const client = new CosmosClient({ endpoint, aadCredentials: credential });
const database = client.database(\"mydb\");
const container = database.container(\"items\");
// Query data
const { resources } = await container.items.query(\"SELECT * FROM c\").fetchAll();
CI/CD Integration
GitHub Actions with Managed Identity
When self-hosted runners use managed identity:
name: Deploy Application
on: [push]
jobs:
deploy:
runs-on: [self-hosted, linux] # Self-hosted runner with managed identity
steps:
- uses: actions/checkout@v2
- name: Azure Login (uses managed identity automatically)
uses: azure/login@v1
with:
# Leave creds empty - uses managed identity of runner VM
enable-AzPSSession: true
- name: Deploy to App Service
run: |\n az webapp deployment source config-zip \\\n --resource-group rg-app \\\n --name app-api-prod \\\n --src app.zip
Azure DevOps with Managed Identity
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureCLI@2
displayName: 'Deploy using Managed Identity'
inputs:
azureSubscription: 'ManagedIdentityConnection' # Service connection using managed identity
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |\n # Access Key Vault
SECRET=$(az keyvault secret show \\\n --vault-name kv-app-prod \\\n --name api-key \\\n --query value -o tsv)
# Deploy application
az webapp config appsettings set \\\n --resource-group rg-app \\\n --name app-api-prod \\\n --settings API_KEY=$SECRET
Best Practices
1. System-assigned for single-resource scenarios
I use system-assigned when the identity is only needed for one resource — VMs, Function Apps, Container Apps. The automatic lifecycle management is the main draw: when the resource is gone, the identity is gone too, and there's nothing to clean up.
I don't use system-assigned when multiple resources need the same identity, when the identity needs to outlive a resource deletion, or when resources are being created and deleted rapidly (there are service principal creation rate limits that will surface in automation).
2. Use DefaultAzureCredential in Code
I always use DefaultAzureCredential in application code:
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()
This automatically uses:
- Managed Identity (when in Azure)
- Azure CLI (when developing locally)
- Visual Studio Code (when debugging)
- Environment variables (in other scenarios)
I never hardcode credentials or use connection strings with passwords.
3. Grant the narrowest role that works
I assign the narrowest possible role. If the workload only reads blobs, it gets Storage Blob Data Reader — not Contributor. Here's what that looks like:
az role assignment create \\\n --role \"Storage Blob Data Reader\" \\\n --assignee $PRINCIPAL_ID \\\n --scope /subscriptions/{sub}/resourceGroups/rg-storage/providers/Microsoft.Storage/storageAccounts/mystorageaccount/blobServices/default/containers/mycontainer
Scoping to the subscription is too broad — avoid it:
# Too broad - don't do this
az role assignment create \\\n --role \"Contributor\" \\\n --assignee $PRINCIPAL_ID \\\n --scope /subscriptions/{subscription-id}
4. Azure-to-Azure authentication only
I use managed identities for VM-to-Key Vault, App Service-to-Storage, Function-to-Cosmos DB, and AKS-to-Container Registry — any Azure resource talking to another Azure resource.
I don't use it for external SaaS authentication — user-assigned gives better control there. Same for on-premises resources and third-party services outside Azure; those scenarios call for a different credential strategy.
5. Enable Diagnostic Logging
# Enable sign-in logs for managed identity
# View in Azure AD > Sign-in logs > Service principal sign-ins
Common Use Cases
1. App Service Accessing SQL Database
// No connection string needed!
using Microsoft.Data.SqlClient;
using Azure.Identity;
var connectionString = \"Server=sql-prod-server.database.windows.net;Database=mydb;\";
using var connection = new SqlConnection(connectionString);
connection.AccessToken = await new DefaultAzureCredential()
.GetTokenAsync(new TokenRequestContext(new[] { \"https://database.windows.net/.default\" }));
connection.Open();
2. VM Accessing Key Vault
# Inside VM with system-assigned identity enabled
# Get access token
TOKEN=$(curl 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net' -H Metadata:true | jq -r .access_token)
# Access Key Vault secret
curl https://kv-app-prod.vault.azure.net/secrets/db-password?api-version=7.4 \\\n -H \"Authorization: Bearer $TOKEN\"
3. Function App Writing to Storage
import azure.functions as func
from azure.storage.blob import BlobServiceClient
from azure.identity import DefaultAzureCredential
def main(req: func.HttpRequest) -> func.HttpResponse:
# Managed identity used automatically
blob_service = BlobServiceClient(
account_url=\"https://mystorageaccount.blob.core.windows.net\",
credential=DefaultAzureCredential()
)
container_client = blob_service.get_container_client(\"uploads\")
blob_client = container_client.get_blob_client(\"data.txt\")
blob_client.upload_blob(req.get_body(), overwrite=True)
return func.HttpResponse(\"Success\")
Things to Avoid
I don't disable managed identity and fall back to connection strings with passwords — that's the pattern we're trying to get away from. I never grant Owner or Contributor to a managed identity; the workload gets the narrowest role that lets it do its job.
Managed identities are an Azure construct, so I don't try to use them for on-premises resource access or external OAuth flows — that's not what they're for. I also don't rapidly create and delete resources that carry managed identities; there are rate limits on service principal creation that will surface in automation pipelines.
I don't share credentials between dev and prod, and I don't use managed identity for user authentication — that's a job for Azure AD B2C or B2B.
One thing I always check after enabling an identity: the RBAC role assignments. The identity is useless until it has permissions, and it's easy to enable it and move on before the assignments are in place.
I use managed identity for all Azure-to-Azure authentication, DefaultAzureCredential in application code, and audit role assignments regularly. Diagnostic logging is always on.
Troubleshooting
Check if Identity is Enabled
# VM
az vm identity show --resource-group rg-compute --name vm-app-01
# App Service
az webapp identity show --resource-group rg-app --name app-api-prod
# Check role assignments
az role assignment list --assignee $PRINCIPAL_ID -o table
Test Managed Identity from VM
# SSH into VM
# Get access token
curl 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/' -H Metadata:true
# If this fails:
# 1. Check identity is enabled
# 2. Verify RBAC role assignments
# 3. Check firewall rules on target resource
Common Errors
Error: "ManagedIdentityCredential authentication failed"
- Cause: Managed identity not enabled on resource
- Fix: Enable identity using
az <resource> identity assign
Error: "Forbidden" or 403
- Cause: Missing RBAC role assignment
- Fix: Grant appropriate role to managed identity
Error: "No MSI Found"
- Cause: Trying to use managed identity outside Azure
- Fix: Use Azure CLI or service principal for local development
Security Considerations
- No secrets to rotate: Azure handles credential rotation automatically — I've never had to schedule a secret rotation for a managed identity.
- Audit trail: Every action is logged in Azure AD sign-in logs. If something goes wrong, service principal sign-ins is the first place I look.
- Conditional Access: I apply CA policies to service principals where the security posture requires it.
- Least privilege: I assign the narrowest possible role. If the workload only reads blobs, it gets Storage Blob Data Reader — not Contributor.
- Scope restrictions: I assign roles at the resource or resource group level, not the subscription. Broad scope means a compromised identity can do more damage.