Skip to main content

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:

FeatureSystem-AssignedUser-Assigned
LifecycleTied to resourceIndependent resource
SharingCannot share across resourcesCan assign to multiple resources
Use CaseSingle resource needs identityMultiple resources need same identity
ManagementAuto-created/deletedManual lifecycle management
When to UseMost common scenariosShared 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:

  1. Managed Identity (when in Azure)
  2. Azure CLI (when developing locally)
  3. Visual Studio Code (when debugging)
  4. 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

  1. No secrets to rotate: Azure handles credential rotation automatically — I've never had to schedule a secret rotation for a managed identity.
  2. 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.
  3. Conditional Access: I apply CA policies to service principals where the security posture requires it.
  4. Least privilege: I assign the narrowest possible role. If the workload only reads blobs, it gets Storage Blob Data Reader — not Contributor.
  5. 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.