Key Vault — Automating Secret Rotation and Certificate Renewal
The Problem
Your team manually rotates secrets every 90 days:
- Database connection strings are hardcoded in apps
- API keys never expire or get rotated
- SSL certificates expire unexpectedly causing downtime
- Developers store secrets in config files (security risk)
- No visibility into who accessed which secrets
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
| Practice | Benefit |
|---|---|
| Enable auto-renewal for certificates | No manual certificate management |
| Use managed identities | Secure access to Key Vault |
| Store connection strings | Centralize all secrets |
| Set expiry alerts | Proactive notification |
| Version secrets | Easy rollback |
| Test rotation | Verify it works |
Summary
- Implement timer-triggered functions for rotating secrets
- Use Key Vault SDK with Managed Identity
- Configure certificates for auto-renewal
- Update applications to use Key Vault references
- Set up notifications for rotation events