← Back to ArticlesKey Vault

Key Vault — Automating Secret Rotation and Certificate Renewal

Complete guide to implementing automated secret rotation, certificate lifecycle management, and key rotation in Azure Key Vault.

Key Vault — Automating Secret Rotation and Certificate Renewal

The Problem

Your team manually rotates secrets every 90 days:

You need automated rotation to eliminate manual work and reduce risk.

Secret Rotation Architecture

┌─────────────────────────────────────────────────────────────────┐
│                    Automated Rotation Flow                      │
└─────────────────────────────────────────────────────────────────┘

  ┌────────────┐    ┌──────────────┐    ┌─────────────────────┐
  │  Schedule  │───▶│  Rotation    │───▶│  Update Secrets     │
  │  (Timer)   │    │  Function    │    │  in Applications    │
  └────────────┘    └──────────────┘    └─────────────────────┘
        │                   │                      │
        ▼                   ▼                      ▼
  ┌────────────┐    ┌──────────────┐    ┌─────────────────────┐
  │ Key Vault  │    │  Generate    │    │  Azure Functions    │
  │  Secrets   │    │  New Secret  │    │  (via Managed ID)   │
  └────────────┘    └──────────────┘    └─────────────────────┘
                          │
                          ▼
              ┌──────────────────────┐
              │  Notification        │
              │  (Teams/Slack)       │
              └──────────────────────┘

Solution Implementation

Step 1: Rotation Function for Database Passwords

// rotate-database-password/index.ts
import { AzureFunction, Context, Timer } from "@azure/functions";
import { SecretClient } from "@azure/keyvault-secrets";
import { DefaultAzureCredential } from "@azure/identity";

interface RotationConfig {
    vaultName: string;
    secretName: string;
    resourceGroup: string;
    serverName: string;
    databaseName: string;
    adminLogin: string;
}

const rotateTrigger: AzureFunction = async function (context: Context, timer: Timer): Promise<void> {
    if (timer.isPast) {
        context.log("Starting database password rotation");
        
        const configs: RotationConfig[] = [
            {
                vaultName: "production-kv",
                secretName: "db-master-password",
                resourceGroup: "prod-rg",
                serverName: "prod-sql-server.database.windows.net",
                databaseName: "main-db",
                adminLogin: "admin"
            }
        ];
        
        for (const config of configs) {
            try {
                await rotateDatabasePassword(context, config);
            } catch (error) {
                context.log.error(`Failed to rotate ${config.secretName}: ${error}`);
                await sendAlert(config.secretName, error);
            }
        }
    }
};

async function rotateDatabasePassword(context: Context, config: RotationConfig): Promise<void> {
    const credential = new DefaultAzureCredential();
    const vaultUrl = `https://${config.vaultName}.vault.azure.net/`;
    const client = new SecretClient(vaultUrl, credential);
    
    // 1. Get current secret (to keep connection string format)
    const currentSecret = await client.getSecret(config.secretName);
    const currentConnectionString = currentSecret.value;
    
    // 2. Generate new password
    const newPassword = generateSecurePassword(32);
    
    // 3. Update SQL Server password
    await updateSqlPassword(config, newPassword);
    
    // 4. Construct new connection string
    const newConnectionString = buildConnectionString(
        config.serverName,
        config.databaseName,
        config.adminLogin,
        newPassword
    );
    
    // 5. Update secret in Key Vault (create new version)
    await client.setSecret(config.secretName, newConnectionString, {
        enabled: true,
        expiresOn: calculateExpiry(90)
    });
    
    // 6. Notify success
    context.log(`Successfully rotated ${config.secretName}`);
    await sendNotification(config.secretName, "success");
}

function generateSecurePassword(length: number): string {
    const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
    const array = new Uint8Array(length);
    crypto.getRandomValues(array);
    return Array.from(array, byte => charset[byte % charset.length]).join("");
}

function buildConnectionString(server: string, database: string, user: string, password: string): string {
    return `Server=tcp:${server},1433;Database=${database};User Id=${user}@${server.replace('.database.windows.net', '')};Password=${password};Encrypt=true;TrustServerCertificate=false;Connection Timeout=30;`;
}

function calculateExpiry(days: number): Date {
    const date = new Date();
    date.setDate(date.getDate() + days);
    return date;
}

async function sendNotification(secretName: string, status: string): Promise<void> {
    // Send to Teams/Slack/PagerDuty
    console.log(`Notification: ${secretName} rotation ${status}`);
}

