← All writing
AzureSecurityAutomation

Eliminating Hardcoded Secrets with Azure Key Vault and Managed Identities

· 6 min read

Every time I review an application I make a point of grepping the repository for things like password=, AccountKey= and ApiKey. More often than I would like, I find them. Not because the team is careless, but because putting a secret in a config file is the path of least resistance, and once it works nobody goes back to fix it. The problem is that a secret in source control, an app setting, or a Dockerfile is a secret that has effectively already leaked, it is just a question of who reads it and when.

Azure gives you a clean way out of this with two services that work together, Key Vault to store the secret, and a managed identity so your application can fetch it without holding any credential of its own. The combination means there is no secret in your code at all, not even the secret needed to get the other secrets.

Prerequisites

  • An Azure subscription and the Contributor role on a resource group.
  • An app already running on an Azure compute service. I will use an App Service web app in this example, but the same approach works for Functions, Container Apps and VMs.
  • Azure CLI, or the portal if you prefer clicking.

1. Create a Key Vault and store a secret

az keyvault create \
  --name "kv-myapp-prod" \
  --resource-group "rg-myapp" \
  --location "uksouth" \
  --enable-rbac-authorization true

I am turning on RBAC authorization deliberately. Key Vault supports two permission models, the older access policies and Azure RBAC. RBAC is the one to use now, it lines up with how the rest of Azure handles permissions and it is far easier to audit. Now add a secret:

az keyvault secret set \
  --vault-name "kv-myapp-prod" \
  --name "DbConnectionString" \
  --value "Server=tcp:...;Password=...;"

That value now lives in the vault, encrypted, versioned, and logged every time it is accessed.

2. Give your app a managed identity

A managed identity is an identity in Microsoft Entra ID that Azure creates and manages for your resource. You never see a password for it, and it cannot be exported. Enable the system assigned identity on the web app:

az webapp identity assign \
  --resource-group "rg-myapp" \
  --name "app-myapp-prod"

The command returns a principalId. Hold on to it, that is the identity you are about to grant access to the vault.

3. Grant the identity read access to secrets, and nothing more

This is where least privilege matters. The app needs to read secrets. It does not need to manage the vault, delete keys, or change access. The predefined Key Vault Secrets User role grants exactly read access to secret values, so use that and resist the temptation to hand out something broader.

az role assignment create \
  --assignee "<principalId-from-step-2>" \
  --role "Key Vault Secrets User" \
  --scope "/subscriptions/<sub-id>/resourceGroups/rg-myapp/providers/Microsoft.KeyVault/vaults/kv-myapp-prod"

Scope it to the single vault, not the resource group or subscription. If this app is ever compromised, the blast radius is one vault’s secrets and nothing else.

4. Reference the secret from your app

You now have two ways to consume the secret, and which you pick depends on how much you want to change your code.

Option A, no code change. App Service can resolve Key Vault references inside its own configuration. Set the app setting to a reference and Azure fetches the value at startup using the managed identity:

az webapp config appsettings set \
  --resource-group "rg-myapp" \
  --name "app-myapp-prod" \
  --settings DbConnectionString="@Microsoft.KeyVault(SecretUri=https://kv-myapp-prod.vault.azure.net/secrets/DbConnectionString/)"

Your application reads DbConnectionString from its environment exactly as before. It has no idea Key Vault is involved, which is rather the point.

Option B, in code. If you want the application to pull secrets itself, use the Azure SDK with DefaultAzureCredential. The nice thing about this credential is that it uses the managed identity in Azure, and falls back to your developer login when you run locally, so the same code works in both places.

from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient

credential = DefaultAzureCredential()
client = SecretClient(
    vault_url="https://kv-myapp-prod.vault.azure.net",
    credential=credential,
)

connection_string = client.get_secret("DbConnectionString").value

Notice there is no key, no token, no password anywhere in that snippet. The managed identity does the authenticating.

5. Rotate without redeploying

Because nothing has the secret baked in, rotation becomes boring, which is the goal. Add a new version of the secret in the vault:

az keyvault secret set --vault-name "kv-myapp-prod" --name "DbConnectionString" --value "<new value>"

Apps that read the secret in code pick up the new version on their next read. For Key Vault references, App Service refreshes them periodically, and you can force it by restarting the app. Either way, you rotated a production credential without touching code or config.

A few things worth doing while you are here

  • Turn on soft delete and purge protection on the vault so a deleted secret or a deleted vault can be recovered. It is on by default now, but confirm it.
  • Enable diagnostic logging to a Log Analytics workspace. Every secret read is then recorded, which is exactly the evidence you want when an auditor asks who accessed what.
  • Use separate vaults per environment. Dev secrets and prod secrets should never share a blast radius.

Closing thoughts

The pattern here is small but it changes your security posture more than almost anything else you can do in an afternoon. The application carries no secret of its own, the one secret it needs is fetched at runtime by an identity that cannot be stolen, and every access is logged and revocable. Once you have set it up once it becomes the default way you wire up every new service, and the habit of pasting a connection string into a config file quietly disappears.