Back to blog
Azure2025-01-15 6 min read

Azure Key Vault Best Practices for Enterprise Workloads

Secret management done right. How to integrate Key Vault into your Terraform code, CI/CD pipelines, and app configs without exposing sensitive data anywhere.

Why Most Teams Get Secret Management Wrong

The most common pattern I see when joining a new project: connection strings hardcoded in appsettings.json, passwords stored as plain-text pipeline variables, and API keys sitting in .env files committed to Git.

Every single one of these is a serious security risk. Azure Key Vault solves all of them — but only if you use it correctly.

The Right Architecture

Application / Pipeline
        ↓
  Managed Identity (no credentials needed)
        ↓
  Azure Key Vault
  ├── Secrets  (passwords, connection strings, API keys)
  ├── Keys     (encryption keys, signing keys)
  └── Certs    (TLS certificates)

The key principle: your application should never handle credentials directly. Managed Identity lets Azure handle authentication for you.

Step 1 — Provision Key Vault with Terraform

resource "azurerm_key_vault" "main" {
  name                = "kv-${var.project}-${var.environment}"
  location            = var.location
  resource_group_name = var.resource_group_name
  tenant_id           = data.azurerm_client_config.current.tenant_id
  sku_name            = "standard"

  # Prevent accidental deletion
  soft_delete_retention_days = 90
  purge_protection_enabled   = true

  # Restrict to your VNet only
  network_acls {
    default_action             = "Deny"
    bypass                     = "AzureServices"
    virtual_network_subnet_ids = [azurerm_subnet.app.id]
  }
}

# Give your app's managed identity Get/List access
resource "azurerm_key_vault_access_policy" "app" {
  key_vault_id = azurerm_key_vault.main.id
  tenant_id    = data.azurerm_client_config.current.tenant_id
  object_id    = azurerm_linux_web_app.main.identity[0].principal_id

  secret_permissions = ["Get", "List"]
  # Never give "Set" or "Delete" to app identities
}

Step 2 — Store Secrets Without Exposing Them

Never pass secret values through your pipeline in plain text. Use Terraform's sensitive flag and feed values from your pipeline's secure variable groups.

resource "azurerm_key_vault_secret" "db_password" {
  name         = "db-password"
  value        = var.db_password   # marked sensitive in variables.tf
  key_vault_id = azurerm_key_vault.main.id

  tags = {
    environment = var.environment
    managed_by  = "terraform"
  }
}

In variables.tf:

variable "db_password" {
  type      = string
  sensitive = true  # never printed in plan output
}

In your pipeline, pull it from Azure DevOps variable groups linked to Key Vault — not hardcoded pipeline variables.

Step 3 — Consume Secrets in Spring Boot

// No credentials in code. Managed Identity handles auth automatically.
@Configuration
public class KeyVaultConfig {

    @Bean
    public SecretClient secretClient(@Value("${azure.keyvault.uri}") String kvUri) {
        return new SecretClientBuilder()
            .vaultUrl(kvUri)
            .credential(new DefaultAzureCredentialBuilder().build())
            .buildClient();
    }
}

Then in application.yaml — just the vault URI, no secrets:

azure:
  keyvault:
    uri: ${KEY_VAULT_URI}   # set as env variable, not a secret itself

Step 4 — Reference Key Vault Secrets in Pipelines

# Azure DevOps — link a variable group to Key Vault
# Then reference secrets like normal variables, they're never logged

variables:
  - group: "kv-production-secrets"  # linked to Key Vault in Library

steps:
  - script: |
      echo "Deploying with DB_HOST=$(DB_HOST)"
      # DB_PASSWORD is available as $(DB_PASSWORD) but never echoed
    env:
      DB_PASSWORD: $(db-password)   # fetched from Key Vault at runtime

Common Mistakes to Avoid

1. Giving apps too many permissions Your app only needs Get and List. Never give Set, Delete, or Purge to application identities. Reserve those for your deployment service principal only.

2. Using access policies instead of RBAC Prefer Azure RBAC over vault access policies — it's auditable, consistent with the rest of Azure, and easier to manage at scale.

resource "azurerm_role_assignment" "app_kv_reader" {
  scope                = azurerm_key_vault.main.id
  role_definition_name = "Key Vault Secrets User"
  principal_id         = azurerm_linux_web_app.main.identity[0].principal_id
}

3. Not enabling diagnostic logging Always send Key Vault audit logs to Log Analytics. You want to know exactly who accessed which secret and when.

resource "azurerm_monitor_diagnostic_setting" "kv" {
  name               = "kv-diagnostics"
  target_resource_id = azurerm_key_vault.main.id
  log_analytics_workspace_id = azurerm_log_analytics_workspace.main.id

  enabled_log { category = "AuditEvent" }
  metric { category = "AllMetrics" }
}

Checklist

Before going to production with Key Vault, verify:

  • [ ] Purge protection enabled
  • [ ] Network ACLs restricting access to known subnets
  • [ ] Managed Identity used (no service principal secrets)
  • [ ] Apps have minimum required permissions only
  • [ ] Diagnostic logs flowing to Log Analytics
  • [ ] Soft delete retention set to 90 days
  • [ ] No secret values in Terraform state (use sensitive = true)
  • [ ] Rotation policy set for long-lived secrets

This is the exact checklist I run through on every Key Vault I provision. It takes 10 minutes and prevents the kind of security incidents that make headlines.