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

PracticeDescription
Use least privilegeAssign specific roles, not Owner
Scope to containersGrant access to specific containers
Use system-assignedSimpler for single resource
Audit assignmentsReview 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

IssueCauseSolution
AuthenticationFailedMissing RBAC roleAssign Storage Blob Data Reader/Contributor
403 ForbiddenWrong scopeVerify role scope matches resource
ResourceNotFoundStorage account not foundVerify account name and subscription
CredentialUnavailableNo identity configuredEnable 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


Azure Integration Hub - Intermediate Level