← Back to ArticlesKey Vault

Azure Key Vault — Secrets Management, Encryption, and Certificate Lifecycle

Complete guide to Azure Key Vault for secure secrets storage, encryption keys, certificate management, and integration patterns.

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:

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:

TierKeys/SecretsCertificatesHSM-Backed KeysPrice
Standard2,0002,000Software-backedLower
Premium2,0002,000HSM-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

  1. Use Managed Identity: Never store credentials in code or environment variables
  2. Enable Private Endpoints: Prevent public internet access to your vault
  3. Enable Audit Logging: Track all access for compliance and security
  4. Enable Soft Delete and Purge Protection: Prevent accidental data loss
  5. Rotate Keys Regularly: Use automatic rotation policies
  6. Use Key Vault DNS Zone: For private endpoint DNS resolution
  7. Separate Environments: Use separate vaults for dev, staging, prod
  8. Use RBAC over Policies: More granular and auditable access control
  9. Implement Least Privilege: Only grant necessary permissions
  10. Monitor for Anomalies: Set up alerts for unusual access patterns

Pricing Considerations

OperationCost (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:

Integrate Key Vault into every production application to eliminate hardcoded secrets and improve your security posture.