Secret Versioning & Rotation

Overview

Azure Key Vault automatically versions secrets on every update. Combined with rotation strategies and expiry notifications, you can build a fully automated secret lifecycle without application downtime.


How Versioning Works

Every time a secret is set (created or updated), Key Vault creates a new version identified by a unique GUID. The secret name remains the same.

https://my-vault.vault.azure.net/secrets/DbPassword
  → latest version (always returned by default)

https://my-vault.vault.azure.net/secrets/DbPassword/abc123def456
  → specific version

Key Behaviors

  • Latest version is returned when no version is specified in the URI.
  • Old versions remain accessible by their version ID until explicitly disabled or deleted.
  • Soft delete protects against accidental deletion (enabled by default on new vaults).

Listing Secret Versions

Azure CLI

az keyvault secret list-versions \
  --vault-name my-vault \
  --name DbPassword \
  --query "[].{id:id, created:attributes.created, enabled:attributes.enabled}" \
  -o table

.NET SDK

var client = new SecretClient(new Uri(vaultUrl), new DefaultAzureCredential());

await foreach (SecretProperties version in client.GetPropertiesOfSecretVersionsAsync("DbPassword"))
{
    Console.WriteLine($"Version: {version.Version}, Created: {version.CreatedOn}, Enabled: {version.Enabled}");
}

Setting Expiry on Secrets

Secrets can have an expiration date. Key Vault does not automatically delete expired secrets, but it emits events and marks them as expired.

# Linux/Cloud Shell:
az keyvault secret set \
  --vault-name my-vault \
  --name ApiKey \
  --value "my-secret-value" \
  --expires "$(date -u -d '+90 days' +%Y-%m-%dT%H:%M:%SZ)"

# macOS:
az keyvault secret set \
  --vault-name my-vault \
  --name ApiKey \
  --value "my-secret-value" \
  --expires "$(date -u -v+90d +%Y-%m-%dT%H:%M:%SZ)"

.NET SDK

var secret = new KeyVaultSecret("ApiKey", "my-secret-value");
secret.Properties.ExpiresOn = DateTimeOffset.UtcNow.AddDays(90);
await client.SetSecretAsync(secret);

Expiry Notifications with Event Grid

Key Vault emits events when secrets are near expiry:

EventTrigger
Microsoft.KeyVault.SecretNearExpiry30 days before expiry (fixed)
Microsoft.KeyVault.SecretExpiredOn expiration date
Microsoft.KeyVault.SecretNewVersionCreatedWhen a new version is created

Subscribe to Events

az eventgrid event-subscription create \
  --name secret-expiry-alert \
  --source-resource-id /subscriptions/{sub}/resourceGroups/my-rg/providers/Microsoft.KeyVault/vaults/my-vault \
  --endpoint https://my-function.azurewebsites.net/api/SecretExpiryHandler \
  --included-event-types Microsoft.KeyVault.SecretNearExpiry

Automated Rotation Strategies

Strategy 1: Event-Driven Rotation (Recommended)

  1. Set secret expiry (e.g., 90 days).
  2. Subscribe to SecretNearExpiry event.
  3. Azure Function regenerates the credential at the source.
  4. Function stores the new value in Key Vault.
  5. Applications using latest version pick up the change automatically.

Rotation Function Example (.NET)

[Function("RotateStorageKey")]
public async Task Run([EventGridTrigger] EventGridEvent eventGridEvent)
{
    var secretName = eventGridEvent.Subject;

    // 1. Regenerate key at the source
    var newKey = await RegenerateStorageKeyAsync();

    // 2. Store new version in Key Vault
    var client = new SecretClient(new Uri(vaultUrl), new DefaultAzureCredential());
    var secret = new KeyVaultSecret(secretName, newKey);
    secret.Properties.ExpiresOn = DateTimeOffset.UtcNow.AddDays(90);
    await client.SetSecretAsync(secret);
}

Strategy 2: Scheduled Rotation

Use a Timer-triggered Azure Function to rotate secrets on a fixed schedule:

[Function("ScheduledRotation")]
public async Task Run([TimerTrigger("0 0 0 1 */2 *")] TimerInfo timer)
{
    // Runs on the 1st of every 2nd month
    await RotateSecretAsync("DbPassword");
}

Dual-Version Rotation Pattern

For zero-downtime rotation of credentials that require propagation time:

  1. Primary secret is active (version N).
  2. Rotation creates secondary (version N+1) and activates it at the source.
  3. After propagation delay, disable version N.
  4. Applications always fetch the latest version.
// Disable old version after rotation
SecretProperties oldVersion = await client.GetSecretAsync("DbPassword", oldVersionId);
oldVersion.Enabled = false;
await client.UpdateSecretPropertiesAsync(oldVersion);

Application-Side Caching

Applications should cache secrets with a reasonable TTL and refresh periodically:

public class SecretCache
{
    private readonly SecretClient _client;
    private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(30);
    private string _cachedValue;
    private DateTimeOffset _cacheExpiry;

    public async Task<string> GetSecretAsync(string name)
    {
        if (_cachedValue != null && DateTimeOffset.UtcNow < _cacheExpiry)
            return _cachedValue;

        var secret = await _client.GetSecretAsync(name);
        _cachedValue = secret.Value.Value;
        _cacheExpiry = DateTimeOffset.UtcNow.Add(_cacheDuration);
        return _cachedValue;
    }
}

Best Practices

  1. Always set expiry dates on secrets to enforce rotation.
  2. Use Event Grid for near-expiry notifications rather than manual tracking.
  3. Automate rotation with Azure Functions — never rotate manually in production.
  4. Implement dual-version pattern for credentials that need propagation time.
  5. Cache secrets in applications with a 15-30 minute TTL to reduce Key Vault calls.
  6. Disable old versions after successful rotation rather than deleting immediately.
  7. Enable soft delete and purge protection to prevent accidental permanent loss.
  8. Monitor rotation success/failure with Application Insights alerts.

Summary

Secret versioning is automatic in Key Vault. Combine it with expiry dates, Event Grid notifications, and automated rotation functions to build a self-healing secret lifecycle. Applications that always fetch the latest version benefit from seamless rotation without redeployment.