Managed Identity for .NET Apps: Eliminating Secrets
The best secret is the one that doesn't exist. Azure Managed Identity lets your .NET applications authenticate to Azure services — databases, storage, Key Vault, Service Bus — without connection strings, client secrets, or certificates stored anywhere.
The identity is managed by Azure. It's assigned to your compute resource (App Service, Container App, VM, Function App), and Azure handles token issuance and rotation. Your code never sees a credential.
System-Assigned vs User-Assigned
System-assigned identities are tied to a single resource. Enable it on your App Service and it gets an identity that's deleted when the resource is deleted. Simple and clean for single-purpose services.
User-assigned identities are standalone Azure resources. You create one, assign it to multiple compute resources, and manage its lifecycle independently. Use this when multiple services need the same permissions, or when you need the identity to survive resource recreation during deployments.
DefaultAzureCredential: The Universal Adapter
The Azure.Identity package provides DefaultAzureCredential, which tries multiple authentication methods in order:
- Environment variables
- Workload identity (for Kubernetes)
- Managed identity
- Azure CLI credentials
- Visual Studio / VS Code credentials
- Azure PowerShell credentials
This means the same code works everywhere:
var credential = new DefaultAzureCredential();
// Works locally (uses your Azure CLI login)
// Works in Azure (uses managed identity)
var blobClient = new BlobServiceClient(
new Uri("https://mystorage.blob.core.windows.net"),
credential);
For production, you can narrow the chain to reduce startup latency:
var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions
{
ExcludeEnvironmentCredential = true,
ExcludeAzurePowerShellCredential = true,
ExcludeVisualStudioCodeCredential = true,
ManagedIdentityClientId = "your-user-assigned-client-id" // if using user-assigned
});
Connecting to Azure Services
Most Azure SDKs accept a TokenCredential directly. Here's how it looks across common services:
// Blob Storage
services.AddSingleton(new BlobServiceClient(
new Uri("https://mystorage.blob.core.windows.net"),
new DefaultAzureCredential()));
// Service Bus
services.AddSingleton(new ServiceBusClient(
"mybus.servicebus.windows.net",
new DefaultAzureCredential()));
// Key Vault
services.AddSingleton(new SecretClient(
new Uri("https://myvault.vault.azure.net/"),
new DefaultAzureCredential()));
// Cosmos DB
services.AddSingleton(new CosmosClient(
"https://mydb.documents.azure.com:443/",
new DefaultAzureCredential()));
The pattern is consistent: endpoint URI plus credential. No connection strings.
SQL Server with Managed Identity
Azure SQL supports token-based authentication. With the latest Microsoft.Data.SqlClient, it's straightforward:
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(new SqlConnection
{
ConnectionString = "Server=myserver.database.windows.net;Database=mydb;",
AccessTokenCallback = async (parameters, cancellationToken) =>
{
var credential = new DefaultAzureCredential();
var token = await credential.GetTokenAsync(
new TokenRequestContext(["https://database.windows.net/.default"]),
cancellationToken);
return new SqlAuthenticationToken(token.Token, token.ExpiresOn);
}
});
});
Alternatively, use the simpler connection string approach with Authentication=Active Directory Default:
var connectionString =
"Server=myserver.database.windows.net;Database=mydb;Authentication=Active Directory Default;";
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
This relies on Microsoft.Data.SqlClient's built-in Azure Identity support and uses the same credential chain as DefaultAzureCredential.
Granting Permissions
The identity needs RBAC role assignments on each resource it accesses. Common roles:
| Service | Role | Purpose |
|---|---|---|
| Blob Storage | Storage Blob Data Contributor | Read/write blobs |
| Service Bus | Azure Service Bus Data Sender | Send messages |
| Key Vault | Key Vault Secrets User | Read secrets |
| Cosmos DB | Cosmos DB Built-in Data Contributor | Read/write data |
| SQL Database | (SQL-level role) | Via ALTER ROLE |
Assign roles via the Azure CLI:
az role assignment create \
--assignee <managed-identity-principal-id> \
--role "Storage Blob Data Contributor" \
--scope /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Storage/storageAccounts/<account>
For SQL Database, you need to create the user in SQL directly:
CREATE USER [my-app-service-name] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [my-app-service-name];
ALTER ROLE db_datawriter ADD MEMBER [my-app-service-name];
Common Pitfalls
- Forgetting role assignments — the identity exists but has no permissions. You'll get 403 errors at runtime.
- Using
DefaultAzureCredentialwithout excluding unused methods — this can cause slow startup as it tries credentials that will never work in your environment. - Local development confusion — make sure you're logged into the Azure CLI (
az login) and that your account has the same role assignments as the managed identity.
Managed identity should be your default approach for every new Azure deployment. The cost is a few role assignments. The payoff is zero secret management, zero rotation, and one less thing to go wrong at 2 AM.