DDenni Tretyakov
01 Jul, 2025

Key Vault for aspnet core app secrets on Azure and local dev environment

Goal

The goal of the article is how to move app secrets (connection strings, passwords, access keys, etc.) to Key Vault.

What problems will be solved

This is especially useful for startups when you need to get things done without big investment in infrastructure, but don’t want to compromise security.

How the config will get transformed

Let’s assume that’s initial application appSettings.json

{
  "AppName": "My App",
  "ConnectionStrings": {
    "Sql": "Sql Database Connection String Including User Id and Password"
  },
  "ApiKeys": {
    "SendGrid": "Send Grid Api Key"
  }
}

In next steps, I’ll create a Key Vault and move all the secret there.

{
  "AppName": "My App",
  "KeyVault": {
    "Url": "https://my-app-key-vault.vault.azure.net/"
  }
}

Instead of removed secrets, I’ll add only a KeyVault Url to configuration, without any access keys. Furthermore, we’ll see how to make it work on different environments:

I’ll cover two methods for getting this done. The Azure CLI for those who love the command line, and the Azure Portal for a visual approach. You can pick your favorite.

While both work, I highly recommend having the Azure CLI installed for any serious Azure development. It’s a powerful tool to have in your back pocket.

Setup Key Vault

Create Key Vault

There are no specific requirements for this example, all matches the azure defaults. However, if something changed be sure that:

In this example, I’ll create key vault with:

namemy-app-key-vault
resource-grouppasswordless-example
locationwesteurope

You’ll see the Key Vault name (my-app-key-vault) pop up a lot in this example. The name must be unique, there is a chance you’ll need a different name going though the exercise.

Don’t sweat the resource group or location too much — they’re totally up to you. That said, it’s generally best to keep your Key Vault in the same region as your applications.

Once crated your Key Vault address will be https://[NAME].vault.azure.net/,
in given example https://my-app-key-vault.vault.azure.net/

Using Az CLI

az keyvault create \
  --name my-app-key-vault \
  --resource-group passwordless-example \
  --location westeurope \
  --enable-rbac-authorization true \
  --public-network-access Enabled \
  --network-acls '{"defaultAction":"Allow","bypass":"AzureServices"}'

Using Azure Portal

find KeyVault in MarketplaceKey Vault basicsKey Vault access configurationKey Vault networking

Assign yourself Key Vault Secrets Officer role

Once you create a Key Vault, you are assigned an Owner role. The owner can manager permissions, but not allowed to read/write secrets. Therefore, you need to assign a Key Vault Secrets Officer role to yourself.

Key Vault Secrets Officer role — allows to Read and Write Key Vault secrets.
Key Vault Secrets User role — allows users to Read secrets.

Every developer in your team (including you) will need to have one or another role!

Using Az CLI

KEYVAULT_NAME="my-app-key-vault"
RESOURCE_GROUP="passwordless-example"
ROLE="Key Vault Secrets Officer"

USER_OBJECT_ID=$(az ad signed-in-user show --query id --output tsv)

SUBSCRIPTION_ID=$(az account show --query id --output tsv)

SCOPE="/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.KeyVault/vaults/$KEYVAULT_NAME"

echo "Assigning '$ROLE' to user $USER_OBJECT_ID on Key Vault $KEYVAULT_NAME..."
az role assignment create \
  --assignee "$USER_OBJECT_ID" \
  --role "$ROLE" \
  --scope "$SCOPE"

Using Azure Portal

KeyVault access control homeKeyVault add Key Vault Secrets OfficerKeyVault add Yourself

Add configuration values into Vault

In a given example, we have two configuration keys to be moved to Key Vault.

In dotnet, configuration keys are structured with colons (:). Since Azure Key Vault doesn’t allow colons in secret names, the standard workaround is to use a double dash (--) instead.

For example in dotnet configuration key ConnectionStrings:Sql in Key Vault would be ConnectionStrings--Sql.

