Azure Key Vault — Secrets Management, Encryption, and Certificate Lifecycle
Azure Key Vault is Microsoft's cloud-native solution for safeguarding cryptographic keys, secrets, and certificates. It provides a secure, scalable foundation for protecting sensitive data in your applications and infrastructure.
Why Azure Key Vault?
Before diving into implementation, let's understand why Key Vault is essential:
- Centralized Secret Management: Store all secrets in one place instead of scattered across configuration files
- Security Boundary: Secrets never leave the vault; applications only receive values at runtime
- Access Control: Role-based access with Azure RBAC and vault-level policies
- Audit Logging: Every access is logged for compliance and security monitoring
- Encryption as a Service: Encrypt data without managing HSMs
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ Your Application │
├─────────────────────────────────────────────────────────────────┤
│ Azure Identity (Managed Identity / Service Principal) │
├─────────────────────────────────────────────────────────────────┤
│ Azure Key Vault │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Secrets │ │ Keys │ │ Certificates │ │
│ │ - API Keys │ │ - Encryption│ │ - TLS/SSL │ │
│ │ - Passwords │ │ - Signing │ │ - Client Auth │ │
│ │ - DB Conns │ │ - Wrapping │ │ - Wildcard │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Azure Storage (Optional: HSM-backed) │
└─────────────────────────────────────────────────────────────────┘
Key Vault Types
Azure offers three Key Vault tiers:
| Tier | Keys/Secrets | Certificates | HSM-Backed Keys | Price |
|---|---|---|---|---|
| Standard | 2,000 | 2,000 | Software-backed | Lower |
| Premium | 2,000 | 2,000 | HSM-protected (FIPS 140-2 L3) | Higher |
Use Premium when you need FIPS 140-2 Level 3 HSM protection for regulatory compliance.
Working with Secrets
Creating a Key Vault
# Create Resource Group
az group create --name keyvault-rg --location eastus
# Create Key Vault (Standard)
az keyvault create \
--name mykeyvault123 \
--resource-group keyvault-rg \
--location eastus \
--sku standard
# Create Key Vault (Premium - HSM-backed keys)
az keyvault create \
--name mykeyvault-premium \
--resource-group keyvault-rg \
--location eastus \
--sku premium
Adding Secrets via Azure CLI
# Store a simple secret
az keyvault secret set \
--vault-name mykeyvault123 \
--name db-connection-string \
--value "Server=tcp:myserver.database.windows.net,1433;Database=mydb;User=admin;Password=P@ssw0rd!;"
# Store a more complex secret (JSON)
az keyvault secret set \
--vault-name mykeyvault123 \
--name api-keys \
--value '{"stripe":"sk_live_xxx","sendgrid":"SG.xxx"}'
# Store with expiration
az keyvault secret set \
--vault-name mykeyvault123 \
--name temp-token \
--value "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
--expires "2024-12-31T23:59:59Z" \
--not-before "2024-01-01T00:00:00Z"
Retrieving Secrets in Code
Using Azure SDK (.NET)
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
public class KeyVaultService
{
private readonly SecretClient _secretClient;
public KeyVaultService(string vaultUri)
{
// Use Managed Identity in production
var credential = new ManagedIdentityCredential();
_secretClient = new SecretClient(new Uri(vaultUri), credential);
}
public async Task<string> GetSecretAsync(string secretName)
{
try
{
KeyVaultSecret secret = await _secretClient.GetSecretAsync(secretName);
return secret.Value;
}
catch (RequestFailedException ex) when (ex.Status == 404)
{
throw new KeyNotFoundException($"Secret '{secretName}' not found in vault.");
}
}
public async Task SetSecretAsync(string secretName, string secretValue)
{
var secret = new KeyVaultSecret(secretName, secretValue)
{
Properties =
{
ExpiresOn = DateTimeOffset.UtcNow.AddYears(1),
ContentType = "text/plain"
}
};
await _secretClient.SetSecretAsync(secret);
}
public async Task DeleteSecretAsync(string secretName)
{
await _secretClient.StartDeleteSecretAsync(secretName);
// Wait for purging to complete
await _secretClient.PurgeDeletedSecretAsync(secretName);
}
}
Using Azure SDK (Node.js)
import { SecretClient } from "@azure/keyvault-secrets";
import { DefaultAzureCredential } from "@azure/identity";
class KeyVaultService {
private client: SecretClient;
constructor(vaultUrl: string) {
const credential = new DefaultAzureCredential();
this.client = new SecretClient(vaultUrl, credential);
}
async getSecret(secretName: string): Promise<string> {
const poller = await this.client.beginGetSecret(secretName);
const secret = await poller.pollUntilDone();
return secret.value!;
}
async setSecret(secretName: string, value: string): Promise<void> {
const secret = {
name: secretName,
value: value,
properties: {
expiresOn: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),
contentType: "text/plain",
},
};
await this.client.setSecret(secret.name, secret.value, {
expiresOn: secret.properties.expiresOn,
contentType: secret.properties.contentType,
});
}
async listSecrets(): Promise<{ name: string; enabled: boolean }[]> {
const secrets: { name: string; enabled: boolean }[] = [];
for await (const secret of this.client.listPropertiesOfSecrets()) {
secrets.push({ name: secret.name, enabled: secret.enabled ?? false });
}
return secrets;
}
}
export const vaultService = new KeyVaultService(
process.env.AZURE_KEY_VAULT_URL || ""
);
Environment Variable Pattern
A common pattern is fetching secrets at application startup:
// config/secrets.ts
import { SecretClient } from "@azure/keyvault-secrets";
import { DefaultAzureCredential } from "@azure/identity";
export async function loadSecrets() {
const vaultUrl = process.env.AZURE_KEY_VAULT_URL!;
const credential = new DefaultAzureCredential();
const client = new SecretClient(vaultUrl, credential);
const secrets = await Promise.all([
client.getSecret("db-connection-string"),
client.getSecret("api-key"),
client.getSecret("jwt-secret"),
]);
process.env.DATABASE_URL = secrets[0].value;
process.env.API_KEY = secrets[1].value;
process.env.JWT_SECRET = secrets[2].value;
}
Working with Keys (Encryption)
Key Vault supports symmetric and asymmetric keys for encryption, signing, and key wrapping.
Creating Keys
# Create an RSA key pair
az keyvault key create \
--vault-name mykeyvault123 \
--name my-rsa-key \
--kty RSA \
--size 2048 \
--ops encrypt decrypt sign verify wrapKey unwrapKey
# Create an EC key (P-256)
az keyvault key create \
--vault-name mykeyvault123 \
--name my-ec-key \
--kty EC \
--curve P-256 \
--ops sign verify
# Import an existing key (from file)
az keyvault key import \
--vault-name mykeyvault123 \
--name imported-key \
--pem-file "./private-key.pem" \
--pem-password "optional-password"
Encrypting and Decrypting Data
using Azure.Identity;
using Azure.Security.KeyVault.Keys;
using Azure.Security.KeyVault.Keys.Cryptography;
public class KeyEncryptionService
{
private readonly KeyClient _keyClient;
private readonly CryptographyClient _cryptoClient;
public KeyEncryptionService(string vaultUri, string keyName)
{
var credential = new ManagedIdentityCredential();
_keyClient = new KeyClient(new Uri(vaultUri), credential);
// Get the key and create a cryptography client
KeyVaultKey key = await _keyClient.GetKeyAsync(keyName);
_cryptoClient = new CryptographyClient(key.Id, credential);
}
public byte[] Encrypt(byte[] plaintext)
{
EncryptResult result = _cryptoClient.Encrypt(
KeyAlgorithm.RsaOaep,
plaintext
);
return result.Ciphertext;
}
public byte[] Decrypt(byte[] ciphertext)
{
DecryptResult result = _cryptoClient.Decrypt(
KeyAlgorithm.RsaOaep,
ciphertext
);
return result.Plaintext;
}
}
// Usage
var encryptionService = new KeyEncryptionService(
"https://mykeyvault123.vault.azure.net/",
"my-rsa-key"
);
string sensitiveData = "Credit card number: 4111111111111111";
byte[] encrypted = encryptionService.Encrypt(
Encoding.UTF8.GetBytes(sensitiveData)
);
// Store encrypted data in database
string encryptedBase64 = Convert.ToBase64String(encrypted);
// Later, decrypt
byte[] decryptedBytes = encryptionService.Decrypt(encrypted);
string decrypted = Encoding.UTF8.GetString(decryptedBytes);
Key Rotation
Keys should be rotated periodically. Azure Key Vault supports automatic key rotation:
# Enable automatic key rotation (90-day rotation policy)
az keyvault key rotation-policy update \
--vault-name mykeyvault123 \
--name my-rsa-key \
--expires-in P90D \
--lifetime-actions type=rotate
# Manually rotate a key (creates a new version)
az keyvault key rotate \
--vault-name mykeyvault123 \
--name my-rsa-key
// Programmatic rotation check
public async Task<bool> IsKeyNearingExpiryAsync(string keyName)
{
KeyVaultKey key = await _keyClient.GetKeyAsync(keyName);
var expiresOn = key.Properties.ExpiresOn;
// Check if key expires within 30 days
return expiresOn.HasValue &&
expiresOn.Value < DateTimeOffset.UtcNow.AddDays(30);
}
Working with Certificates
Key Vault can manage SSL/TLS certificates, including auto-renewal from CA partners.
Creating Certificates
# Create a self-signed certificate
az keyvault certificate create \
--vault-name mykeyvault123 \
--name my-selfsigned-cert \
--policy "$(cat <<EOF
{
"issuerParameters": {
"name": "Self"
},
"keyProperties": {
"exportable": true,
"keySize": 2048,
"keyType": "RSA",
"reuseKey": false
},
"secretProperties": {
"contentType": "application/x-pkcs12"
},
"lifetimeActions": [
{
"trigger": {
"daysBeforeExpiry": 30
},
"action": {
"type": "AutoRenew"
}
}
],
"validityInMonths": 12
}
EOF
)"
# Create a certificate from CA (Let's Encrypt, DigiCert, etc.)
az keyvault certificate create \
--vault-name mykeyvault123 \
--name letsencrypt-cert \
--policy "$(cat <<EOF
{
"issuerParameters": {
"name": "Let's Encrypt",
"certTransparency": true
},
"keyProperties": {
"exportable": true,
"keySize": 2048,
"keyType": "RSA",
"reuseKey": true
},
"secretProperties": {
"contentType": "application/x-pkcs12"
},
"lifetimeActions": [
{
"trigger": { "daysBeforeExpiry": 30 },
"action": { "type": "AutoRenew" }
}
],
"validityInMonths": 3
}
EOF
)"
Using Certificates in Applications
// Download certificate for use in Node.js
import { CertificateClient } from "@azure/keyvault-certificates";
import { DefaultAzureCredential } from "@azure/identity";
async function getCertificatePem(): Promise<string> {
const client = new CertificateClient(
"https://mykeyvault123.vault.azure.net/",
new DefaultAzureCredential()
);
const poller = await client.beginGetCertificate("my-tls-cert");
const cert = await poller.pollUntilDone();
// Get the PEM version
const certificateObject = await client.getCertificateVersion(
"my-tls-cert",
cert.properties.version
);
// Export as PEM
return certificateObject.pem;
}
// Use with Node.js HTTPS server
import * as https from "https";
import * as fs from "fs";
const cert = await getCertificatePem();
const privateKey = await getSecret("ssl-private-key");
const httpsServer = https.createServer({
key: privateKey,
cert: cert,
}, app);
httpsServer.listen(443);
// Using certificate in ASP.NET
public void ConfigureServices(IServiceCollection services)
{
// Load certificate from Key Vault
var vaultUri = Configuration["KeyVault:Uri"];
var certificateName = Configuration["KeyVault:CertificateName"];
var credential = new ManagedIdentityCredential();
var certClient = new CertificateClient(new Uri(vaultUri), credential);
var cert = certClient.GetCertificate(certificateName).Value;
services.AddHttpsRedirection(options =>
{
options.HttpsPort = 443;
});
services.AddHttpsCertificate(cert);
}
Access Control (RBAC and Policies)
Azure RBAC (Recommended for New Vaults)
# Assign Key Vault Reader to a user
az role assignment create \
--role "Key Vault Reader" \
--scope "/subscriptions/{sub-id}/resourceGroups/keyvault-rg/providers/Microsoft.KeyVault/vaults/mykeyvault123" \
--assignee "user@domain.com"
# Assign Key Vault Secrets Officer to an app
az role assignment create \
--role "Key Vault Secrets Officer" \
--scope "/subscriptions/{sub-id}/resourceGroups/keyvault-rg/providers/Microsoft.KeyVault/vaults/mykeyvault123" \
--assignee "my-app-identity"
# Custom role for specific operations
az role definition create --role-definition '{
"Name": "Key Vault Crypto Officer",
"Description": "Can encrypt/decrypt but not manage keys",
"Actions": [
"Microsoft.KeyVault/vaults/keys/encrypt/action",
"Microsoft.KeyVault/vaults/keys/decrypt/action",
"Microsoft.KeyVault/vaults/keys/read/action"
],
"DataActions": [],
"AssignableScopes": ["/subscriptions/{sub-id}"]
}'
Vault Access Policies (Legacy)
# Set access policy for an app (get, list secrets)
az keyvault set-policy \
--name mykeyvault123 \
--spn "http://my-app-identity" \
--secret-permissions get list \
--key-permissions read \
--certificate-permissions get list
# Set access policy for a user (all secrets)
az keyvault set-policy \
--name mykeyvault123 \
--upn "user@domain.com" \
--secret-permissions all \
--key-permissions all \
--certificate-permissions all
Managed Identity (Production Best Practice)
Never store credentials in code. Use Managed Identity:
// In Azure App Service / Azure Functions
var credential = new ManagedIdentityCredential();
// In local development
var credential = new DefaultAzureCredential(
new DefaultAzureCredentialOptions {
ManagedIdentityClientId = "user-assigned-identity-id"
}
);
// Automatically uses Managed Identity in Azure
const credential = new DefaultAzureCredential();
const client = new SecretClient(vaultUrl, credential);
Network Access and Firewall
Private Endpoints
# Enable firewall and virtual networks
az keyvault update \
--name mykeyvault123 \
--resource-group keyvault-rg \
--public-network-access Disabled \
--default-action Deny
# Add specific virtual network
az keyvault network-rule add \
--name mykeyvault123 \
--resource-group keyvault-rg \
--subnet "my-subnet-id" \
--ip-address "123.45.67.89/32"
# Create private endpoint
az network private-endpoint create \
--name keyvault-pe \
--resource-group keyvault-rg \
--subnet my-subnet \
--private-connection-resource-id $(az keyvault show -n mykeyvault123 --query id -o tsv) \
--connection-name keyvault-connection \
--type Microsoft.KeyVault/vaults
Service Endpoints
# Allow trusted Microsoft services
az keyvault network-rule add \
--name mykeyvault123 \
--resource-group keyvault-rg \
--bypass AzureServices
Audit Logging and Monitoring
Enabling Diagnostic Logs
# Create Log Analytics workspace
az monitor log-analytics workspace create \
--resource-group keyvault-rg \
--workspace-name keyvault-logs
# Enable diagnostics
az monitor diagnostic-settings create \
--name keyvault-audit \
--resource $(az keyvault show -n mykeyvault123 --query id -o tsv) \
--workspace $(az monitor log-analytics workspace show -n keyvault-logs --query id -o tsv) \
--logs '[
{
"category": "AuditEvent",
"enabled": true,
"retentionPolicy": {"days": 90, "enabled": true}
}
]' \
--metrics '[
{
"category": "AllMetrics",
"enabled": true,
"retentionPolicy": {"days": 90, "enabled": true}
}
]'
KQL Queries for Security Monitoring
// All secret access in the last 24 hours
AzureDiagnostics
| where ResourceType == "VAULTS"
| where OperationName has "Secret"
| where TimeGenerated > ago(24h)
| project TimeGenerated, Identity, OperationName, SecretName, Success
// Failed access attempts (potential attack)
AzureDiagnostics
| where ResourceType == "VAULTS"
| where ResultType == "Failure"
| where ResultCode != "403" // Filter authorized failures
| project TimeGenerated, Identity, OperationName, ResultCode, CallerIPAddress
// Secret changes (who modified what)
AzureDiagnostics
| where OperationName has "Secret" and OperationName has "Set"
| project TimeGenerated, Identity, SecretName, OperationName
// Key usage patterns
AzureDiagnostics
| where OperationName has "Key"
| summarize KeyOps=count() by bin(TimeGenerated, 1h), OperationName
| render timechart
// Certificate expiration alerts
AzureDiagnostics
| where OperationName == "CertificateNearExpiry"
| project TimeGenerated, CertificateName, DaysUntilExpiry
Setting Up Alerts
# Alert for secret access from unexpected IP
az monitor metric alert create \
--name "SecretAccessFromNewIP" \
--resource-group keyvault-rg \
--condition "type=SecurityEvent and timeGenerated > ago(1h)" \
--description "Alert when secrets accessed from new IP"
# Alert for high secret operations
az monitor alert create \
--name "HighSecretOperations" \
--resource-group keyvault-rg \
--condition "type=Metric and metricName=SecretOperations and aggregationType=Count and operator=GreaterThan 1000" \
--description "Alert on high secret operation volume"
Soft Delete and Recovery
Always enable soft delete for production vaults:
# Enable soft delete (now default)
az keyvault update \
--name mykeyvault123 \
--resource-group keyvault-rg \
--enable-soft-delete \
--soft-delete-retention-days 90
# Enable purge protection (prevents accidental deletion)
az keyvault update \
--name mykeyvault123 \
--resource-group keyvault-rg \
--enable-purge-protection
Recovering Deleted Secrets
# List deleted secrets
az keyvault secret list-deleted --vault-name mykeyvault123
# Recover a deleted secret
az keyvault secret recover \
--vault-name mykeyvault123 \
--name my-deleted-secret
# Permanently delete (purge) - requires purge permission
az keyvault secret purge \
--vault-name mykeyvault123 \
--name my-deleted-secret
Integration Patterns
Azure Functions with Key Vault
// Azure Function with Key Vault binding
export async function run(context: Context, req: HttpRequest) {
const vaultUrl = process.env.KEY_VAULT_URL;
const credential = new DefaultAzureCredential();
const client = new SecretClient(vaultUrl, credential);
// Fetch secret at runtime - never in code
const dbConnection = await client.getSecret("db-connection").then(r => r.value);
// Use the secret
const pool = new Pool({ connectionString: dbConnection });
context.res = {
body: { status: "connected" }
};
}
AKS Pod Identity Integration
# AKS deployment with Key Vault access
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
metadata:
labels:
aadpodidbinding: my-app-identity # Matches AzureIdentity binding
spec:
containers:
- name: app
env:
- name: AZURE_KEY_VAULT_URL
value: "https://mykeyvault.vault.azure.net/"
# Create Azure Identity and binding
az identity create --name my-app-identity --resource-group rg
# Install aad-pod-identity in AKS
kubectl apply -f https://raw.githubusercontent.com/Azure/aad-pod-identity/master/deploy/manifest/stable/aad-pod-identity.yaml
# Create AzureIdentityBinding
apiVersion: "aadpodidentity.k8s.io/v1"
kind: AzureIdentityBinding
metadata:
name: my-app-binding
spec:
azureIdentity: my-app-identity
selector: my-app-identity
Azure API Management Integration
<!-- APIM policy to fetch secret from Key Vault -->
<policies>
<inbound>
<base />
<!-- Validate API key stored in Key Vault -->
<set-variable name="api-key" value="{{ vault-secret('apim-api-key') }}" />
<validate-jwt header-name="Authorization">
<issuer-signing-keys>
<key>{{ vault-key('jwt-signing-key') }}</key>
</issuer-signing-keys>
</validate-jwt>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
</policies>
Best Practices
- Use Managed Identity: Never store credentials in code or environment variables
- Enable Private Endpoints: Prevent public internet access to your vault
- Enable Audit Logging: Track all access for compliance and security
- Enable Soft Delete and Purge Protection: Prevent accidental data loss
- Rotate Keys Regularly: Use automatic rotation policies
- Use Key Vault DNS Zone: For private endpoint DNS resolution
- Separate Environments: Use separate vaults for dev, staging, prod
- Use RBAC over Policies: More granular and auditable access control
- Implement Least Privilege: Only grant necessary permissions
- Monitor for Anomalies: Set up alerts for unusual access patterns
Pricing Considerations
| Operation | Cost (per 10K operations) |
|---|---|
| Secret/Key/Certificate Operations | ~$0.03 |
| HSM Key Operations (Premium) | ~$0.50 |
| Storage (per GB/month) | ~$0.07 |
Key Vault is cost-effective compared to managing your own HSM infrastructure.
Summary
Azure Key Vault provides a secure, scalable foundation for:
- Secrets Management: API keys, passwords, connection strings
- Key Management: Encryption, signing, key rotation
- Certificate Lifecycle: TLS certificates with auto-renewal
- Access Control: RBAC and managed identities
- Audit Logging: Complete visibility into usage
Integrate Key Vault into every production application to eliminate hardcoded secrets and improve your security posture.