# 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 `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 -&gt; Identity and turn on User-Assigned Identity and assign the UAMI you created above.

Turn System-Assigned Identity off if it is on.

<div data-node-type="callout">
<div data-node-type="callout-emoji">🚨</div>
<div data-node-type="callout-text"><strong>Be careful with this.</strong> 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.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">🚨</div>
<div data-node-type="callout-text">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.</div>
</div>

## 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

* [MS: Role Based Access for Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/azure-ad-authentication?tabs=aspnetcore#configure-and-enable-microsoft-entra-id-based-authentication)
    

### Azure Functions (Web Jobs Storage)

Function App Settings:

```ini
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

* [MS: Tutorial: Create a function app that connects to Azure services using identities instead of secrets](https://learn.microsoft.com/en-us/azure/azure-functions/functions-identity-based-connections-tutorial)
    
* [MS: Role Based Access for Azure Functions (Web Jobs)](https://learn.microsoft.com/en-us/azure/azure-functions/functions-identity-based-connections-tutorial#grant-the-system-assigned-identity-access-to-the-storage-account)
    

### Service Bus Message Publishing

You will need to add `Azure.Identity` to projects that use `DefaultAzureCredential` or you can get a runtime error.

Example:

```csharp
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:

```yaml
"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:

```csharp
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

* [Azure Service Bus trigger for Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-service-bus-trigger)
    
* [Role Based Access for Service Bus Triggers](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-service-bus-trigger?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cextensionv5&pivots=programming-language-csharp#grant-permission-to-the-identity)
    

### Storage Account Queue Publishing

You will need to add `Azure.Identity` to projects that use `DefaultAzureCredential` or you can get a runtime error.

Example:

```csharp
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.

```ini
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:

```csharp
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

* [Azure Queue storage trigger for Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-queue-trigger)
    
* [MS: Role Based Access for Azure Function Triggers](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-queue-trigger)
    

## 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.

<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">You must run these commands from the Azure Cloud Shell. Running them locally will error.</div>
</div>

```bash
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}"
#...
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">🚧</div>
<div data-node-type="callout-text">This section is still a work-in-progress and needs expanded.</div>
</div>

## 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`.

```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.

```json
{
  "Values": {
    "AzureWebJobsStorage": "<storage_account_connection_string>"
  }
}
```

If you want to use Azurite, a storage emulator, for Web Jobs Storage you can instead set:

```json

{
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true"  
  }
}
```
