Skip to content

RBAC-Only Authentication Patterns (No Secrets in Code)

This document describes best practices for implementing authentication and authorization using RBAC and managed identities in Azure, eliminating the need to store secrets in code or configuration files.

Core Principles

Zero Secrets in Code

Eliminate hardcoded credentials, connection strings, and API keys:

Anti-Pattern Problem Solution
connectionString = "Server=db;User=sa;Password=..." Secrets in code Managed Identity
apiKey = Environment.GetEnvironmentVariable("APIKEY") Secrets in env vars Managed Identity + Azure RBAC
secrets.json committed to Git Exposed in history Use Key Vault with managed identity
appsettings.Production.json with connection strings Secrets in config Azure App Configuration + managed identity

Authentication vs Authorization

Authentication: "Who are you?" (validated via Entra ID)

Authorization: "What can you do?" (enforced via RBAC)

Request -> Entra ID Authentication -> RBAC Authorization -> Access Granted/Denied
           (Verify identity)            (Check permissions)

Azure Managed Identity

System-Assigned Managed Identity

One per resource, lifecycle tied to resource:

# Create App Service with system-assigned identity
$appService = New-AzAppService `
  -ResourceGroupName 'prod-rg' `
  -Name 'my-app' `
  -AppServicePlanName 'my-plan' `
  -IdentityType 'SystemAssigned'

# Check assigned identity
$identity = Get-AzAppService -ResourceGroupName 'prod-rg' -Name 'my-app' | 
  Select-Object -ExpandProperty Identity

Write-Host "Object ID: $($identity.PrincipalId)"

User-Assigned Managed Identity

Shared identity, manual lifecycle management:

# Create user-assigned identity
$identity = New-AzUserAssignedIdentity `
  -ResourceGroupName 'prod-rg' `
  -Name 'app-shared-identity'

# Assign to multiple resources
New-AzAppService `
  -ResourceGroupName 'prod-rg' `
  -Name 'my-app-1' `
  -AppServicePlanName 'my-plan' `
  -IdentityType 'UserAssigned' `
  -IdentityId $identity.Id

New-AzAppService `
  -ResourceGroupName 'prod-rg' `
  -Name 'my-app-2' `
  -AppServicePlanName 'my-plan' `
  -IdentityType 'UserAssigned' `
  -IdentityId $identity.Id

RBAC Role Assignments

Azure Built-In Roles

Assign least-privilege roles:

# App needs to read from storage account
$role = 'Storage Blob Data Reader'
$scope = "/subscriptions/.../resourceGroups/prod-rg/providers/Microsoft.Storage/storageAccounts/mystore"

New-AzRoleAssignment `
  -ObjectId $appIdentity.PrincipalId `
  -RoleDefinitionName $role `
  -Scope $scope

# Verify role assignment
Get-AzRoleAssignment `
  -ObjectId $appIdentity.PrincipalId `
  -Scope $scope

Common Role Patterns

Resource Read Write Admin
Storage Storage Blob Data Reader Storage Blob Data Contributor Storage Account Contributor
Key Vault Key Vault Secrets User Key Vault Secrets Officer Key Vault Administrator
SQL Database SQL DB Datareader SQL DB Datawriter SQL Server Contributor
Service Bus Azure Service Bus Data Receiver Azure Service Bus Data Sender Azure Service Bus Data Owner

Custom Roles

Define custom roles for fine-grained control:

# Custom role: Can read secrets but not delete
$customRole = @{
    Name = 'KeyVault Secret Reader'
    Description = 'Can read (get) secrets only'
    Actions = @('Microsoft.KeyVault/vaults/secrets/read')
    NotActions = @('Microsoft.KeyVault/vaults/secrets/delete')
    AssignableScopes = @("/subscriptions/")
}

$role = New-AzRoleDefinition -InputObject $customRole

New-AzRoleAssignment `
  -ObjectId $appIdentity.PrincipalId `
  -RoleDefinitionId $role.Id `
  -Scope $scope

Code Patterns: No Secrets

Pattern 1: App Service → Azure SQL

Use managed identity to authenticate:

