Back to Blog
4 min read

Secrets Management Patterns: Best Practices for Azure

Effective secrets management is crucial for security. Let’s explore patterns and best practices for managing secrets in Azure environments.

Secret Categories

CategoryExampleRecommended Approach
Azure ServicesStorage, SQLManaged Identity
External APIsThird-party APIsKey Vault
CertificatesTLS, AuthKey Vault or Managed
Connection StringsLegacy systemsKey Vault with rotation
Encryption KeysData encryptionKey Vault Keys

Centralized Secret Management

// Central Key Vault setup
resource keyVault 'Microsoft.KeyVault/vaults@2021-11-01-preview' = {
  name: 'kv-${environment}-secrets'
  location: location
  properties: {
    sku: {
      family: 'A'
      name: 'premium'  // For HSM-backed keys
    }
    tenantId: subscription().tenantId
    enableRbacAuthorization: true
    enableSoftDelete: true
    softDeleteRetentionInDays: 90
    enablePurgeProtection: true
    networkAcls: {
      defaultAction: 'Deny'
      bypass: 'AzureServices'
      virtualNetworkRules: [
        {
          id: appSubnet.id
        }
      ]
      ipRules: [
        {
          value: allowedIpRange
        }
      ]
    }
  }
}

// RBAC assignments
resource secretsUserRole 'Microsoft.Authorization/roleAssignments@2020-10-01-preview' = {
  scope: keyVault
  name: guid(keyVault.id, appServiceIdentity, 'secrets-user')
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')  // Key Vault Secrets User
    principalId: appServiceIdentity
    principalType: 'ServicePrincipal'
  }
}

Application Configuration Pattern

// Program.cs - ASP.NET Core
using Azure.Identity;
using Azure.Extensions.AspNetCore.Configuration.Secrets;

var builder = WebApplication.CreateBuilder(args);

// Add Key Vault as configuration source
var keyVaultUri = builder.Configuration["KeyVaultUri"];
if (!string.IsNullOrEmpty(keyVaultUri))
{
    builder.Configuration.AddAzureKeyVault(
        new Uri(keyVaultUri),
        new DefaultAzureCredential(),
        new AzureKeyVaultConfigurationOptions
        {
            ReloadInterval = TimeSpan.FromMinutes(5)
        });
}

// Use configuration
builder.Services.Configure<ApiOptions>(
    builder.Configuration.GetSection("ExternalApi"));
// Key Vault secrets map to configuration
// Secret name: ExternalApi--ApiKey
// Configuration path: ExternalApi:ApiKey
{
  "ExternalApi": {
    "ApiKey": "from-key-vault",
    "BaseUrl": "https://api.external.com"
  }
}

Secrets in Kubernetes

# secrets-provider-class.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: azure-keyvault-secrets
spec:
  provider: azure
  parameters:
    usePodIdentity: "false"
    useVMManagedIdentity: "true"
    userAssignedIdentityID: "$IDENTITY_CLIENT_ID"
    keyvaultName: "kv-prod-secrets"
    objects: |
      array:
        - |
          objectName: database-connection-string
          objectType: secret
        - |
          objectName: api-key
          objectType: secret
    tenantId: "$TENANT_ID"
  secretObjects:
    - secretName: app-secrets
      type: Opaque
      data:
        - objectName: database-connection-string
          key: DB_CONNECTION
        - objectName: api-key
          key: API_KEY
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      containers:
        - name: app
          volumeMounts:
            - name: secrets-store
              mountPath: "/mnt/secrets"
              readOnly: true
          env:
            - name: DB_CONNECTION
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: DB_CONNECTION
      volumes:
        - name: secrets-store
          csi:
            driver: secrets-store.csi.k8s.io
            readOnly: true
            volumeAttributes:
              secretProviderClass: azure-keyvault-secrets

Secret Reference Pattern

// Generic secret provider abstraction
public interface ISecretProvider
{
    Task<string> GetSecretAsync(string secretName);
    Task<T> GetSecretAsync<T>(string secretName);
}

public class KeyVaultSecretProvider : ISecretProvider
{
    private readonly SecretClient _client;
    private readonly IMemoryCache _cache;

    public KeyVaultSecretProvider(string vaultUri, IMemoryCache cache)
    {
        _client = new SecretClient(new Uri(vaultUri), new DefaultAzureCredential());
        _cache = cache;
    }

    public async Task<string> GetSecretAsync(string secretName)
    {
        return await _cache.GetOrCreateAsync($"secret:{secretName}", async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
            var secret = await _client.GetSecretAsync(secretName);
            return secret.Value.Value;
        });
    }

    public async Task<T> GetSecretAsync<T>(string secretName)
    {
        var secretValue = await GetSecretAsync(secretName);
        return JsonSerializer.Deserialize<T>(secretValue);
    }
}

// Usage with dependency injection
services.AddSingleton<ISecretProvider>(sp =>
    new KeyVaultSecretProvider(
        configuration["KeyVaultUri"],
        sp.GetRequiredService<IMemoryCache>()));

DevOps Pipeline Secrets

# azure-pipelines.yml
variables:
  - group: Production-Secrets  # Variable group linked to Key Vault

stages:
  - stage: Deploy
    jobs:
      - deployment: Deploy
        environment: Production
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureKeyVault@2
                  inputs:
                    azureSubscription: 'Azure-Connection'
                    KeyVaultName: 'kv-prod-secrets'
                    SecretsFilter: '*'
                    RunAsPreJob: true

                - task: AzureCLI@2
                  inputs:
                    azureSubscription: 'Azure-Connection'
                    scriptType: bash
                    scriptLocation: inlineScript
                    inlineScript: |
                      # Secrets are available as environment variables
                      az webapp config appsettings set \
                        --name $(WebAppName) \
                        --resource-group $(ResourceGroup) \
                        --settings "ApiKey=$(ApiKey)"

Audit and Compliance

// Comprehensive secret access audit
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.KEYVAULT"
| where Category == "AuditEvent"
| project
    TimeGenerated,
    OperationName,
    ResultType,
    CallerIPAddress,
    Resource = id_s,
    Identity = identity_claim_upn_s,
    ClientInfo = client_info_s
| where OperationName in ("SecretGet", "SecretSet", "SecretDelete", "SecretList")
| summarize
    AccessCount = count(),
    UniqueIdentities = dcount(Identity)
    by Resource, OperationName, bin(TimeGenerated, 1h)

Effective secrets management combines centralization, automation, and proper access controls to maintain security while enabling developer productivity.

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.