export default rotateTrigger;

Step 2: API Key Rotation

// rotate-api-key/index.ts
import { AzureFunction, Context, Timer } from "@azure/functions";
import { SecretClient } from "@azure/keyvault-secrets";
import { DefaultAzureCredential } from "@azure/identity";

interface ApiKeyConfig {
    vaultName: string;
    secretName: string;
    serviceName: string;
    rotationCallbackUrl: string;
}

const rotateTrigger: AzureFunction = async function (context: Context, timer: Timer): Promise<void> {
    if (timer.isPast) {
        const configs: ApiKeyConfig[] = [
            {
                vaultName: "production-kv",
                secretName: "stripe-api-key",
                serviceName: "Stripe",
                rotationCallbackUrl: "https://api.stripe.com/v1/api_keys/rotate"
            },
            {
                vaultName: "production-kv",
                secretName: "sendgrid-api-key",
                serviceName: "SendGrid",
                rotationCallbackUrl: "https://api.sendgrid.com/v3/api_keys/rotate"
            }
        ];
        
        for (const config of configs) {
            await rotateApiKey(context, config);
        }
    }
};

async function rotateApiKey(context: Context, config: ApiKeyConfig): Promise<void> {
    const credential = new DefaultAzureCredential();
    const vaultUrl = `https://${config.vaultName}.vault.azure.net/`;
    const client = new SecretClient(vaultUrl, credential);
    
    // 1. Generate new API key
    const newApiKey = generateApiKey();
    
    // 2. Call service to register new key and disable old one
    await registerNewApiKey(config, newApiKey);
    
    // 3. Store new key in Key Vault
    await client.setSecret(config.secretName, newApiKey, {
        enabled: true,
        contentType: "api-key",
        expiresOn: calculateExpiry(90)
    });
    
    context.log(`Rotated API key for ${config.serviceName}`);
}

async function registerNewApiKey(config: ApiKeyConfig, newKey: string): Promise<void> {
    // Call the external service API to register the new key
    const response = await fetch(config.rotationCallbackUrl, {
        method: "POST",
        headers: {
            "Authorization": `Bearer ${process.env["SERVICE_API_TOKEN"]}`,
            "Content-Type": "application/json"
        },
        body: JSON.stringify({ api_key: newKey })
    });
    
    if (!response.ok) {
        throw new Error(`Failed to register new key: ${response.statusText}`);
    }
}

function generateApiKey(): string {
    return "sk_" + Array.from(crypto.getRandomValues(new Uint8Array(32)))
        .map(b => b.toString(16).padStart(2, "0"))
        .join("");
}

export default rotateTrigger;

Step 3: Certificate Auto-Renewal

// check-certificates/index.ts
import { AzureFunction, Context, Timer } from "@azure/functions";
import { CertificateClient } from "@azure/keyvault-certificates";
import { DefaultAzureCredential } from "@azure/identity";

const checkCertificates: AzureFunction = async function (context: Context, timer: Timer): Promise<void> {
    const vaultName = process.env["KEY_VAULT_NAME"]!;
    const credential = new DefaultAzureCredential();
    const vaultUrl = `https://${vaultName}.vault.azure.net/`;
    const client = new CertificateClient(vaultUrl, credential);
    
    // Get all certificates
    const certificates = client.listPropertiesOfCertificates();
    
    for await (const cert of certificates) {
        if (needsRenewal(cert)) {
            await renewCertificate(context, client, cert);
        }
    }
};

function needsRenewal(cert: any): boolean {
    if (!cert.properties.expiresOn) return false;
    
    const expiryDate = new Date(cert.properties.expiresOn);
    const daysUntilExpiry = (expiryDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24);
    
    // Renew if within 30 days
    return daysUntilExpiry <= 30;
}

async function renewCertificate(context: Context, client: CertificateClient, cert: any): Promise<void> {
    try {
        context.log(`Renewing certificate: ${cert.name}`);
        
        // Create renewal policy
        const policy = {
            lifetimePercentage: 80,
            daysBeforeExpiry: 30
        };
        
        // Create new version of certificate
        // For CA-issued certs, this triggers new issuance
        const poller = await client.beginCreateCertificate(cert.name, {
            certificatePolicy: {
                issuerParameters: { name: "Let's Encrypt" },
                keyProperties: {
                    reuseKey: true,
                    keyType: "RSA",
                    keySize: 2048
                },
                lifetimeActions: [{
                    action: "AutoRenew",
                    trigger: {
                        lifetimePercentage: policy.lifetimePercentage,
                        daysBeforeExpiry: policy.daysBeforeExpiry
                    }
                }]
            }
        });
        
        await poller.pollUntilDone();
        
        context.log(`Certificate ${cert.name} renewed successfully`);
        await sendNotification(cert.name, "renewed");
    } catch (error) {
        context.log.error(`Failed to renew ${cert.name}: ${error}`);
        await sendAlert(cert.name, error);
    }
}

