The Azure User-Assigned Managed Identity Bible
User-Assigned Managed Identity in Azure is a powerful tool when connecting Azure resources and controlling permissions. But using it across applications requires specific code and configuration. This guide is a consolidation of my learnings and practical applications.
Why User-Assigned Managed Identity?
Role-Based Access Control
Using Managed Identities allows us to control permissions by role. This means we can grant specific roles to specific applications, just like how we grant specific roles to our developers.
Singular Permissions Controls Across Resources
User-Assigned Managed Identities (UAMIs) allow us to control permissions for multiple resources at once. While granular permissions per resource generally make sense, where this provides the most benefit is with deployment slots. Instead of having to manage a separate managed identity per deployment slot and controlling permissions for each, we can assign one UAMI to our app and all its deployment slots.
Alternatively, with System-Assigned Managed Identities (SAMIs), we get a different Entra User for every single deployment slot and every different resource. This means an application with a web app resource and a function app resource, each with 5 deployment slots, would have 12 separate users we have to manage permissions for.
No Keys to Manage and Rotate
When using secrets, managing those secrets is a security concern. Depending on the secret type, Azure might also force the keys to expire, and they will need to be rotated regularly. Without manual reminders, the secrets expire and our apps break.
UAMIs allow us to assign permissions without the need for keys. Once a UAMI is set up and access is configured, it will never expire.
Tradeoffs
Using UAMIs can be more difficult:
You must manually create and enable UAMIs (unless automated by IaC like Bicep or Terraform). They are not enabled by default and not as easy to configure and use as System-Assigned Managed Identity.
You can have both system-defined and user-defined identities or multiple user-defined identities. So, you often have to tell your application/resource which identity to use.
If you want to access Key Vault using UAMI, you must set
keyVaultReferenceIdentityon each resource.For SQL connections, you must set a User ID in the connection string which maps to the Client ID of the managed identity.
Azure Setup
Creating a Managed Identity
In the Azure Portal, go to Managed Identities. Create a new Managed Identity. Give it a name per resource and environment. Put it in the same resource group as your application.
Turning On User-Assigned Identity
On the application resource, go to Settings -> Identity and turn on User-Assigned Identity and assign the UAMI you created above.
Turn System-Assigned Identity off if it is on.
App Configurations
SQL Connections
SQL connection strings must include a User ID parameter. The value should be the Client ID (not the Object ID) of the UAMI.
Ex: Server=<server>; Authentication=Active Directory Managed Identity;Database=<database_name>;User Id=<uami_client_id>
Assigning login and access to the SQL server and database is the same as any other user. The Managed Identity is just an Entra user.
One note: In many SQL logs, the user will not show up with its display name. Instead, you will see its Client ID which is a GUID.
Application Insights
Give the UAMI the following access to the Application Insights resource.
- Monitoring Metrics Publisher
This works for the Open Telemetry SDK with Azure Monitor as well as the Application Insights SDK.
References
Azure Functions (Web Jobs Storage)
Function App Settings:
AzureWebJobsStorage__accountName = <storage_account_name>
AzureWebJobsStorage__credential = managedIdentity
AzureWebJobsStorage__clientId = <uami_client_id>
Roles Needed on the Storage Account Resource:
Storage Blob Data Owner
Storage Account Contributor
- required if using storage-queue-triggered functions
Storage Queue Data Contributor
- required if using storage-queue-triggered functions
Storage Table Data Contributor
- this one is not in Microsoft’s docs, but if you don’t add it, the function app will regularly throw an exception
Azure.RequestFailedException at Azure.Data.Tables.TableRestClientwith trace messageError occurred when attempting to purge previous diagnostic event versions
- this one is not in Microsoft’s docs, but if you don’t add it, the function app will regularly throw an exception
References
Service Bus Message Publishing
You will need to add Azure.Identity to projects that use DefaultAzureCredential or you can get a runtime error.
Example:
var serviceBusNamespace = "<service-bus-namespace>";
var managedIdentityClientId = "<uami-client-id>";
var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions
{
ManagedIdentityClientId = managedIdentityClientId
});
await using var serviceBusClient = new ServiceBusClient(serviceBusNamespace, credential);
await using var sender = serviceBusClient.CreateSender(QUEUE_NAME);
await sender.SendMessageAsync(message);
Roles Needed on the Service Bus Resource:
Azure Service Bus Data Owner
Service Bus Message Consuming (Azure Function Triggers)
To run a function whenever a message arrives in a Service Bus Queue or Subscription, you have to set Connection = "MyAppServiceBusConnection" in your ServiceBusTrigger. Then you have to set your local.settings.json and the environment variables on the function app service as:
"MyAppServiceBusConnection__fullyQualifiedNamespace": "<servicebusname>.servicebus.windows.net",
"MyAppServiceBusConnection__credential": "managedIdentity",
"MyAppServiceBusConnection__clientId": "<clientId>"
Note if you set these keys in Key Vault or Azure App Config, you have to nest them or use the : delimiter in the keys instead of the __ delimiter.
Then you can use an Azure Functions trigger like this:
public class MyFunction()
{
[Function(nameof(MyFunction))]
public async Task Run(
[ServiceBusTrigger(
topicName: "%MyTopicName%", // %% reads the variable from configuration
subscriptionName: "%MySubscriptionName%",
Connection = "MyAppServiceConnection")
] QueueMessage message)
{
}
}
Roles Needed on the Service Bus Resource:
Azure Service Bus Data Receiver
Azure Service Bus Data Owner
References
Storage Account Queue Publishing
You will need to add Azure.Identity to projects that use DefaultAzureCredential or you can get a runtime error.
Example:
var queueServiceUri = "<storage-account-queue-service-uri");
var queueName = "<queue-name>";
var managedIdentityClientId = "<uami-client-id>";
var credentials = new DefaultAzureCredential(new DefaultAzureCredentialOptions
{
ManagedIdentityClientId = managedIdentityClientId
});
var queueClient = new QueueClient(
new Uri($"{queueServiceUri}/{queueName}"),
credentials);
Roles Needed on the Storage Account Resource:
- Storage Queue Data Contributor
Storage Account Queue Consuming (Azure Function Triggers)
Azure Functions should use UAMI to trigger on new messages to a storage account queue.
These are the settings you will need in your appsettings.json file.
MyStorageConnection__queueServiceUri = https://<storage_account_name>.queue.core.windows.net
MyStorageConnection__credential = managedIdentity
MyStorageConnection__clientId = <uami_client_id>
Then you can use an Azure Functions trigger like this:
public class MyFunction()
{
[Function(nameof(MyFunction))]
public async Task Run(
[QueueTrigger(
"%MyQueueName%", // %% reads the variable from configuration
Connection = "MyStorageConnection")
] QueueMessage message)
{
}
}
Roles Needed on the Storage Account:
Storage Queue Data Reader
Storage Queue Data Message Processor
References
Key Vault
To access Key Vault, the application resource (both web apps and function apps and probably others) needs a special property set. You must set the keyVaultReferenceIdentity to the ARM of the UAMI. There is no way in the Azure Portal UI to do this. Instead, you must use the Azure Cloud Shell or Bicep.
az account set --subscription "<subscription_name>"
identityResourceId=$(az identity show --resource-group <resource_group_name> --name <resource_name> --query id -o tsv)
echo ${identityResourceId}
az functionapp update --resource-group <resource_group_name> --name <resource_name> --set keyVaultReferenceIdentity="${identityResourceId}"
az functionapp update --resource-group <resource_group_name> --name <resource_name> --slot <slot_name> --set keyVaultReferenceIdentity="${identityResourceId}"
#...
az webapp update --resource-group <resource_group_name> --name <resource_name> --slot <slot_name> --set keyVaultReferenceIdentity="${identityResourceId}"
#...
Running Locally
You cannot use UAMI with Azure function app triggers locally since managed identity only works within Azure.
Instead, you can use azurecli as the credential in local.settings.json.
{
"Values": {
"AzureWebJobsStorage__accountName": "<storage_account_name>",
"AzureWebJobsStorage__credential": "azurecli"
}
}
These must be set in local.settings.json and not in appsettings.json files since they must be available to the Function App host at startup.
That works for Service Bus and Storage Account queue triggers too.
Alternatively, you can specify a full connection string.
{
"Values": {
"AzureWebJobsStorage": "<storage_account_connection_string>"
}
}
If you want to use Azurite, a storage emulator, for Web Jobs Storage you can instead set:
{
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true"
}
}