using Azure.Identity;
using Microsoft.Data.SqlClient;

public class OrderRepository
{
    private readonly string _connectionString;

    public OrderRepository(IConfiguration config)
    {
        // No password, no credentials
        _connectionString = "Server=orders-db.database.windows.net;Database=Orders;";
    }

    public async Task<Order> GetOrderAsync(int orderId)
    {
        // DefaultAzureCredential uses managed identity in App Service
        var credential = new DefaultAzureCredential();

        using (var connection = new SqlConnection(_connectionString))
        {
            // Get access token from Entra ID
            var token = await credential.GetTokenAsync(
                new TokenRequestContext(scopes: new[] { "https://database.windows.net/.default" }));

            connection.AccessToken = token.Token;
            await connection.OpenAsync();

            using (var command = connection.CreateCommand())
            {
                command.CommandText = "SELECT * FROM Orders WHERE OrderId = @id";
                command.Parameters.AddWithValue("@id", orderId);

                using (var reader = await command.ExecuteReaderAsync())
                {
                    if (await reader.ReadAsync())
                    {
                        return new Order { /* map from reader */ };
                    }
                }
            }
        }

        return null;
    }
}

Pattern 2: App Service → Azure Storage

Access blobs without connection string:

using Azure.Identity;
using Azure.Storage.Blobs;

public class DocumentService
{
    private readonly BlobContainerClient _containerClient;

    public DocumentService(IConfiguration config)
    {
        var accountUri = new Uri("https://mystore.blob.core.windows.net");
        var credential = new DefaultAzureCredential();

        _containerClient = new BlobContainerClient(
            new Uri($"{accountUri}/documents"),
            credential);
    }

    public async Task<Stream> DownloadDocumentAsync(string fileName)
    {
        // Managed identity automatically authenticated via RBAC
        var blobClient = _containerClient.GetBlobClient(fileName);
        BlobDownloadInfo download = await blobClient.DownloadAsync();
        return download.Content;
    }

    public async Task UploadDocumentAsync(string fileName, Stream content)
    {
        var blobClient = _containerClient.GetBlobClient(fileName);
        await blobClient.UploadAsync(content, overwrite: true);
    }
}

Pattern 3: App Service → Key Vault

Retrieve secrets without storing them:

using Azure.Identity;
using Azure.Security.KeyVault.Secrets;

public class ConfigurationService
{
    private readonly SecretClient _secretClient;

    public ConfigurationService(IConfiguration config)
    {
        var vaultUri = new Uri("https://mykeyvault.vault.azure.net/");
        var credential = new DefaultAzureCredential();
        _secretClient = new SecretClient(vaultUri, credential);
    }

    public async Task<string> GetDatabasePasswordAsync()
    {
        // Retrieve secret at runtime (never stored in code/config)
        KeyVaultSecret secret = await _secretClient.GetSecretAsync("db-password");
        return secret.Value;
    }

    public async Task<string> GetApiKeyAsync(string keyName)
    {
        KeyVaultSecret secret = await _secretClient.GetSecretAsync(keyName);
        return secret.Value;
    }
}

Pattern 4: Azure Function → Event Hub

Receive messages using managed identity:

using Azure.Identity;
using Azure.Messaging.EventHubs.Consumer;

public static class EventProcessorFunction
{
    [FunctionName("ProcessEvents")]
    public static async Task Run(
        [TimerTrigger("0 */1 * * * *")] TimerInfo myTimer,
        ILogger log)
    {
        var credential = new DefaultAzureCredential();
        var fullyQualifiedNamespace = "myeventhub.servicebus.windows.net";

        var consumerClient = new EventHubConsumerClient(
            EventHubConsumerClient.DefaultConsumerGroupName,
            fullyQualifiedNamespace,
            "my-hub",
            credential);

        try
        {
            // List partition IDs
            string[] partitionIds = await consumerClient.GetPartitionIdsAsync();

            foreach (string partitionId in partitionIds)
            {
                EventPosition startingPosition = EventPosition.Latest;
                using (var partitionReceiver = new PartitionReceiver(
                    EventHubConsumerClient.DefaultConsumerGroupName,
                    partitionId,
                    startingPosition,
                    fullyQualifiedNamespace,
                    "my-hub",
                    credential))
                {
                    IEnumerable<EventData> events = await partitionReceiver.ReceiveBatchAsync(
                        maximumEventCount: 100,
                        maximumWaitTime: TimeSpan.FromSeconds(5));

                    foreach (EventData eventData in events)
                    {
                        log.LogInformation($"Event: {Encoding.UTF8.GetString(eventData.Body.ToArray())}");
                    }
                }
            }
        }
        finally
        {
            await consumerClient.CloseAsync();
        }
    }
}