The example below adds ConnectionStrings--Sql to Key Vault, same way you can add ApiKeys--SendGrid.

Using Az CLI

az keyvault secret set \
  --vault-name my-app-key-vault \
  --name "ConnectionStrings--Sql" \
  --value "Sql Database Connection String Including User Id and Password"

Using Azure Portal

Key Vault secretsAdd Secret to Key Vault

When playing with Key Vault, remember that deleted secrets aren’t gone immediately. Look under “Manage Deleted Secrets” to purge them completely before you can reuse the name.

Configure app and setup local (dev) environment

There are no much to code, but if something goes sideways, you can compare it to example repo.

The following example assumes your appSettings.json looks like this

{
  "AppName": "My App",
  "KeyVault": {
    "Url": "https://my-app-key-vault.vault.azure.net/"
  }
}

Connect an application

Add configuration source

// required nuget packages:
// - Azure.Extensions.AspNetCore.Configuration.Secrets
// - Azure.Identity

using Azure.Identity;

var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddAzureKeyVault(
    vaultUri: new Uri(builder.Configuration.GetValue<string>("KeyVault:Url")!),
    credential: new DefaultAzureCredential());

Just for local testing purposes, let’s add a silly route, which obviously should never see a production.

var app = builder.Build();

// a silly route for testing
app.MapGet("/", (IConfiguration cfg) => Results.Json(new
{
    AppName = cfg.GetValue<string>("AppName")
    ConnectionString = cfg.GetConnectionString("Sql"),
    SendGridApiKey = cfg.GetValue<string>("ApiKeys:SendGrid"),
}));

await app.RunAsync();

That’s it! No more changes to the app are required. No password or access keys or anything else.

The environment where the app runs must be connected to Key Vault, and that’s what we are going to go though further in the article starting with Local Environment.

Local overrides

Just one thing before going forward.

As KeyVault is added after all other (default) configuration sources, the values in Key Vault, if present, will have priority over all others. For local overrides, I prefer to use secrets configuration source.

using Azure.Identity;

var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddAzureKeyVault(
    vaultUri: new Uri(builder.Configuration.GetValue<string>("KeyVault:Url")!),
    credential: new DefaultAzureCredential())
    .AddUserSecrets(typeof(Program).Assembly);

Connecting local environment (your PC)

There is a chance you are already connected.

Let’s first figure out how it works.

Connecting Key Vault as a configuration source, we passed DefaultAzureCredential, which will try to resolve Key Vault credentials from different sources in given order and will use the first one it finds.

DefaultAzureCredential access token resolution order

  1. EnvironmentCredential
  2. WorkloadIdentityCredential
  3. ManagedIdentityCredential
  4. SharedTokenCacheCredential
  5. VisualStudioCredential
  6. AzureCliCredential
  7. AzurePowerShellCredential
  8. AzureDeveloperCliCredential
  9. InteractiveBrowserCredential

Important — it will pick first found credential, if the credential is invalid (like expired) or targets different account or subscription, it will fail to connect.

As mentioned before, you might be logged in already, you can try to run the app and see if it works. If not in the next chapter, we’ll see how to connect using Azure CLI and even further how to troubleshoot the connection.

Connect with Az CLI

If you happened not to have it, you can install it on Windows, Linux and even macOS. Anyways it’s a must have tool workigng with Azure.

After install run az login to login.
You can verify that you are logged in using Az CLI using az account show command.
The az account show will display (as json) currently selected subscription (check the name field).
If you see incorrect subscription selected, you can use az account list to list all subscriptions available on your account.

To select the correct run az account set -s "SubscriptionId or Name"

Once you are done, you should be able to run the app.

Troubleshooting connection

As DefaultAzureCredential uses different sources occasionally it might become a pain in the ass.
For example, you just logged in using Azure CLI, yet a while ago you logged inVisual Studio with another account, or just haven’t used Visual Studio for a while and access token expired.
As Visual Studio has higher priority the invalid token will be used.

