Azure Key Vault provides secure storage of keys and other secrets to be used by other Azure services and apps. It is analogous to AWS Secrets Manager.


Update (Nov. 2022): The Microsoft.Extensions.Configuration.AzureKeyVault has now been deprecated. I'll eventually get around to updating this post to account for these changes ... someday.


Previously I'd added sensitive API keys, secrets and Connection Strings to Azure App Service Application Settings and Connection Strings which do offer a level of security in that they are encrypted at rest, but the Azure Key Vault appears to offer a greater level of security, so I've just made use of an Azure Key Vault for my latest project.

Unfortunately, the available information on what's needed to be able to access these secrets is a bit piecemeal & there's a few gotcha's along the way.

The best start point is the Microsoft article Azure Key Vault Configuration Provider in ASP.NET Core which explains how to make use of the Secret Manager tool when developing locally to store secrets in the local Secret Manager store. This will allow you to develop locally without needing to make use of the Azure Key Vault during development.

When it comes time to set up your Azure Key Vault however, the article does rely on the Azure CLI - which is certainly one way to go, but the Azure Portal is just as easy.

alt text

Select an existing resource group (or create a new one), enter a Key vault name, select a Region and click Review + create. Its possible to fine tune the access policy, networking and add Tags using additional steps, but for the purpose of storing and accessing secrets from App Services, the defaults are enough here.

Add your secret/secrets to the Key Vault by selecting the Key Vault in the list of Key Vaults, then selecting Settings / Secrets then + Generate/Import

alt text

Enter a Name and a Value for the secret - obviously the Name has to match the name given the secret added to the local Secret Manager when doing your development.

alt text

To access the Azure Key Vault in an ASP.Net Core 3.1 web application, add a package reference to the Microsoft.Extensions.Configuration.AzureKeyVault package. There are other packages mentioned around the web, but at least one of those is now deprecated.

Open appsetting.json and add a key/value called KeyVaultName with the value set to the name you gave when creating your Azure Key Vault above (e.g. AppDefaultKeyVault above)

alt text

Add the following code to Program.cs CreateHostBuilder method

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((context, config) =>
            {
                if (context.HostingEnvironment.IsProduction())
                {
                    var builtConfig = config.Build();
 
                    var azureServiceTokenProvider = new AzureServiceTokenProvider();
 
                    var keyVaultClient = new KeyVaultClient(
                        new KeyVaultClient.AuthenticationCallback(
                            azureServiceTokenProvider.KeyVaultTokenCallback));
 
                    config.AddAzureKeyVault(
                        $"https://{builtConfig["KeyVaultName"]}.vault.azure.net/",
                        keyVaultClient,
                        new DefaultKeyVaultSecretManager());
                }
            })
 
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

** Gotcha #1:** Wrapping the creation of the KeyVaultClient in the test context.HostingEnvironment.IsProduction is useful to avoid using the Key Vault during development, however when this is deployed to an Azure App Service, its necessary to set the ASPNETCORE_ENVIRONMENT Application Setting to Production

alt text

Secrets stored in the Key Vault can now be accessed in Startup.ConfigureServices as if they were local configuration settings.

For example, when adding OAth authentication in Startup.ConfigureServices to allow logging in using a third party, its necessary to provide Client Ids/Secrets - these are easily stored and read from the Azure Key Vault

services.AddAuthentication()
    .AddTwitter(twitterOptions =>
    {
        twitterOptions.ConsumerKey = Configuration["TwitterAPI-ConsumerAPIKey"];
        twitterOptions.ConsumerSecret = Configuration["TwitterAPI-ConsumerSecret"];
        twitterOptions.RetrieveUserDetails = true;
    })
    .AddMicrosoftAccount(microsoftOptions =>
    {
        microsoftOptions.ClientId = Configuration["Microsoft-ClientId"];
        microsoftOptions.ClientSecret = Configuration["Microsoft-ClientSecret"];
    });

alt text

It is also possible to either store a complete connection string in the Azure Key Vault, or just the DbUserId and DbPassword and build the connection string using the SqlConnectionStringBuilder, reading the connection string (without UserId and Password stored) and then read the UserId and Password from the Azure KeyVault.

For example, the appsettings.json has a ConnectionStrings:DefaultConnection of;

"ConnectionStrings": {
  "DefaultConnection": "Server=tcp:xxxxxx-sql.database.windows.net,1433;Initial Catalog=xxxxx;Persist Security Info=False;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
},

and Azure Key Vault has the DbUserId and DbPassword stored. The full connection string can then be created in Startup.ConfigureServices like this;

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });
 
    var builder = new SqlConnectionStringBuilder(Configuration.GetConnectionString("DefaultConnection"))
    {
        UserID = Configuration["DbUserId"], 
        Password = Configuration["DbPassword"]
    };
 
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            builder.ConnectionString));

Gotcha #2: Some additional configuration is needed in Azure once the App Service has been deployed before the App Service can access the secrets in the Azure Key Vault. Failure to do so will result in an inaccessible web site and the following error logged to the Application Event Logs for the App Service.

Application: w3wp.exe
CoreCLR Version: 4.700.20.41105
.NET Core Version: 3.1.8
Description: The process was terminated due to an unhandled exception.
Exception Info: Microsoft.Azure.Services.AppAuthentication.AzureServiceTokenProviderException: Parameters: Connection String: [No connection string specified], Resource: https://vault.azure.net, Authority: https://login.windows.net/fbf4e279-e268-43c0-9379-628db4cad246. Exception Message: Tried the following 3 methods to get an access token, but none of them worked.
Parameters: Connection String: [No connection string specified], Resource: https://vault.azure.net, Authority: https://login.windows.net/fbf4e279-e268-43c0-9379-628db4cad246. Exception Message: Tried to get token using Managed Service Identity. Access token could not be acquired. An attempt was made to access a socket in a way forbidden by its access permissions.
Parameters: Connection String: [No connection string specified], Resource: https://vault.azure.net, Authority: https://login.windows.net/fbf4e279-e268-43c0-9379-628db4cad246. Exception Message: Tried to get token using Visual Studio. Access token could not be acquired. Visual Studio token provider file not found at "C:\local\LocalAppData\.IdentityService\AzureServiceAuth\tokenprovider.json"
Parameters: Connection String: [No connection string specified], Resource: https://vault.azure.net, Authority: https://login.windows.net/fbf4e279-e268-43c0-9379-628db4cad246. Exception Message: Tried to get token using Azure CLI. Access token could not be acquired. 'az' is not recognized as an internal or external command,
operable program or batch file.

This error has nothing to do with the database connection string, but is a failure of the App Service to be able to connect to the Azure Key Vault.

To resolve this error (or deal with the issue before it happens!), the App Service needs a system assigned managed identity to be able to authenticate & access Azure Key Vaults.

Open the App Service in the Azure Portal, and under Settings, select Identity, and set the Status to On. Click Save.

alt text

Allow a few minutes to pass, then click Refresh. Eventually you'll get an Object Id displayed

alt text

Copy the Object Id

Open the Key Vault you created earlier, and Select Settings / Access policies

alt text

Click + Add Access Policy, then click the Secret Permissions drop down to select the Get and List Secret Permissions

alt text

Be sure to select both Get and List. Failure to select List will result in the following error being logged

Application: w3wp.exe
CoreCLR Version: 4.700.20.41105
.NET Core Version: 3.1.8
Description: The process was terminated due to an unhandled exception.
Exception Info: Microsoft.Azure.KeyVault.Models.KeyVaultErrorException: The user, group or application 'appid=8b8b6308-{redacted};oid=78dbf548-{redacted};iss=https://sts.windows.net/fbf4e279-e268-43c0-9379-628db4cad246/' does not have secrets list permission on key vault 'DefaultKeyVault;location=australiaeast'. For help resolving this issue, please see https://go.microsoft.com/fwlink/?linkid=2125287

Next, click Select Principal. This will open a Principal window to the right. Paste into the search box the Object Id for the App Service Identity and select the App Service identity found.

IF the Object Id does not return a result, wait a few minutes - it seems sometimes to take a few minutes to propagate. If after waiting a while there are still no results, return to the App Service Identity and recheck/recopy the Object ID.

Once the Principal has been selected, Click the Add button.

Restart the App Service and it should now be able to access the secrets from Azure Key Vault.