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
- Set appropriate expiry - 90 days is common
- Test rotation thoroughly - Verify applications handle rotation
- Keep previous version - Allow rollback if needed
- Monitor closely - Alert on rotation failures
- Stagger rotations - Avoid rotating everything at once
Automated secret rotation ensures credentials remain secure while reducing operational burden.