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.