You can trace the token resolution process, by adding a code line, like on the example below.

using System.Diagnostics.Tracing;
using Azure.Core.Diagnostics;
using Azure.Identity;

using var listener = AzureEventSourceListener.CreateConsoleLogger(EventLevel.Verbose);

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddAzureKeyVault(
    vaultUri: new Uri(builder.Configuration.GetValue<string>("KeyVault:Url")!),
    credential: new DefaultAzureCredential());

Don’t panic when you see errors in a resolution process, read full trace, it’s a try and move on a process. For example ManagedIdentityCredential will try to connect to a local (available only on cloud environment) endpoint to receive credentials, that will fail locally for obvious reasons, and resolution will move on.

Custom chain

Having a problem with default credential resolution chain, or for optimization purposes, you can define your own.

Given credential must implement TokenCredential. package Azure.Identity comes with many different implementations. One of them is ChainedTokenCredential that requires array of TokenCredential which will be tried in a given order. In a way it is similar to AzureDefaultCredential except it allows you to define what kind of credentials to try and in which order.

The following example defines a custom chain using only token sources used in this example.

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddAzureKeyVault(
    vaultUri: new Uri(builder.Configuration.GetValue<string>("KeyVault:Url")!),
    credential: new ChainedTokenCredential(
        new EnvironmentCredential(),        // for app running anywhere
        new AzureCliCredential(),           // for local development environment
        new WorkloadIdentityCredential(),   // for web / container app and AKS
    ));

Web App (App Service) / Container Instance

The steps to connect Web App (App Service) or Container Instance are the same, therefore, I’ll cover both at once.

First, we need to enable System Assigned Identity, below are examples of Web App (App Service) and Container Instance.

Enable System Assigned Identity

The System Assigned Identity — is an Azure feature that will register an identity in Entra ID for a given service (e.g., WebApp or ContainerApp) as well as will delete it after service is deleted. The provisioned identity credentials will be served via internal http endpoint consumed by WorkloadIdentityCredential which is a part of AzureDefaultCredential chain. As a result, your application will connect to Key Vault using system assigned identity.

Using Az CLI

# for Web App (App Service)
az webapp identity assign --name my-web-app --resource-group passwordless-example

# for Container App
az containerapp identity assign --name my-container-app --resource-group passwordless-example

Using Azure Portal

enable app service identityenable container instance identity

Assign Key Vault Secrets User role to System Assigned Identity

By default, system assigned identity doesn’t have any permissions. To allow it to read secrets from Key Vault, we need to assign it to Key Vault Secrets User role.

Using Az CLI

RESOURCE_GROUP="passwordless-example"
WEBAPP_NAME="my-web-app"
KEYVAULT_NAME="my-app-key-vault"
ROLE_NAME="Key Vault Secrets User"

PRINCIPAL_ID=$(az webapp show \  # use "az container app show" for container apps
  --name $WEBAPP_NAME \
  --resource-group $RESOURCE_GROUP \
  --query identity.principalId \
  --output tsv)

KEYVAULT_ID=$(az keyvault show \
  --name $KEYVAULT_NAME \
  --query id \
  --output tsv)

az role assignment create \
  --assignee $PRINCIPAL_ID \
  --role "$ROLE_NAME" \
  --scope $KEYVAULT_ID

Using Azure Portal

Key Vault access control homeAdd Key Vault Secrets UserKey Vault add Web App

That’s it — deployed as WebApp or Container App the app should work.

Azure Kubernetes Service (AKS)

Running in AKS pod the app will retrieve credential using WorkloadIdentityCredential which also is a part of DefaultAzureCredential chain, however by default it will be getting credentials for identity provisioned for node pool. Technically, you could give node pool identity access to your Key Vault, but this is a very bad practice. That would give any software running on that node pool, including 3rd party tools, access to Key Vault.

Important: Do not give node pool identity access to your Key Vault

Instead, we’ll create our own Managed Identity and will configure kubernetes to provision credentials for this identity to our app pods.