Pattern 5: Container → CosmosDB

Access CosmosDB with managed identity:

using Azure.Identity;
using Microsoft.Azure.Cosmos;

public class OrderDataAccess
{
    private readonly Container _container;

    public OrderDataAccess(IConfiguration config)
    {
        var endpoint = "https://mycosmosdb.documents.azure.com:443/";
        var credential = new DefaultAzureCredential();

        var client = new CosmosClient(endpoint, credential);
        _container = client.GetDatabase("OrdersDB").GetContainer("Orders");
    }

    public async Task<Order> GetOrderAsync(string orderId)
    {
        ItemResponse<Order> response = await _container.ReadItemAsync<Order>(
            orderId,
            new PartitionKey(orderId));

        return response.Resource;
    }
}

Dependency Injection Setup

ASP.NET Core Configuration

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Register managed identity credential
builder.Services.AddSingleton<TokenCredential>(new DefaultAzureCredential());

// Register services that use managed identity
builder.Services.AddScoped<OrderRepository>();
builder.Services.AddScoped<DocumentService>();
builder.Services.AddScoped<ConfigurationService>();

// Use DefaultAzureCredential for all Azure SDK clients
builder.Services.AddSingleton(x =>
{
    var credential = x.GetRequiredService<TokenCredential>();
    return new BlobContainerClient(
        new Uri("https://mystore.blob.core.windows.net/documents"),
        credential);
});

var app = builder.Build();
// ... rest of configuration

Service-to-Service Authentication

Workload Identity Federation

Enable GitHub Actions to authenticate to Azure without federated credentials:

# Register GitHub app in Entra ID
$app = New-AzADApplication `
  -DisplayName 'github-actions' `
  -SignInAudience 'AzureADMyOrg'

# Create service principal
$sp = New-AzADServicePrincipal `
  -ApplicationId $app.AppId

# Create federated credential for GitHub repo
$credential = @{
    name          = 'github-federated'
    issuer        = 'https://token.actions.githubusercontent.com'
    subject       = 'repo:my-org/my-repo:ref:refs/heads/main'
    audiences     = @('api://AzureADTokenExchange')
    description   = 'GitHub Actions'
}

Update-AzADApplication `
  -ObjectId $app.Id `
  -FederatedIdentityCredentials @($credential)

# Assign role to service principal
New-AzRoleAssignment `
  -ObjectId $sp.Id `
  -RoleDefinitionName 'Contributor' `
  -Scope "/subscriptions/.../resourceGroups/prod-rg"

Monitoring and Auditing

Track Managed Identity Usage

# View managed identity role assignments
Get-AzRoleAssignment -ObjectId $appIdentity.PrincipalId

# Audit access logs
Get-AzLog -ResourceGroup 'prod-rg' `
  -StartTime (Get-Date).AddDays(-7) `
  -Caller $appIdentity.PrincipalId |
  Select-Object -Property EventTimestamp, OperationName, Status

Azure Policy Enforcement

Enforce managed identity usage:

{
  "mode": "All",
  "policyRule": {
    "if": {
      "allOf": [
        {
          "field": "type",
          "equals": "Microsoft.Web/sites"
        },
        {
          "field": "identity.type",
          "notIn": ["SystemAssigned", "UserAssigned"]
        }
      ]
    },
    "then": {
      "effect": "deny"
    }
  }
}

Base Coat Assets

  • Agent: agents/identity-architect.agent.md
  • Instruction: instructions/zero-trust-identity.instructions.md
  • Skill: skills/azure-rbac/

References