.NET Shorts – Configuration

Problem

Have you ever opened an appsettings.json file and found something like this?

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=CarsService;User Id=SA;Password=yourStrong(!)Password;"
  },
}

You’d be surprised what hides in the appsettings.json. Worst thing that I found was username and password for Windows domain account, that has access to almost everything.

Well, hello production, what can I fuck up today?

Also, I noticed that there is some confusion on how configuration in .NET works although everything is covered in microsoft docs.

Solution

Store it somewhere else:

But, let’s start from the beginning. If you want to dig deeper, you can clone repository and move to branch shorts/configuration. There is also configuration controller where you can “query” your configuration data. Play with it.

Configuration provider in .NET reads configuration data from a variety of configuration sources:

  • appsettings.json
  • environment variables
  • Azure Key Vault
  • Command-line arguments
  • etc

And it is just a key-value pair.

When you use:

var builder = WebApplication.CreateBuilder(args);

it will read configuration data using configuration providers from lowest to highest priority.

  1. appsettings.json
  2. appsettings.{ASPNETCORE_ENVIRONMENT}.json
  3. secrets.json -> Only if it’s Development Environment.
  4. environment variables
  5. command line arguments

After a bit of digging – here is the source code

To keep things simple, lets assume that we are in Development environment.

  • How can we find in what enviroment are we? You can go to your launchSettings.json file and check value of ASPNETCORE_ENVIROMENT variable. If variable isn’t specified there, it will look at your system environment variables and if nothing is there, will fallback to Production.
  • Just put it in launchSettings.json for Development

Also, let’s assume we have appsettings.json and appsettings.Development.json.

{
  "PropertyA": "From appsettings.json" 
}
{
  "PropertyA": "From appsettings.Development.json",
  "PropertyB": "I'm here" 
}

If we try to read PropertyA, we are going to get “From appsettings.Development.json“. This means that a configuration source with higher priority has the same key as configuration source with lower priority, which means it will override PropertyA with new value.

Also, if configuration source with higher priority have extra key-value pairs, it will append those values to configuration data. When we are in Development, we can read PropertyB as well. Pretty simple, right?

How can we leverage this for our secrets then?

For local development, we are going to use secrets.json. You just need to right click on your Startup project and select Manage User Secrets. This will generate your secrets.json file and will add UserSecretsId to your .csproj file.

<UserSecretsId>cce98938-947d-45b7-86fe-161ef15f8c52</UserSecretsId>

If you wish to see your secrets file on your local HDD, go to:

%APPDATA%\Microsoft\UserSecrets\{secretId}

Now, when we are in Development our configuration provider will append anything that is in secrets.json file to our configuration data.

For production, store it somewhere else. This entirely dependes on your situation: you can use Azure Key Vault, AWS Secrets Manager, web.config, environment variables.

I like blue, so lets try out Azure Key Vault.

First, lets, look how our WebApplicationBuilder is configured.

namespace Company.Shorts
{
    using Company.Shorts.Blocks.Bootstrap;

    public static class Program
    {
        public static async Task Main(string[] args) => await ApplicationLauncher.RunAsync<Startup>(args);
    }
}
namespace Company.Shorts.Blocks.Bootstrap
{
    using Azure.Extensions.AspNetCore.Configuration.Secrets;
    using Azure.Identity;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.Hosting;
    using Serilog;

    public static partial class ApplicationLauncher
    {
        public static async Task<int> RunAsync<TStartup>(string[] args)
            where TStartup : class
        {
            var builder = new ConfigurationBuilder();

            Log.Logger = new LoggerConfiguration()
                .ReadFrom.Configuration(builder.Build())
                .CreateBootstrapLogger();

            try
            {
                Log.Information("Starting web host.");

                var host = CreateHostBuilder<TStartup>(args)
                    .ConfigureAppConfiguration((hostBuildContext, configurationBuilder) =>
                {
                    BuildConfiguration<TStartup>(configurationBuilder, hostBuildContext.HostingEnvironment, args);
                });

                await host.Build().RunAsync();

                return ExitCode.Success;
            }
            catch (Exception exception)
            {
                Log.Fatal(exception, "Host terminated unexpectedly.");
                return ExitCode.Failure;
            }
            finally
            {
                Log.CloseAndFlush();
            }
        }

        private static IHostBuilder CreateHostBuilder<TStartup>(string[] args)
            where TStartup : class
        {
            return Host.CreateDefaultBuilder(args)
                .UseSerilog((hostContext, loggerConfiguration) => loggerConfiguration.ReadFrom.Configuration(hostContext.Configuration))
                .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<TStartup>());
        }

        private static void BuildConfiguration<TStartup>(
            IConfigurationBuilder builder,
            IHostEnvironment environment,
            string[] args)
            where TStartup : class
        {
            builder
                .SetBasePath(AppContext.BaseDirectory)
                .AddJsonFile(
                    path: "appsettings.json",
                    optional: false,
                    reloadOnChange: true)
                .AddJsonFile(
                    path: $"appsettings.{environment.EnvironmentName}.json",
                    optional: true,
                    reloadOnChange: true)
                .AddEnvironmentVariables();

            if (environment.IsDevelopment())
            {
                builder.AddUserSecrets<TStartup>();
            }

            if (environment.IsProduction())
            {
                var keyVaultName = builder.Build().GetSection("Azure:KeyVault:Name").Value;

                builder.AddAzureKeyVault(
                    new Uri($"https://{keyVaultName}.vault.azure.net/"),
                    new DefaultAzureCredential(new DefaultAzureCredentialOptions
                    {
                        ExcludeAzureCliCredential = true,
                        ExcludeAzurePowerShellCredential = true,
                        ExcludeEnvironmentCredential = true,
                        ExcludeInteractiveBrowserCredential = true,
                        ExcludeManagedIdentityCredential = true,
                        ExcludeSharedTokenCacheCredential = true,
                        ExcludeVisualStudioCodeCredential = true,
                        ExcludeVisualStudioCredential = false
                    }),
                    new AzureKeyVaultConfigurationOptions()
                    {
                        Manager = new PrefixKeyVaultSecretManager(environment.EnvironmentName, environment.ApplicationName)
                    });
            }

            builder.AddCommandLine(args);

            builder.Build();
        }
    }
}

As you can see, if we are in production we are going to use AddAzureKeyVault extension method which will append secrets to our configuration data.

When I add secrets to Azure Key Vault I tend to follow this pattern: [Environment]-[ApplicationName]-[Key]

Production-CompanyShorts-PropertyA

or in case of a nested property

Production-CompanyShorts-PropertyC--PropertyC1

In order to make this work, lets take a look at PrefixKeyVaultSecretManager.cs

namespace Company.Shorts.Blocks.Bootstrap
{
    using Azure.Extensions.AspNetCore.Configuration.Secrets;
    using Azure.Security.KeyVault.Secrets;
    using Microsoft.Extensions.Configuration;

    internal class PrefixKeyVaultSecretManager : KeyVaultSecretManager
    {
        private readonly string prefix;

        public PrefixKeyVaultSecretManager(string environment, string aplicationName)
        {
            var parsedApplicationName = aplicationName.Replace(".", string.Empty);

            this.prefix = $"{environment}-{parsedApplicationName}-";
        }

        public override bool Load(SecretProperties properties)
        {
            return properties.Name.StartsWith(this.prefix);
        }

        public override string GetKey(KeyVaultSecret secret)
        {
            return secret.Name[this.prefix.Length..].Replace("--", ConfigurationPath.KeyDelimiter);
        }
    }
}

Since I wanted to test it locally, I authenticate with Azure using VisualStudioCredential. When you go to the production this isn’t the best option and you should pick approach that best suits your needs.

Benefits?

  • your secrets are safe + they are not on the github
  • your development is easier
  • no one (me) will laugh when I see your windows username and password in appsettings.json 🙂
  • it makes your CI/CD life easier
  • now that you tried it out, created an account on Azure, why not get those certificates as well? 😛