One more thing.
In this example I’ll create Identity called my-app-identity and my-app-service-account implying that both are “app specific”, however you can use the same identity and/or service account for various apps.
For example an Identity per Environment.

Enable Workload Identity feature on AKS

In order to use Workload Identity in AKS, it must be enabled first as well as OidcIssuer, as Workload Identity relies on it.

Using Az CLI

Use the query below to check for current status.

az aks show --name my-k8s \
  --resource-group my-k8s \
  --query "{oidcIssuerProfile:oidcIssuerProfile, securityProfileWorkloadIdentity:securityProfile.workloadIdentity}"

Use the following commands to enable features that are not enabled.

# to Enable OidcIssuer
az aks update --name my-k8s --resource-group my-k8s --enable-oidc-issuer

# to Enable Workload Identity
az aks update --name my-k8s --resource-group my-k8s --enable-workload-identity

Using Azure Portal

Locate AKS OIDC Issuer URL

Create Managed Identity

This will be the identity used by the app. In Key Vault we’ll assign Key Vault Secrets user role to it, to grand permission to read secrets.

FIY: The resource group for managed identity doesn’t have to match the cluster.

Using Az CLI

az identity create --resource-group passwordless-example --name my-app-identity

In successful response will be a json object, from where we’ll need a clientId and tenantId. Don’t worry if you lost it, you can always see registered identities using

# the resource group is optional parameter for filtering purposes
az identity list -o table --resource-group passwordless-example

Using Azure Portal

Go to Managed Identities and create new one.

Find Managed IdentitiesAdd Managed IdentityManaged Identity Props

Open created Managed Identity and capture clientId and tenantId.

Find Managed IdentitiesAdd Managed IdentityManaged Identity Props

Assign Key Vault Secrets User role to the Identity

Using Az CLI

IDENTITY_CLIENT_ID=$(az identity show \
  --name "my-app-identity" \
  --resource-group "passwordless-example" \
  --query "clientId" \
  --output tsv)

KEY_VAULT_ID=$(az keyvault show \
  --name "my-app-key-vault" \
  --resource-group "passwordless-example" \
  --query "id" \
  --output tsv)

az role assignment create \
  --assignee "$IDENTITY_CLIENT_ID" \
  --role "Key Vault Secrets User" \
  --scope "$KEY_VAULT_ID"

Using Azure Portal

KeyVault access control homeKeyVault add Key Vault Secrets UserKeyVault add Managed Identity

Create Service Account

Create a service account in the same namespace where the app pod will be placed. For the sake of simplicity, I’ll use default namespace.

The labels and annotations in given example are required. azure.workload.identity/use must be true. Provide the clientId and tenantId of the identity created in previous exercise.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-app-service-account
  namespace: default
  labels:
    azure.workload.identity/use: "true"
  annotations:
    azure.workload.identity/client-id: e5b81681-2513-4e01-ad92-f37633e7a1ec
    azure.workload.identity/tenant-id: e3646567-309b-4f27-b943-7afc7bb7e198

Create Federated Credential

This will be the bridge between Managed Identity and Kubernetes Service Account. The name of federated credential is irrelevant, however, I prefer it to reflect exactly is it bound to.

Using Az CLI

Subject system:serviceaccount:NAMESPACE:SERVICE_ACCOUNT_NAME

# get cluster OIDC Issuer Url
AKS_OIDC_ISSUER_URL=$(az aks show \
  --name my-k8s \
  --resource-group my-k8s \
  --query "oidcIssuerProfile.issuerUrl" -o tsv)

# create federated credential
az identity federated-credential create \
  --name "my-k8s-default-my-app-service-account" \
  --identity-name "my-app-identity" \
  --resource-group "passwordless-example" \
  --issuer $AKS_OIDC_ISSUER_URL \
  --subject "system:serviceaccount:default:my-app-service-account"

Using Azure Portal

Locate AKS OIDC Issuer URLAdd Managed IdentityManaged Identity Props

Deploy the Pod

Now you can deploy the pod labeling it with azure.workload.identity/use: "true" and specifying the service account we’ve created.

Assuming you have built a docker image and pushed it into cluster connected registry.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app-pod
  template:
    metadata:
      labels:
        app: my-app-pod
        azure.workload.identity/use: "true"
    spec:
      serviceAccountName: my-app-service-account
      containers:
        - name: my-app-pod
          image: mypasswordlessregistry.azurecr.io/my-app:latest
          ports:
            - containerPort: 8080

Any other way hosted using Environment Variables

The difference with this approach is that we’ll create an identity used by the service manually, and will pass credential via Environment Variables which will be consumed by EnvironmentCredential which is a part of AzureDefaultCredential. As a result, your application will connect to Key Vault using that identity.

Environment Variables required

Create Service Account (aka App Registration)

Using Az CLI

APP_ID=$(az ad app create \
  --display-name "my-app-service-principal" \
  --query appId -o tsv)

TENANT_ID=$(az account show --query tenantId --output tsv)

az ad sp create --id $APP_ID

CLIENT_SECRET=$(az ad app credential reset \
  --id $APP_ID \
  --display-name "my-app" \
  --append \
  --query password \
  -o tsv)

echo AZURE_CLIENT_ID: $APP_ID
echo AZURE_TENANT_ID: $TENANT_ID
echo AZURE_CLIENT_SECRET: $CLIENT_SECRET

Using Azure Portal

Create app registration (a service account will be created automatically)

Find Entra ID in storeOpen App RegistrationsRegister App (Service Principal)

Create and capture secret

Service Principal viewService Add SecretService Secret Added

Give Service Principal permissions to read secrets from Key Vault

Using Az CLI

SERVICE_PRINCIPAL_ID=$(az ad sp show --id $APP_ID --query id -o tsv)

KEY_VAULT_ID=$(az keyvault show --name "my-app-key-vault" --query id -o tsv)

az role assignment create \
  --assignee-object-id $SERVICE_PRINCIPAL_ID \
  --assignee-principal-type ServicePrincipal \
  --role "$ROLE_NAME" \
  --scope $KEY_VAULT_ID

Using Azure Portal

KeyVault access control homeAdd Key Vault Secrets UserKeyVault add Service Principal

Testing Environment Credential locally

For testing purposes, you can specify all three Environment Variables in Properties/launchSettings.json or for even run your app in docker passing Environment Variables as parameters. Second approach would be even better, because you’d know for sure that running from image locally it can’t access Key Vault in any other way but using EnvironmentCredential.

Local docker run example

# assuming you have build your app with tag my-app
docker run -e AZURE_CLIENT_ID=93e498b9-0d3c-4613-b0f4-059ebdd1b976 \
  -e AZURE_TENANT_ID=e3646567-309b-4f27-b943-7afc7bb7e198 \
  -e AZURE_CLIENT_SECRET=vr78Q~TTU~EBNkN5UB24H98TbFvXpuoHnU3Htc~g \
  my-app
Key Vault for aspnet core app secrets on Azure and local dev environmentGoalHow the config will get transformedSetup Key VaultCreate Key VaultAssign yourself Key Vault Secrets Officer roleAdd configuration values into VaultConfigure app and setup local (dev) environmentConnect an applicationLocal overridesConnecting local environment (your PC)Connect with Az CLITroubleshooting connectionCustom chainWeb App (App Service) / Container InstanceEnable System Assigned IdentityAssign Key Vault Secrets User role to System Assigned IdentityAzure Kubernetes Service (AKS)Enable Workload Identity feature on AKSCreate Managed IdentityAssign Key Vault Secrets User role to the IdentityCreate Service AccountCreate Federated CredentialDeploy the PodAny other way hosted using Environment VariablesCreate Service Account (aka App Registration)Give Service Principal permissions to read secrets from Key VaultTesting Environment Credential locally