Collaboration Not Competition

Managing Secrets in Dev and Prod with .NET Core 3.1 Secret Manager tool and Azure Key Vault

July 30, 2020

There is some great documentation on the Microsoft docs site that covers configuration of Azure Key Vault and using the .NET Core Secret Manager and also how to then connect Azure Key Vault to an Azure App Service but there isn’t anything that I have found that ties them all together. This is a blog post that attempts to do that specifically for when working with a .NET Core web application and deploying to Azure App Service.

I am going to try and not repeat too much of what is already out there and instead focus on joining the dots.

The first step is figuring out how we will manage secrets in the development environment. Some of the docs demonstrate how to do this by connecting to Azure Key Vault. I don’t want to do this in a development environment as this assumes you have an Internet connection to fetch secrets during development. Instead I am going to use the .NET Core Secret Manager tool that comes with .NET Core. To set this up use the following - Using Secret Manager.

The Secret Manager tool will handle setting secrets in development but next we need a way to manage secrets once the web application is deployed to an Azure App Service. To do this we need to setup an Azure Key Vault and an Azure App Service to deploy to - Azure resource setup.

To connect the Azure App Service to Key Vault you need to:

  1. Go to the Identity menu option in the App Service and turn “System assigned managed identity” to “On” - this will create a managed identity for the App Service that can be given permissions to get secrets from key Vault for that App Service.
  2. Use the Azure Cli to give this identity permissions to the Key Vault

az keyvault set-policy --name {KEY VAULT NAME} --object-id {OBJECT ID} --secret-permissions get list
view raw az cli hosted with ❤ by GitHub

The final step is to populate your app config with the secrets for development from the Secret Management tool and for production from Azure Key Vault. To do this you will need to add the Nuget packages for Azure.Identity and Azure.Security.KeyVault.Secrets. The code to access the Key Vault is run in Startup.cs and the secrets are added to a singleton service which minimises the number of times your application needs to call the Key Vault API keeping costs to a minimum.

The following Startup.cs for an MVC web application shows how to add secrets from Key Vault and Secret Manager to a config object that can then be injected where needed in your application:

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using VSTestKeyVault.Models;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Azure.Core;
using System.Data.SqlClient;
using VSTestKeyVault.Database;
using Microsoft.EntityFrameworkCore;
namespace VSTestKeyVault
{
public class Startup
{
private IWebHostEnvironment CurrentEnvironment{ get; set; }
public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
CurrentEnvironment = env;
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
AppConfig appConfig = Configuration.GetSection("AppSettings").Get<AppConfig>();
services.AddSingleton(appConfig);
SecretConfig secretConfig = new SecretConfig();
services.AddControllersWithViews();
var builder = new SqlConnectionStringBuilder(
Configuration.GetConnectionString("BeautifulLoopProcessing"));
if(CurrentEnvironment.IsDevelopment())
{
//if the environment is development then fetch secrets from the Secret Manager
secretConfig = Configuration.Get<SecretConfig>();
builder.Password = Configuration["BeautifulLoopProcessingDbPassword"];
}
else
{
//otherwise fetch secrets from Azure Key Vault
SecretClientOptions options = new SecretClientOptions()
{
Retry =
{
Delay= TimeSpan.FromSeconds(2),
MaxDelay = TimeSpan.FromSeconds(16),
MaxRetries = 5,
Mode = RetryMode.Exponential
}
};
var client = new SecretClient(new Uri(appConfig.AzureKeyVault), new DefaultAzureCredential(),options);
KeyVaultSecret secret = client.GetSecret("MySecret");
secretConfig.MySecret = secret.Value;
KeyVaultSecret dbPasswordSecret = client.GetSecret("BeautifulLoopProcessingDbPassword");
builder.Password = dbPasswordSecret.Value;
}
services.AddSingleton(secretConfig);
string connection = builder.ConnectionString;
services.AddDbContext<ManagementReportingContext>(
options => options.UseSqlServer(connection));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
view raw Startup.cs hosted with ❤ by GitHub

appsettings.Development.json looks like this:

{
"AppSettings":{
"AzureKeyVault":"https://hours-key-vault.vault.azure.net/",
"Title": "Something that is not secret"
},
"Comment":"Connection strings don't include passwords - these are stored in Secret Manager or Azure key Vault",
"ConnectionStrings": {
"BeautifulLoopProcessing" : "Server=tcp:xxx.database.windows.net,1433;Initial Catalog=xxx-xxx-xxx-dev;Persist Security Info=False;User ID=xxxxx;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
}
}