Back to Blog
3 min read

Azure Key Vault Secret Rotation: Automating Credential Management

While managed identities eliminate many secrets, some credentials still require management. Azure Key Vault’s automatic rotation capabilities help maintain security without manual intervention.

Automatic Rotation for Storage Accounts

// Enable automatic rotation for storage account keys
resource keyVault 'Microsoft.KeyVault/vaults@2021-11-01-preview' = {
  name: keyVaultName
  location: location
  properties: {
    sku: {
      family: 'A'
      name: 'standard'
    }
    tenantId: subscription().tenantId
    enableRbacAuthorization: true
    enableSoftDelete: true
    softDeleteRetentionInDays: 90
  }
}

resource storageAccountKey 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = {
  parent: keyVault
  name: 'storage-account-key'
  properties: {
    value: storageAccount.listKeys().keys[0].value
    attributes: {
      enabled: true
      exp: dateTimeAdd(utcNow(), 'P90D')  // Expire in 90 days
    }
    contentType: 'application/vnd.ms-StorageAccountAccessKey'
  }
}

Event-Based Rotation with Azure Functions

using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;

public class SecretRotationFunction
{
    private readonly SecretClient _secretClient;

    public SecretRotationFunction()
    {
        var credential = new DefaultAzureCredential();
        _secretClient = new SecretClient(
            new Uri(Environment.GetEnvironmentVariable("KeyVaultUri")),
            credential);
    }

    [FunctionName("RotateStorageKey")]
    public async Task RotateStorageKey(
        [EventGridTrigger] EventGridEvent eventGridEvent,
        ILogger log)
    {
        var secretName = eventGridEvent.Subject;

        if (eventGridEvent.EventType == "Microsoft.KeyVault.SecretNearExpiry")
        {
            log.LogInformation($"Secret {secretName} is near expiry, rotating...");

            // Get current secret metadata
            var currentSecret = await _secretClient.GetSecretAsync(secretName);
            var tags = currentSecret.Value.Properties.Tags;

            if (tags.TryGetValue("ResourceId", out var resourceId))
            {
                // Rotate the storage key
                var newKey = await RotateStorageAccountKeyAsync(resourceId);

                // Update the secret
                await _secretClient.SetSecretAsync(
                    secretName,
                    newKey,
                    new SecretContentType("application/vnd.ms-StorageAccountAccessKey"));

                log.LogInformation($"Successfully rotated secret {secretName}");
            }
        }
    }

    private async Task<string> RotateStorageAccountKeyAsync(string resourceId)
    {
        var credential = new DefaultAzureCredential();
        var armClient = new ArmClient(credential);

        var storageAccount = armClient.GetStorageAccountResource(new ResourceIdentifier(resourceId));

        // Regenerate key1
        var keys = await storageAccount.RegenerateKeyAsync(
            new StorageAccountRegenerateKeyContent("key1"));

        return keys.Value.Keys.First(k => k.KeyName == "key1").Value;
    }
}

Event Grid Configuration

// Configure Event Grid for secret expiry notifications
resource eventGridSubscription 'Microsoft.EventGrid/systemTopics/eventSubscriptions@2021-12-01' = {
  parent: keyVaultTopic
  name: 'secret-expiry-subscription'
  properties: {
    destination: {
      endpointType: 'AzureFunction'
      properties: {
        resourceId: functionApp.id
        maxEventsPerBatch: 1
        preferredBatchSizeInKilobytes: 64
      }
    }
    filter: {
      includedEventTypes: [
        'Microsoft.KeyVault.SecretNearExpiry'
        'Microsoft.KeyVault.SecretExpired'
      ]
      subjectBeginsWith: ''
      subjectEndsWith: ''
    }
    eventDeliverySchema: 'EventGridSchema'
    retryPolicy: {
      maxDeliveryAttempts: 30
      eventTimeToLiveInMinutes: 1440
    }
  }
}

resource keyVaultTopic 'Microsoft.EventGrid/systemTopics@2021-12-01' = {
  name: 'keyvault-events'
  location: location
  properties: {
    source: keyVault.id
    topicType: 'Microsoft.KeyVault.vaults'
  }
}

SQL Server Password Rotation

public class SqlPasswordRotator
{
    private readonly SecretClient _secretClient;
    private readonly string _sqlServerResourceId;

    public async Task RotateSqlPasswordAsync(string secretName)
    {
        // Generate new secure password
        var newPassword = GenerateSecurePassword();

        // Update SQL Server
        await UpdateSqlServerPasswordAsync(newPassword);

        // Update Key Vault secret
        var secret = new KeyVaultSecret(secretName, newPassword)
        {
            Properties =
            {
                ExpiresOn = DateTimeOffset.UtcNow.AddDays(90),
                Tags =
                {
                    ["ResourceId"] = _sqlServerResourceId,
                    ["RotatedAt"] = DateTime.UtcNow.ToString("O")
                }
            }
        };

        await _secretClient.SetSecretAsync(secret);

        // Store old password temporarily for rollback
        var oldSecretName = $"{secretName}-previous";
        await _secretClient.SetSecretAsync(
            oldSecretName,
            await GetCurrentPasswordAsync(secretName));
    }

    private string GenerateSecurePassword()
    {
        const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";
        var random = RandomNumberGenerator.Create();
        var bytes = new byte[32];
        random.GetBytes(bytes);

        return new string(bytes.Select(b => chars[b % chars.Length]).ToArray());
    }

    private async Task UpdateSqlServerPasswordAsync(string newPassword)
    {
        var credential = new DefaultAzureCredential();
        var armClient = new ArmClient(credential);

        var sqlServer = armClient.GetSqlServerResource(new ResourceIdentifier(_sqlServerResourceId));

        await sqlServer.UpdateAsync(WaitUntil.Completed, new SqlServerPatch
        {
            AdministratorLoginPassword = newPassword
        });
    }
}

Monitoring Rotation

// Track secret rotation events
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.KEYVAULT"
| where OperationName in ("SecretSet", "SecretGet", "SecretNearExpiry")
| project
    TimeGenerated,
    OperationName,
    SecretName = id_s,
    CallerIPAddress,
    ResultType
| order by TimeGenerated desc

// Alert on rotation failures
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.KEYVAULT"
| where OperationName == "SecretSet"
| where ResultType != "Success"
| project
    TimeGenerated,
    SecretName = id_s,
    ResultType,
    ResultDescription

Best Practices

  1. Set appropriate expiry - 90 days is common
  2. Test rotation thoroughly - Verify applications handle rotation
  3. Keep previous version - Allow rollback if needed
  4. Monitor closely - Alert on rotation failures
  5. Stagger rotations - Avoid rotating everything at once

Automated secret rotation ensures credentials remain secure while reducing operational burden.

Michael John Peña

Michael John Peña

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