Storing sensitive configuration data (i.e. API keys, database usernames, and passwords) appropriately is anything but a trivial concern for application developers. There are a variety of recommended approaches in ASP.NET Core, each with its own advantages and disadvantages. Making the right decision based on your application’s security requirements is critical, but not always simple. In this article, we will walk through different options for protecting your application secrets in both development and production environments.

Security in Development

It’s a poor (read: non-secure) practice to store sensitive data as constants in code or in plain text configuration files. You should always separate the configuration from the code because it varies between different environments, but the code should stay the same. This also is an obstacle if you want to make your code open source because all your sensitive data will be public.

Application secrets should also not be part of your source control repository, for the same reason. If your code goes open source, secrets become public too. Let’s see how we can store this information in a way that avoids these problems.

Environment Configurations

ASP.NET Core allows you to manage different environment configurations using the appsettings.json file.

The idea is simple: All the base configuration should be placed on the appsettings.json file. Then, you can add environment-specific configuration by creating additional configuration files where the name of each file contains the environment name they belong to, i.e. appsettings.development.json . Each of these files could be stored directly on each specific environment server.

The current environment is set via the ASPNETCORE_ENVIRONMENT environment variable. The values Development , Staging , and Production are used by convention, but you can add any environment name you like, for example, jon_snow_staging .

During application startup, the environment name will be determined from this environment variable and will load the environment-specific settings file:

public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); builder.AddEnvironmentVariables(); Configuration = builder.Build(); } 1 2 3 4 5 6 7 8 9 10 11 public Startup ( IHostingEnvironment env ) { var builder = new ConfigurationBuilder ( ) . SetBasePath ( env . ContentRootPath ) . AddJsonFile ( "appsettings.json" , optional : true , reloadOnChange : true ) . AddJsonFile ( $ "appsettings.{env.EnvironmentName}.json" , optional : true ) ; builder . AddEnvironmentVariables ( ) ; Configuration = builder . Build ( ) ; }

The order in which configuration files are listed is important because if the same setting exists on more than one file, the last one is the one that prevails.

Remember that these configuration files should not be part of your source control repository.

This approach has some drawbacks: It doesn’t scale well with the number of environments the application should handle, as every environment needs its own configuration file. It’s also a little too easy to accidentally commit them to the source control repository, as they aren’t ignored by default. And, you if you want the source control to ignore them, you have to add them manually to the ignore list.

Environment Variables

A better approach to securing your config data is to store secrets in environment variables instead of local configuration files. During application startup you can call the AddEnvironmentVariables method to read all the values defined in the environment variables.

As I mentioned before, the order of configurations on Startup is important. The suggested approach is to call this method last so that the local environment can override anything set in local configuration files.

Environment variables are completely decoupled from code, so you can change their values without changing the deployed code. Also, unlike appsettings.json files, you cannot check them into the source control accidentally and they are independent from language and the underlying OS.

But beware! Environment variables are usually not encrypted. So anyone with access to the system can read them. Also, if you need to change these values, you’ll need to restart your application in order to read the new values.

Secrets Manager

.NET Core provides a secrets manager tool which is an elegant option, if used ONLY in development, never production. This tool allows you to store your sensitive data locally on your machine, outside the project tree.

The keys are stored in a JSON configuration file in the user profile directory, and the way to access them is similar to the previous approaches:

public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() ... if (env.IsDevelopment()) { builder.AddUserSecrets(); } builder.AddEnvironmentVariables(); Configuration = builder.Build(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 public Startup ( IHostingEnvironment env ) { var builder = new ConfigurationBuilder ( ) . . . if ( env . IsDevelopment ( ) ) { builder . AddUserSecrets ( ) ; } builder . AddEnvironmentVariables ( ) ; Configuration = builder . Build ( ) ; }

This tool is not super secure, and the keys are not encrypted, but it provides an easy way to avoid storing secrets in your project config files and having to remember to add them to the source control ignore list.

Security in Production

So far, we have reviewed some options for development. The first and second approaches (appsettings and environment variables) can be viably implemented in production too.

These approaches could be a solid option if only medium-level security is required for your application, but if you need more security for your data, read on!

Secure Secrets in Azure

If you’re hosting your application on Azure, you’re able to store your secrets in the application settings of your Azure Web App. This configuration overwrites the settings you have in the configuration files, so if you have duplicated settings Azure will take precedence.

Azure also provides a more secure option: Azure Key Vault. Key Vault is a cloud-hosted service for managing secrets, which will be accessible by the applications you authorize through an encrypted channel.

Key Vault allows you to encrypt keys and secrets by using keys that are protected by hardware security modules (HSMs). An HSM is a device or module designed for the protection of the crypto key lifecycle.

Fortunately, anyone with an Azure subscription can create and use key vaults. This approach requires a little bit of setup, which you can find instructions for on the Microsoft blog.

Custom Security Providers

If you decide to store your secrets in the appsettings.json file, you can easily encrypt and decrypt them via a custom configuration provider. Your custom provider should inherit from the ConfigurationProvider class. This class has a Load method that you should override and overwrite with your custom application configuration load logic.

For example, suppose you have an encryption utility class and you create a CustomConfigProvider class that will decrypt all our configuration settings from a file and load it into Data using this class:

public class CustomConfigProvider : ConfigurationProvider { public CustomConfigProvider() { } public override void Load() { Data = MyEncryptUtils.DecryptConfiguration(); } } 1 2 3 4 5 6 7 8 9 10 public class CustomConfigProvider : ConfigurationProvider { public CustomConfigProvider ( ) { } public override void Load ( ) { Data = MyEncryptUtils . DecryptConfiguration ( ) ; } }

To hook it up on the ASP.NET Core pipeline, you need to define a custom IConfigurationSource . In the Build method you’re going to return the custom configuration provider:

public class CustomConfigurationSource : IConfigurationSource { public CustomConfigurationSource() { } public IConfigurationProvider Build(IConfigurationBuilder builder) { return new CustomConfigProvider(); } } 1 2 3 4 5 6 7 8 9 10 11 public class CustomConfigurationSource : IConfigurationSource { public CustomConfigurationSource ( ) { } public IConfigurationProvider Build ( IConfigurationBuilder builder ) { return new CustomConfigProvider ( ) ; } }

Then, create an extension method to add the custom configuration source to the configuration builder:

public static class CustomConfigurationExtensions { public static IConfigurationBuilder AddCustomConfiguration(this IConfigurationBuilder builder) { return builder.Add(new CustomConfigurationSource()); } } 1 2 3 4 5 6 7 8 9 10 public static class CustomConfigurationExtensions { public static IConfigurationBuilder AddCustomConfiguration ( this IConfigurationBuilder builder ) { return builder . Add ( new CustomConfigurationSource ( ) ) ; } }

And call it during the application startup:

var builder = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddCustomConfiguration() .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); 1 2 3 4 5 6 7 var builder = new ConfigurationBuilder ( ) . AddJsonFile ( "appsettings.json" ) . AddCustomConfiguration ( ) . AddJsonFile ( $ "appsettings.{env.EnvironmentName}.json" , optional : true ) ;

Guidelines and Next Steps

As you can see, there are several options for storing and protecting your data, each with different levels of security and ease of use. No matter which you ultimately select, follow these basic guidelines:

Never commit secrets or sensitive data to a code repository, even a private repository

Do not store secrets or sensitive data in source code

Use an encryption mechanism to keep your secrets safe

I like the ASP.NET Core Secret Manager tool because it’s easy to use and helps developers to keep their secrets out of the source control, but this is only applicable in development. For production, I utilize environment variables for most of my medium-level security applications as they are suitable for any environment. Environment variables are also easy to work into most Continuous Integration workflows, as most CI servers have a way to implement them.

What about you? Let us know what approach you’re using for your applications in the comments!

Or, if you’re interested in learning more about secure user management in ASP.NET Core, check out any of the following resources: