Azure Key Vault: Securing Your Application Secrets
Every application needs secrets — database connection strings, API keys, certificates, encryption keys. Hardcoding them in source code is the single most common security failure in enterprise apps. Azure Key Vault solves this completely.
Prerequisites
| Requirement | Details |
|---|---|
| Azure subscription | Free trial available |
| Azure CLI | Version 2.40 or later |
| Role | Contributor or Key Vault Contributor on the target resource group |
| Knowledge | Basic familiarity with Microsoft Entra ID (formerly Azure AD) |
Architecture Overview
flowchart TB
subgraph Apps["Applications"]
APP1[App Service]
APP2[Azure Functions]
APP3[AKS / Container]
APP4[VM / On-Prem\nvia Arc]
end
subgraph Identity["Managed Identity Layer"]
MI[DefaultAzureCredential\nAuto-selects credential source]
end
subgraph KV["Azure Key Vault"]
SEC[Secrets\nConn strings, API keys]
KEY[Keys\nEncryption, signing]
CERT[Certificates\nTLS / mTLS]
end
subgraph Control["Access Control"]
RBAC[Entra ID RBAC\nSecrets User / Officer]
FW[Firewall\nPrivate Endpoint]
AUDIT[Audit Logs\nLog Analytics]
end
Apps --> MI
MI --> RBAC
RBAC --> KV
FW --> KV
KV --> AUDIT
style Apps fill:#dbeafe,stroke:#3b82f6,color:#1e3a8a
style Identity fill:#fef3c7,stroke:#f59e0b,color:#78350f
style KV fill:#ede9fe,stroke:#8b5cf6,color:#4c1d95
style Control fill:#d1fae5,stroke:#059669,color:#065f46
Zero-trust model: No application stores credentials. Identity is asserted via Managed Identity tokens that expire automatically. Key Vault validates every access request against RBAC.
Secret Types and Access Patterns
| Type | Use Cases | Recommended Pattern | Rotation Cycle |
|---|---|---|---|
| Secrets | DB conn strings, API keys, passwords | Startup prefetch + TTL cache | 30–90 days |
| Keys | Data encryption, JWT signing | Per-operation via SDK | 1–2 years |
| Certificates | TLS, mTLS, code signing | Loaded once at startup | 1 year (auto-renew) |
Step 1: Create an Azure Key Vault
# Set variables
$RESOURCE_GROUP = "rg-keyvault-demo"
$LOCATION = "eastus"
$KEY_VAULT_NAME = "kv-secure-app-$(Get-Random -Maximum 9999)"
# Create resource group
az group create --name $RESOURCE_GROUP --location $LOCATION
# Create Key Vault with RBAC authorization (recommended over access policies)
az keyvault create `
--name $KEY_VAULT_NAME `
--resource-group $RESOURCE_GROUP `
--location $LOCATION `
--enable-rbac-authorization true `
--enabled-for-deployment false `
--enabled-for-disk-encryption false `
--enabled-for-template-deployment false
Configuration decisions:
| Setting | Value | Reason |
|---|---|---|
--enable-rbac-authorization true |
✅ Enabled | Preferred over legacy Access Policies |
--enabled-for-deployment false |
✅ Disabled | Restricts VM certificate injection |
| Soft delete | ✅ On by default | Retains deleted secrets for 90 days |
| Purge protection | Enable for prod | Prevents permanent deletion during retention |
Grant yourself the admin role:
$USER_OBJECT_ID = az ad signed-in-user show --query id -o tsv
$KV_SCOPE = "/subscriptions/$(az account show --query id -o tsv)/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.KeyVault/vaults/$KEY_VAULT_NAME"
az role assignment create `
--role "Key Vault Secrets Officer" `
--assignee $USER_OBJECT_ID `
--scope $KV_SCOPE
Step 2: Store Secrets, Keys, and Certificates
# Database connection string
az keyvault secret set `
--vault-name $KEY_VAULT_NAME `
--name "DatabaseConnectionString" `
--value "Server=myserver.database.windows.net;Database=mydb;User Id=admin;Password=<redacted>"
# API key with 1-year expiry
$EXPIRY_DATE = (Get-Date).AddYears(1).ToString("yyyy-MM-ddTHH:mm:ssZ")
az keyvault secret set `
--vault-name $KEY_VAULT_NAME `
--name "ThirdPartyApiKey" `
--value "sk_live_51H..." `
--expires $EXPIRY_DATE
# RSA encryption key
az keyvault key create `
--vault-name $KEY_VAULT_NAME `
--name "DataEncryptionKey" `
--kty RSA `
--size 2048 `
--ops encrypt decrypt
# Certificate (from PFX file)
az keyvault certificate import `
--vault-name $KEY_VAULT_NAME `
--name "AppServiceCertificate" `
--file "path/to/certificate.pfx" `
--password "cert-password"
Verify a secret was stored:
az keyvault secret show \
--vault-name $KEY_VAULT_NAME \
--name "DatabaseConnectionString" \
--query "value" -o tsv
Step 3: Configure Managed Identity Access
sequenceDiagram
participant App as Application\n(App Service / Functions)
participant MI as Managed Identity\n(Entra ID)
participant KV as Key Vault
participant Cache as Local Cache\n(in-memory TTL)
App->>MI: Request access token
MI-->>App: JWT token (auto-refreshed)
App->>KV: GET /secrets/DatabaseConnectionString\n+ Bearer token
KV->>MI: Validate token + RBAC check
MI-->>KV: Identity confirmed — role: Secrets User
KV-->>App: Secret value
App->>Cache: Store value with TTL=5min
Note over App,Cache: Subsequent reads served from cache
Cache-->>App: Cached value (no vault call)
# Create user-assigned managed identity
$IDENTITY_NAME = "id-secure-app"
az identity create `
--name $IDENTITY_NAME `
--resource-group $RESOURCE_GROUP `
--location $LOCATION
# Get principal ID
$IDENTITY_PRINCIPAL_ID = az identity show `
--name $IDENTITY_NAME `
--resource-group $RESOURCE_GROUP `
--query principalId -o tsv
# Grant read-only access to secrets
az role assignment create `
--role "Key Vault Secrets User" `
--assignee $IDENTITY_PRINCIPAL_ID `
--scope $KV_SCOPE
RBAC roles reference:
| Role | Read Secrets | Write/Delete Secrets | Manage Access | When To Use |
|---|---|---|---|---|
| Key Vault Secrets User | ✅ | ❌ | ❌ | Application identities |
| Key Vault Secrets Officer | ✅ | ✅ | ❌ | DevOps pipelines, rotation scripts |
| Key Vault Administrator | ✅ | ✅ | ✅ | Break-glass admin only |
Step 4: Application Integration (Node.js)
Install the SDK:
npm install @azure/keyvault-secrets @azure/identity
With caching (production-grade):
const { SecretClient } = require("@azure/keyvault-secrets");
const { DefaultAzureCredential } = require("@azure/identity");
class SecretCache {
constructor(client, ttlMs = 300_000) { // 5-min default TTL
this.client = client;
this.ttlMs = ttlMs;
this.store = new Map();
}
async get(name) {
const entry = this.store.get(name);
if (entry && (Date.now() - entry.ts) < this.ttlMs) {
return entry.value;
}
const secret = await this.client.getSecret(name);
this.store.set(name, { value: secret.value, ts: Date.now() });
return secret.value;
}
}
// DefaultAzureCredential: uses Managed Identity in Azure,
// falls back to Azure CLI locally — no code change needed
const credential = new DefaultAzureCredential();
const vaultUrl = `https://${process.env.KEY_VAULT_NAME}.vault.azure.net`;
const client = new SecretClient(vaultUrl, credential);
const cache = new SecretCache(client);
async function main() {
const dbConn = await cache.get("DatabaseConnectionString");
console.log("Database connection retrieved successfully");
// Never log the actual secret value
}
main().catch(err => { console.error(err); process.exit(1); });
Set the environment variable:
# Development (local)
$env:KEY_VAULT_NAME = "kv-secure-app-1234"
# Production — App Service setting
az webapp config appsettings set `
--name "my-app-service" `
--resource-group $RESOURCE_GROUP `
--settings KEY_VAULT_NAME="kv-secure-app-1234"
Step 5: Monitoring and Auditing
# Create Log Analytics workspace
az monitor log-analytics workspace create \
--workspace-name "law-keyvault-audit" \
--resource-group $RESOURCE_GROUP \
--location $LOCATION
WORKSPACE_ID=$(az monitor log-analytics workspace show \
--workspace-name "law-keyvault-audit" \
--resource-group $RESOURCE_GROUP \
--query id -o tsv)
# Enable diagnostic logging (AuditEvent + AllMetrics)
az monitor diagnostic-settings create \
--name "KeyVaultAuditLogs" \
--resource "$KV_SCOPE" \
--workspace $WORKSPACE_ID \
--logs '[{"category": "AuditEvent", "enabled": true}]' \
--metrics '[{"category": "AllMetrics", "enabled": true}]'
KQL — all secret reads in last 24 h:
AzureDiagnostics
| where ResourceType == "VAULTS"
| where OperationName == "SecretGet"
| project TimeGenerated, CallerIPAddress, identity_claim_upn_s, requestUri_s
| order by TimeGenerated desc
KQL — top callers by volume:
AzureDiagnostics
| where OperationName == "SecretGet"
| summarize requests = count() by identity_claim_upn_s
| order by requests desc
Alert on these signals:
- Spike in
SecretGetfrom a single identity in a short window - Any
PurgeSecretorDeleteSecretoutside a maintenance window - Secret expiry approaching within 14 days (use Event Grid + Function trigger)
Step 6: Secret Rotation
flowchart TD
A([Secret approaching expiry]) --> B[Event Grid\nnotifies rotation Function]
B --> C[Generate new secret value\n e.g. new API key from provider]
C --> D[Set new version in Key Vault]
D --> E[Update app references\n restart if needed]
E --> F{Verify app health}
F -- Healthy --> G[Deprecate old version\nafter grace period]
F -- Errors --> H[Roll back — re-point\nto previous version]
G --> I([Rotation complete])
H --> D
style A fill:#fef3c7,stroke:#f59e0b,color:#78350f
style I fill:#d1fae5,stroke:#059669,color:#065f46
style H fill:#fee2e2,stroke:#ef4444,color:#7f1d1d
Automated rotation via Bicep (AVM module):
module kv 'br/public:avm/res/key-vault/vault:<version>' = {
name: 'kvEnterprise'
params: {
name: 'kv-enterprise-prod'
enablePurgeProtection: true
enableRbacAuthorization: true
keys: [
{
name: 'encKey'
rotationPolicy: {
attributes: { expiryTime: 'P2Y' }
lifetimeActions: [
{ action: { type: 'rotate' }, trigger: { timeBeforeExpiry: 'P30D' } }
{ action: { type: 'notify' }, trigger: { timeBeforeExpiry: 'P60D' } }
]
}
}
]
}
}
Performance and Caching
| Concern | Symptom | Strategy |
|---|---|---|
| Latency | Repeated vault calls per request | In-memory cache with TTL (≤ rotation interval) |
| Cold start delay | Many secrets needed at startup | Batch fetch asynchronously in parallel |
| Throttling (429) | Sustained high-volume calls | Exponential backoff + jitter; monitor throttling metrics |
| Regional resilience | Cross-region latency | Deploy one vault per region; sync via pipeline |
| Large payloads | Slow transfers | Keep secrets small; offload config to Azure App Configuration |
Multi-Environment Isolation
flowchart LR
DEV[Dev Team] --> KV_DEV[kv-myapp-dev\nRelaxed RBAC\nPublic access OK]
CI[CI/CD Pipeline] --> KV_TEST[kv-myapp-test\nService Principal\nNo public access]
PROD[Production App\nManaged Identity] --> KV_PROD[kv-myapp-prod\nPrivate endpoint only\nPurge protection ON]
style KV_DEV fill:#dbeafe,stroke:#3b82f6,color:#1e3a8a
style KV_TEST fill:#fef3c7,stroke:#f59e0b,color:#78350f
style KV_PROD fill:#fee2e2,stroke:#ef4444,color:#7f1d1d
Never share vaults across environments. A misconfigured dev RBAC role should never expose production secrets.
Resilience and Disaster Recovery
| Risk | Mitigation | Detail |
|---|---|---|
| Accidental deletion | Soft delete + purge protection | Mandatory for production vaults |
| Regional outage | Multiple vaults (active-active) | Deploy per region; sync secrets via pipeline |
| Compromised secret | Immediate version rollback | Re-point apps to previous version ID |
| Access escalation | Quarterly RBAC audits | Principle of least privilege reviews |
| Network isolation failure | Private endpoint + deny-all public | Enforce via Azure Policy |
Best Practices
Use Managed Identity everywhere. Never store Key Vault credentials in code or configuration.
DefaultAzureCredentialworks locally (CLI) and in Azure (MI) with zero code change.
Separate vaults by environment. Dev, test, and prod should each have their own vault with independent RBAC controls.
Enable purge protection in production. Soft delete alone is not enough — purge protection prevents the 90-day window from being bypassed.
Cache secrets with a TTL. Fetching from Key Vault on every request adds latency and risks throttling. Cache with a TTL shorter than your rotation interval.
Rotate proactively. Set expiry dates on every secret and automate rotation before expiry — not after a breach.
Troubleshooting
| Symptom | Root Cause | Fix |
|---|---|---|
does not have secrets get permission |
Missing RBAC role | Verify with az role assignment list --scope <kv-id> — wait up to 10 min for propagation |
DefaultAzureCredential failed (local) |
Not logged into CLI | Run az login |
| Secrets not accessible from App Service | MI not enabled or wrong role | Enable MI on App Service; check role assignment to the vault |
| Certificate import fails (parsing error) | Wrong format or bad password | Convert to PFX/PKCS12 with openssl; verify full cert chain |
| Slow cold start | Many serial vault fetches | Parallelize: await Promise.all([cache.get('A'), cache.get('B')]) |
Throttling 429s |
High request volume | Implement cache; use exponential backoff; monitor Vault metrics |
Key Takeaways
- ✅ Azure Key Vault centralises all secrets — no more hardcoded credentials in source code
- ✅ Managed Identity provides password-less, auto-rotating authentication between services
- ✅ RBAC access control enforces least-privilege down to the individual secret level
- ✅ Full audit logging tracks every access event for compliance and security monitoring
- ✅ Soft delete, versioning, and rotation policies protect against accidental or malicious loss
- ✅ Separate vaults per environment prevent cross-contamination
Additional Resources
- Azure Key Vault documentation
- RBAC vs Access Policies
- Key rotation guide
- Event Grid integration for expiry alerts
- Azure Policy samples for Key Vault
What secrets have you moved to Key Vault? Any rotation automation patterns worth sharing? Drop them in the comments.
Discussion