Azure Blob Storage Managed Identity & RBAC
Secure Storage Access Without Keys
Introduction
Using Managed Identity with Azure Blob Storage provides a secure, credential-free way to access blobs, containers, and storage services. Instead of storing storage account keys in your application configuration, you can leverage Azure Active Directory authentication through the identity assigned to your Azure Functions, container apps, or virtual machines. This eliminates the risk of exposed keys, simplifies credential management, and provides detailed audit trails.
This comprehensive guide covers:
- Enabling Managed Identity — Configuring identity on various Azure resources
- RBAC Role Assignment — Granting appropriate permissions
- Code Implementation — Reading and writing blobs with managed identity
- Storage SDK Integration — Using Azure.Storage.Blobs SDK
- Best Practices — Security and operational guidance
Understanding the Architecture
How Managed Identity Accesses Blob Storage
┌─────────────────────────────────────────────────────────────────────┐
│ MANAGED IDENTITY + BLOB STORAGE FLOW │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ AZURE FUNCTION APP │ │
│ │ │ │
│ │ var blobClient = new BlobServiceClient( │ │
│ │ new Uri("https://mystorage.blob.core.windows.net"), │ │
│ │ new DefaultAzureCredential()); │ │
│ │ │ │
│ └────────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ AZURE ACTIVE DIRECTORY │ │
│ │ │ │
│ │ Request token for https://storage.azure.com │ │
│ │ Validate: Function App has MI enabled │ │
│ │ Issue: OAuth access token │ │
│ │ │ │
│ └────────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ BLOB STORAGE ACCOUNT │ │
│ │ │ │
│ │ Validate: Token + RBAC role │ │
│ │ Authorize: Check role assignment │ │
│ │ Execute: Read/Write/Delete blob │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
RBAC Role Hierarchy
┌─────────────────────────────────────────────────────────────────────┐
│ STORAGE RBAC ROLE HIERARCHY │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Owner │
│ ├── Can manage everything │
│ └── Full access to all operations │
│ │ │
│ ▼ │
│ Contributor │
│ ├── Can read and write │
│ └── Cannot manage permissions │
│ │ │
│ ▼ │
│ Storage Blob Data Owner │
│ ├── Full access to blob data │
│ ├── Read, write, delete blobs │
│ └── Manage blob containers │
│ │ │
│ ▼ │
│ Storage Blob Data Contributor │
│ ├── Read, write, delete blobs │
│ └── Cannot set permissions │
│ │ │
│ ▼ │
│ Storage Blob Data Reader │
│ └── Read-only access to blob data │
│ │
└─────────────────────────────────────────────────────────────────────┘
Enable Managed Identity
On Azure Functions
# Enable system-assigned managed identity
az functionapp update \
--name my-function-app \
--resource-group my-rg \
--identity systemassigned
# Verify identity is enabled
az functionapp show \
--name my-function-app \
--resource-group my-rg \
--query "identity"
# Get the principal ID (object ID)
az functionapp identity show \
--name my-function-app \
--resource-group my-rg \
--query principalId
On Azure Container Apps
# Enable system-assigned identity
az containerapp update \
--name my-container-app \
--resource-group my-rg \
--identity systemassigned
# Get the principal ID
az containerapp show \
--name my-container-app \
--resource-group my-rg \
--query identity.principalId
On Virtual Machines
# Enable system-assigned identity
az vm identity assign \
--name my-vm \
--resource-group my-rg \
--system-assigned
# Get the principal ID
az vm show \
--name my-vm \
--resource-group my-rg \
--query identity.principalId
ARM Template
{
"type": "Microsoft.Web/sites",
"apiVersion": "2022-09-01",
"name": "my-function-app",
"location": "eastus",
"identity": {
"type": "SystemAssigned"
},
"properties": {
"serverFarmId": "/subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.Web/serverfarms/my-plan"
}
}
Grant RBAC Access
Role Assignment at Storage Account Level
# Get the managed identity principal ID
IDENTITY_ID=$(az functionapp identity show \
--name my-function-app \
--resource-group my-rg \
--query principalId \
--output tsv)
# Assign Storage Blob Data Contributor (read/write/delete)
az role assignment create \
--assignee $IDENTITY_ID \
--role "Storage Blob Data Contributor" \
--scope "/subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.Storage/storageAccounts/mystorage"
# Or assign Storage Blob Data Reader (read-only)
az role assignment create \
--assignee $IDENTITY_ID \
--role "Storage Blob Data Reader" \
--scope "/subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.Storage/storageAccounts/mystorage"
Role Assignment at Container Level
# Grant access to specific container
az role assignment create \
--assignee $IDENTITY_ID \
--role "Storage Blob Data Contributor" \
--scope "/subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.Storage/storageAccounts/mystorage/blobServices/default/containers/my-container"
# Grant read-only to specific container
az role assignment create \
--assignee $IDENTITY_ID \
--role "Storage Blob Data Reader" \
--scope "/subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.Storage/storageAccounts/mystorage/blobServices/default/containers/read-only-container"
Role Assignment at Blob Level
# Grant access to specific blob
az role assignment create \
--assignee $IDENTITY_ID \
--role "Storage Blob Data Reader" \
--scope "/subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.Storage/storageAccounts/mystorage/blobservices/default/containers/my-container/path/to/blob.txt"
Role Assignment with ARM Template
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
"name": "[guid(resourceId('Microsoft.Web/sites', variables('functionAppName')), variables('roleDefinitionId'))]",
"properties": {
"roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a96f-38f27f1cf541')]",
"principalId": "[reference(resourceId('Microsoft.Web/sites', variables('functionAppName')), '2022-09-01', 'full').identity.principalId]",
"scope": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
}
}
Use in Code
Basic Blob Operations
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Azure.Identity;
public class BlobOperations
{
[FunctionName("ReadBlob")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "blobs/{container}/{name}")]
HttpRequest req,
string container,
string name)
{
// Create client with managed identity
var blobServiceClient = new BlobServiceClient(
new Uri("https://mystorage.blob.core.windows.net"),
new DefaultAzureCredential());
// Get container and blob
var containerClient = blobServiceClient.GetBlobContainerClient(container);
var blobClient = containerClient.GetBlobClient(name);
try
{
// Download content
var response = await blobClient.DownloadContentAsync();
var content = response.Value.Content.ToString();
return new OkObjectResult(new
{
container = container,
name = name,
content = content
});
}
catch (RequestFailedException ex) when (ex.Status == 404)
{
return new NotFoundResult();
}
}
[FunctionName("WriteBlob")]
public async Task Run(
[QueueTrigger("write-queue", Connection = "StorageConnection")]
BlobWriteMessage message)
{
var blobServiceClient = new BlobServiceClient(
new Uri("https://mystorage.blob.core.windows.net"),
new DefaultAzureCredential());
var containerClient = blobServiceClient.GetBlobContainerClient(message.Container);
var blobClient = containerClient.GetBlobClient(message.BlobName);
// Upload content
await blobClient.UploadAsync(
BinaryData.FromString(message.Content),
new BlobUploadOptions
{
Metadata = new Dictionary<string, string>
{
{ "uploaded-by", "managed-identity" },
{ "timestamp", DateTime.UtcNow.ToString("o") },
{ "content-type", message.ContentType ?? "text/plain" }
}
});
}
}
Blob Trigger with Managed Identity
public class BlobTriggerFunction
{
[FunctionName("ProcessUploadedBlob")]
public async Task Run(
[BlobTrigger("uploads/{name}", Connection = "StorageConnection")]
BlobClient inputBlob,
string name,
ILogger log)
{
log.LogInformation("Processing blob: {Name}", name);
// Download and process the blob
var downloadResponse = await inputBlob.DownloadContentAsync();
var content = downloadResponse.Value.Content.ToString();
// Determine file type and process accordingly
var extension = Path.GetExtension(name).ToLowerInvariant();
switch (extension)
{
case ".csv":
await ProcessCsvAsync(content, name);
break;
case ".json":
await ProcessJsonAsync(content, name);
break;
default:
log.LogWarning("Unsupported file type: {Extension}", extension);
break;
}
// Move to processed folder
await MoveToProcessedAsync(inputBlob, name);
}
private async Task MoveToProcessedAsync(BlobClient sourceBlob, string originalName)
{
var blobServiceClient = new BlobServiceClient(
"https://mystorage.blob.core.windows.net",
new DefaultAzureCredential());
var sourceContainer = blobServiceClient.GetBlobContainerClient("uploads");
var destContainer = blobServiceClient.GetBlobContainerClient("processed");
var destBlob = destContainer.GetBlobClient($"processed/{originalName}");
await destBlob.StartCopyFromUriAsync(sourceBlob.Uri);
await sourceBlob.DeleteIfExistsAsync();
}
}
Batch Operations
public class BatchBlobOperations
{
[FunctionName("BatchUpload")]
public async Task Run(
[HttpTrigger(AuthorizationLevel.Function, "post")]
HttpRequest req)
{
var blobServiceClient = new BlobServiceClient(
"https://mystorage.blob.core.windows.net",
new DefaultAzureCredential());
var containerClient = blobServiceClient.GetBlobContainerClient("batch-input");
await containerClient.CreateIfNotExistsAsync();
// Read files from request
var form = await req.ReadFormAsync();
var tasks = new List<Task>();
foreach (var file in form.Files)
{
var blobClient = containerClient.GetBlobClient(file.FileName);
tasks.Add(blobClient.UploadAsync(file.OpenReadStream(), new BlobUploadOptions
{
Metadata = new Dictionary<string, string>
{
{ "original-filename", file.FileName },
{ "content-length", file.Length.ToString() }
}
}));
}
await Task.WhenAll(tasks);
return new OkObjectResult(new { uploaded = tasks.Count });
}
}
Using BlobClient Directly
public class BlobClientOperations
{
// Get specific blob client with managed identity
public async Task<string> ReadSpecificBlob(string containerName, string blobName)
{
var options = new BlobClientOptions
{
Audience = "https://storage.azure.com"
};
var blobClient = new BlobClient(
new Uri($"https://mystorage.blob.core.windows.net/{containerName}/{blobName}"),
new DefaultAzureCredential(),
options);
var response = await blobClient.DownloadContentAsync();
return response.Value.Content.ToString();
}
// Upload with specific content type
public async Task UploadWithContentType(string containerName, string blobName, string content, string contentType)
{
var blobClient = new BlobClient(
new Uri($"https://mystorage.blob.core.windows.net/{containerName}/{blobName}"),
new DefaultAzureCredential());
var blobHttpHeaders = new BlobHttpHeaders
{
ContentType = contentType,
ContentHash = null
};
var options = new BlobUploadOptions
{
HttpHeaders = blobHttpHeaders,
Metadata = new Dictionary<string, string>
{
{ "processed-by", "function-app" }
}
};
await blobClient.UploadAsync(BinaryData.FromString(content), options);
}
// Set access tier for cost optimization
public async Task SetAccessTier(string containerName, string blobName, AccessTier tier)
{
var blobClient = new BlobClient(
new Uri($"https://mystorage.blob.core.windows.net/{containerName}/{blobName}"),
new DefaultAzureCredential());
await blobClient.SetAccessTierAsync(tier);
}
}
With Bindings
Blob Input Binding
[FunctionName("ReadWithBinding")]
public IActionResult ReadBlob(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "files/{filename}")]
HttpRequest req,
[Blob("documents/{filename}", FileAccess.Read, Connection = "StorageConnection")]
string content)
{
return new OkObjectResult(new { content = content });
}
Blob Output Binding
[FunctionName("WriteWithBinding")]
public async Task<IActionResult> WriteBlob(
[HttpTrigger(AuthorizationLevel.Function, "post")]
HttpRequest req,
[Blob("output/{DateTime.UtcNow:yyyy-MM-dd}/{Guid}.txt", FileAccess.Write, Connection = "StorageConnection")]
Stream outputStream)
{
await req.Body.CopyToAsync(outputStream);
return new OkObjectResult(new { status = "written" });
}
Binding with Custom Identity
// host.json - specify identity for binding
{
"extensions": {
"blobs": {
"version": "3.0",
"connection": "BlobConnection"
}
}
}
Local Development
Azure CLI Setup
# Login to Azure
az login
# Set default subscription
az account set --subscription "your-subscription-id"
# Verify login
az account show
Environment Configuration
// local.settings.json
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"StorageConnection": ""
}
}
DefaultAzureCredential Fallback
// DefaultAzureCredential tries these in order:
// 1. Managed Identity (in Azure)
// 2. Visual Studio
// 3. Visual Studio Code
// 4. Azure CLI
// 5. Azure PowerShell
// For local development, ensure at least one is configured:
// - Azure CLI: az login
// - VS: Tools > Options > Azure Service Authentication
// - VS Code: Azure extension signed in
Best Practices
Security Configuration
| Practice | Description |
|---|---|
| Use least privilege | Assign specific roles, not Owner |
| Scope to containers | Grant access to specific containers |
| Use system-assigned | Simpler for single resource |
| Audit assignments | Review quarterly |
Code Optimization
// GOOD: Create client once
public class GoodExample
{
private readonly BlobServiceClient _blobClient;
public GoodExample()
{
_blobClient = new BlobServiceClient(
"https://mystorage.blob.core.windows.net",
new DefaultAzureCredential());
}
[Function("Process")]
public async Task Run([QueueTrigger("orders")] string message)
{
// Reuse _blobClient
var container = _blobClient.GetBlobContainerClient("orders");
}
}
// BAD: Create client per invocation
[Function("BadProcess")]
public async Task Run([QueueTrigger("orders")] string message)
{
// Creates new client each time - inefficient
var blobClient = new BlobServiceClient(
"https://mystorage.blob.core.windows.net",
new DefaultAzureCredential());
}
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| AuthenticationFailed | Missing RBAC role | Assign Storage Blob Data Reader/Contributor |
| 403 Forbidden | Wrong scope | Verify role scope matches resource |
| ResourceNotFound | Storage account not found | Verify account name and subscription |
| CredentialUnavailable | No identity configured | Enable managed identity on resource |
Debug Commands
# 1. Verify managed identity
az functionapp identity show \
--name my-function-app \
--resource-group my-rg
# 2. List role assignments
az role assignment list \
--assignee <principal-id> \
--output table
# 3. Verify storage account
az storage account show \
--name mystorage \
--resource-group my-rg
# 4. Test access with CLI
az storage blob list \
--container-name mycontainer \
--account-name mystorage \
--auth-mode login
Network Issues
// If using private endpoints, ensure correct audience
var options = new BlobClientOptions
{
Audience = "https://mystorage.blob.core.windows.net"
};
var blobClient = new BlobClient(
new Uri("https://mystorage.blob.core.windows.net/container/blob"),
new DefaultAzureCredential(),
options);
Related Topics
- Blob Triggers — Trigger functions on blob changes
- Blob Event Grid — Event-driven blob processing
- Storage SDK — Advanced blob operations
Azure Integration Hub - Intermediate Level