Home / Azure / Azure Key Vault: Securing Your Application Secrets
Azure

Azure Key Vault: Securing Your Application Secrets

Stop hardcoding secrets. Azure Key Vault centralises secrets, keys, and certificates with zero-trust access via Managed Identity, RBAC, and full audit logging.

What you will learn

Practical execution with concise explanations, real implementation patterns, and production-ready recommendations.

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

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

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

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 SecretGet from a single identity in a short window
  • Any PurgeSecret or DeleteSecret outside 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. DefaultAzureCredential works 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


What secrets have you moved to Key Vault? Any rotation automation patterns worth sharing? Drop them in the comments.

Discussion