export default checkCertificates;

Step 4: Connection String Rotation for Apps

// rotate-connection-strings/index.ts
import { AzureFunction, Context, Timer } from "@azure/functions";
import { SecretClient } from "@azure/keyvault-secrets";
import { DefaultAzureCredential } from "@azure/identity";

// Azure SQL connection string rotation
async function rotateSqlConnectionString(
    vaultName: string,
    secretName: string,
    serverName: string,
    databaseName: string,
    adminUser: string
): Promise<void> {
    const credential = new DefaultAzureCredential();
    const vaultUrl = `https://${vaultName}.vault.azure.net/`;
    const client = new SecretClient(vaultUrl, credential);
    
    // Generate new password
    const newPassword = generatePassword(32);
    
    // Update SQL
    await executeSqlCommand(serverName, adminUser, newPassword);
    
    // Update connection string secret
    const newConnectionString = `Server=tcp:${serverName},1433;Database=${databaseName};User Id=${adminUser}@${serverName.replace('.database.windows.net', '')};Password=${newPassword};Encrypt=true;Connection Timeout=30;`;
    
    await client.setSecret(secretName, newConnectionString, {
        expiresOn: addDays(90)
    });
}

// Redis connection string rotation
async function rotateRedisConnectionString(
    vaultName: string,
    secretName: string,
    redisName: string,
    resourceGroup: string
): Promise<void> {
    // Regenerate Redis access keys
    const { exec } = require('child_process');
    const result = await execAsync(`az redis regenerate-keys --name ${redisName} --resource-group ${resourceGroup}`);
    
    const newKey = JSON.parse(result.stdout).primaryKey;
    
    // Update secret
    const credential = new DefaultAzureCredential();
    const vaultUrl = `https://${vaultName}.vault.azure.net/`;
    const client = new SecretClient(vaultUrl, credential);
    
    await client.setSecret(secretName, newKey, {
        expiresOn: addDays(90)
    });
}

Step 5: Application Integration (Using Key Vault References)

// Configure App Service to use Key Vault references
// In App Service Configuration:
/*
  {
    "name": "DATABASE_CONNECTION_STRING",
    "value": "@Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/db-connection-string/)",
    "slotSetting": false
  }
*/

// Or programmatically - refresh without restart
import { SecretClient } from "@azure/keyvault-secrets";

class SecretRefresher {
    private client: SecretClient;
    private cachedValues: Map<string, { value: string; expires: Date }> = new Map();
    private refreshInterval: number = 300000; // 5 minutes
    
    constructor(vaultUrl: string) {
        const credential = new DefaultAzureCredential();
        this.client = new SecretClient(vaultUrl, credential);
    }
    
    async getSecret(secretName: string): Promise<string> {
        const cached = this.cachedValues.get(secretName);
        
        // Return cached if valid
        if (cached && cached.expires > new Date()) {
            return cached.value;
        }
        
        // Fetch fresh
        const secret = await this.client.getSecret(secretName);
        this.cachedValues.set(secretName, {
            value: secret.value!,
            expires: new Date(Date.now() + this.refreshInterval)
        });
        
        return secret.value!;
    }
    
    // Manually refresh (call after rotation)
    async refreshSecret(secretName: string): Promise<void> {
        this.cachedValues.delete(secretName);
        await this.getSecret(secretName);
    }
}

Step 6: Rotation Schedule Configuration

// function.json for rotation timers
{
  "bindings": [
    {
      "name": "timer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 1 * *",  // Monthly on 1st
      "runOnStartup": true
    },
    {
      "name": "timer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 */7 * *",  // Weekly on Sunday
      "runOnStartup": true
    }
  ]
}

Best Practices Summary

PracticeBenefit
Enable auto-renewal for certificatesNo manual certificate management
Use managed identitiesSecure access to Key Vault
Store connection stringsCentralize all secrets
Set expiry alertsProactive notification
Version secretsEasy rollback
Test rotationVerify it works

Summary