Skip to main content

Command Palette

Search for a command to run...

The Azure User-Assigned Managed Identity Bible

Updated
•7 min read

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 keyVaultReferenceIdentity on 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.

🚨
Be careful with this. If SAMI was previously turned on and used for Azure resource permissions or SQL connections, turning it off will remove those permissions. Make sure you enable User-Assigned Identity and get everything running with it before turning off System-Assigned Identity.
🚨
If you turn System-Assigned Identity off and back on, it will create a new Managed Identity with a new Client ID, so you will have to reconfigure its resource or SQL permissions.

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.TableRestClient with trace message Error occurred when attempting to purge previous diagnostic event versions

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.

âš 
You must run these commands from the Azure Cloud Shell. Running them locally will error.
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}"
#...
🚧
This section is still a work-in-progress and needs expanded.

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"  
  }
}