Skip to content

Storing an API Key in Azure Key Vault for a Function App

11 min read · updated August 11, 2026

A Key Vault reference replaces the value of an app setting with a pointer, and the platform resolves it before your process starts. Your code is unchanged — it still reads an environment variable. That is the appeal, and it is also why the failure mode is so quiet.

The reference syntax

A reference is of the form @Microsoft.KeyVault({referenceString}), and Microsoft documents two accepted reference strings:

@Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/aoai-key)

@Microsoft.KeyVault(VaultName=myvault;SecretName=aoai-key)

The SecretUri form takes the full data-plane URI, optionally with a version appended as a further path segment. The VaultName form takes an optional SecretVersion as a third semicolon-separated field. Omit the version in either form and the app uses the latest version that exists in the vault — which is what you want for a rotating provider key, and which has a timing consequence covered below. Microsoft, Use Key Vault references as app settings.

This syntax belongs to App Service and Azure Functions (and Logic Apps Standard). It is not Container Apps syntax — a container app has its own secret store and its own vault integration, and pasting this string into a container app environment variable leaves the literal text in the container. See the Container Apps deployment page for the secretref: equivalent.

Identity and the role it needs

References use the app’s system-assigned identity by default. The access it needs depends on the vault’s permission model, and Microsoft documents both:

  • Azure RBAC: assign the Key Vault Secrets User role to the managed identity.
  • Vault access policy: assign the Get secrets permission.

There is a chicken-and-egg case worth knowing about: some apps need to refer to secrets at creation time, before a system-assigned identity exists. The documented answer is a user-assigned identity granted access in advance, then pointed at with the app’s keyVaultReferenceIdentity property:

identityResourceId=$(az identity show \
  --resource-group rg-model --name id-fn-model --query id -o tsv)

az webapp update --resource-group rg-model --name fn-model-proxy \
  --set keyVaultReferenceIdentity=${identityResourceId}

That setting applies to all Key Vault references for the app, not per setting. Setting it back to the string SystemAssigned reverts.

Wiring it up

  1. Put the secret in the vault.
    az keyvault secret set \
      --vault-name myvault --name aoai-key --value "<key>"
  2. Turn on the app’s system-assigned identity and capture its principal ID.
    principalId=$(az functionapp identity assign \
      --resource-group rg-model --name fn-model-proxy \
      --query principalId -o tsv)
  3. Grant it read access. On an RBAC vault:
    vaultId=$(az keyvault show --name myvault --query id -o tsv)
    
    az role assignment create \
      --role "Key Vault Secrets User" \
      --assignee-object-id $principalId \
      --assignee-principal-type ServicePrincipal \
      --scope $vaultId
  4. Replace the app setting value with the reference. The setting name is unchanged, so the code is unchanged.
    az functionapp config appsettings set \
      --resource-group rg-model --name fn-model-proxy \
      --settings AZURE_OPENAI_API_KEY="@Microsoft.KeyVault(VaultName=myvault;SecretName=aoai-key)"

Microsoft’s guidance is to keep a separate vault per environment and mark these settings as slot settings, so a slot swap does not carry a production key into staging.

Rotation takes up to 24 hours

This is the number most people get wrong. When the reference omits a version, the app picks up a new version automatically — within 24 hours. The delay is because App Service caches resolved values and refetches them every 24 hours. Rotating a provider key and expecting the app to follow within minutes leaves you with a window where the app is still presenting the old key.

Two documented ways to shorten it:

  • Any configuration change to the app causes a restart and an immediate refetch of all referenced secrets.
  • An authenticated POST forces resolution without a config change:
    POST https://management.azure.com/{resourceId}/config/configreferences/appsettings/refresh?api-version=2022-03-01

The practical rotation sequence, then, is: add the new secret version, force the refresh, verify, and only then revoke the old key at the provider. Revoking first means up to 24 hours of 401s.

When it does not resolve

Here is the failure that makes this page worth reading. Microsoft documents it plainly: if a reference is not resolved properly, the reference string is used instead. Your app setting’s value becomes the literal text @Microsoft.KeyVault(VaultName=myvault;SecretName=aoai-key).

Your code does not know that. It sends that string as the API key. The model provider returns a 401, and you spend an hour investigating a key that is, as far as the vault is concerned, perfectly valid. The tell is the shape of the failure: an authentication error whose credential starts with an at sign. Log the length of the credential at startup, or assert that it does not begin with @Microsoft.KeyVault, and this becomes a startup failure instead of a runtime mystery.

Documented causes of a failure to resolve:

  • Misconfigured vault access — the most common by far.
  • A secret that no longer exists, or a pinned version that was purged.
  • A syntax error in the reference itself.
  • Network restrictions on the vault. Microsoft is specific here: the vault should accept traffic from the virtual network the app uses, not from its public outbound IPs, because the origin IP of the secret request may differ. Linux apps connecting to private endpoints must be configured to route all traffic through the virtual network by setting vnetRouteAllEnabled to true — except on Flex Consumption, where that routing is automatic.

Microsoft also documents an expected oddity in vault audit logs: a failed 403 - Forbidden SecretGet from the app’s public outbound IP followed by a successful one from its private IP. That first line is by design and is not the problem you are chasing.

For anything the error message does not explain, the portal has a resolution status per setting: go to Application Settings and select Edit on the reference in question, and the dialog shows status information including the error. Microsoft notes that if you see no status message at all, the syntax is invalid and the value was never recognised as a Key Vault reference — which is a different bug from a permission failure and is fixed in the string, not the vault. There is also a built-in detector, Key Vault Application Settings Diagnostics, under Diagnose and solve problems — for a function app, beneath Availability and Performance Function app down or reporting errors.

Three settings not to reference

A Key Vault reference is not universally applicable, and three specific app settings behave badly when you point one at them.

  • WEBSITE_CONTENTAZUREFILECONNECTIONSTRING. This is the setting that mounts Azure Files as the app’s file system, and it carries validation checks. Microsoft documents that when you use a Key Vault reference here, the validation check fails by default, because the secret cannot be resolved while the incoming configuration request is still being processed. The documented escape is to set WEBSITE_SKIP_CONTENTSHARE_VALIDATION to 1 — but read the caveat before you do: that tells App Service to bypass all checks and it will not create the content share for you, so you must create it in advance. If you skip validation and either the connection string or the share is invalid, the app does not start properly and produces HTTP 500 errors, with nothing in the configuration to suggest why.
  • APPINSIGHTS_INSTRUMENTATIONKEY and APPLICATIONINSIGHTS_CONNECTION_STRING. These work behind a reference, but the Azure portal uses them to surface telemetry on the resource blade, and that stops working when the value is a reference. Microsoft’s own guidance is that these values are not considered secrets, so the usual answer is to configure them directly and keep the portal integration. Trading your telemetry view for the appearance of rigour is a bad deal.

One deployment-ordering rule belongs here too, because it is the most common cause of a first-deploy failure. In an ARM or Bicep template, define the app settings as their own child resource rather than through a siteConfig property on the site. The app has to exist first so its system-assigned identity is created and can be granted access to the vault; a template that declares both at once has no identity to authorise. Where a later resource depends on the settings having landed, add WEBSITE_ENABLE_SYNC_UPDATE_SITE so the settings update is synchronous — without it the update is asynchronous and a dependency on it is not the guarantee it appears to be.

Role names, the refresh API version, the 24-hour cache interval and the settings-specific behaviours above are Microsoft’s documented values at the time of writing.