.NET Tools Rider

Securing Sensitive Information with .NET User Secrets

Working with secure keys in .NET has always been a delicate balance of convenience and safety. It’s easy to add a production database connection string to your application’s configuration for quick testing, only to accidentally check it into source control. The embarrassment of such a mistake is not a fun experience to have. Luckily, with recent iterations of .NET, there is a straightforward way to store sensitive information during local development.

In this post, I will introduce you to .NET User Secrets and how to use the feature to store sensitive values locally during development, significantly reducing the chance of exposing secrets.

The .NET Secrets Manager

In .NET Core 3.1, the SDK introduced the secrets manager, a command-line utility that uses your current project to create a unique store for project-related secrets. When initialized, .NET will store your project secrets in a user-specific directory outside the current project. Depending on your host operating system, these folders differ on Windows and Linux/macOS. 

Keeping secrets outside your project reduces the chance of accidentally committing secrets to source control. The folder containing your secrets is unique to each project and utilizes a UserSecretsId MSBuild element in your project files. The value can be any unique identifier, and .NET templates typically initialize the value for you.

<PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UserSecretsId>dotnet-UserSecrets-3825E1F5-F0B3-4C37-80C4-D5F24C7F6645</UserSecretsId>
</PropertyGroup>

You can also use MSBuild properties as your UserSecretsId value. Here’s a neat trick to use your ProjectName value. Just be sure your project names are unique across your development environment.

<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <UserSecretsId>$(ProjectName)</UserSecretsId>
    </PropertyGroup>

</Project>

If your project is missing the UserSecretsId element, you can initialize your project in two ways.

First, you can run the following command from JetBrains Rider’s terminal in the project directory.

dotnet user-secrets init

The more accessible approach is to right-click the project in the Solution Explorer window and navigate to Tools | .NET User Secrets. JetBrains Rider will initialize the project and open a secrets.json in your user directory.

Enabling .NET User Secrets using JetBrains Rider’s context menu action.

Most project templates in .NET already have User Secrets initialized for you. As you’ll see in the following section, accessing secrets relies on the .NET Configuration system, a foundational set of APIs in the .NET ecosystem.

Managing User Secrets in .NET

Once you’ve initialized your project, you’ll want to manage your secrets. The secrets.json file is similar to the appsettings.json configuration file used in many .NET projects. You can have complex structured configuration data, but typically you’ll find key-value pairs in these files.

You can start editing this file, but be sure that you follow the JSON format. When attempting to edit the secrets.json file, you may get a warning about editing a non-project file, which is there to prevent you from accidentally editing files outside your project (and source control). Just click OK to allow file editing.

Non-project files protection dialog showing the location of the .NET User Secrets project file.

You can add anything you like here, but let’s start with a simple Hello, World console application.

Hello, World with User Secrets

This section’s exercise will help you understand how to load user secrets and how other project templates might as well.

First, create a brand new Console Application project. This project template typically lacks dependencies, so you’ll need to add the configuration packages. 

You’ll add the NuGet package called Microsoft.Extensions.Configuration.UserSecrets in the NuGet tool window. This dependency will allow you to create a configuration instance and load your user secrets file. Once you have installed the dependency, initialize the project using the Tools | .NET User Secrets context menu element, as shown previously.

With the secrets.json file open, copy in the following JSON. Feel free to substitute your name.

{
   "Name" : "Khalid"
}

Let’s finish our secrets-powered application with the following code in Program.cs.

using Microsoft.Extensions.Configuration;

var config = new ConfigurationBuilder()
    .AddUserSecrets<Program>()
    .Build();

Console.WriteLine($"Hello, {config["Name"]}");

Note that you’re using the ConfigurationBuilder type to build an IConfiguration instance. Accessing your user secrets uses the same keying approach as all other configuration values.

An essential part of the AddUserSecrets call is its generic type argument. The UserSecretsId MSBuild property value is added to your assembly and accessed using the UserSecretsIdAttribute custom attribute. Without the attribute’s value, .NET would be unable to find the location of your user secrets file.

If you’ve successfully followed this section, your program’s output should look like the following.

Hello, Khalid

User Secrets in ASP.NET Core and Worker Services

In the previous section, you took the manual approach to add user secrets to your console application. Most project templates in .NET already include user secrets by default, which typically means their secret values are ready for you to use immediately. Let’s look at where configuration providers are registered. Here you’ll find when and how .NET adds your secrets to the configuration instance.

Most ASP.NET Core applications will start with the following line of code.

var builder = WebApplication.CreateBuilder(args);

The call to WebApplication.CreateBuilder performs a lot of work here, but most importantly for this post, it builds your IConfiguration instance. As of .NET 7, the default providers include JSON, environment variables, command line arguments, and user secrets. The registration for these configuration providers occurs in HostingHostBuilderExtensions in the ApplyDefaultAppConfiguration method.

internal static void ApplyDefaultAppConfiguration(HostBuilderContext hostingContext, IConfigurationBuilder appConfigBuilder, string[]? args)
{
    IHostEnvironment env = hostingContext.HostingEnvironment;
    bool reloadOnChange = GetReloadConfigOnChangeValue(hostingContext);

    appConfigBuilder.AddJsonFile("appsettings.json", optional: true, reloadOnChange: reloadOnChange)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: reloadOnChange);

    if (env.IsDevelopment() && env.ApplicationName is { Length: > 0 })
    {
        var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
        if (appAssembly is not null)
        {
            appConfigBuilder.AddUserSecrets(appAssembly, optional: true, reloadOnChange: reloadOnChange);
        }
    }

    appConfigBuilder.AddEnvironmentVariables();

    if (args is { Length: > 0 })
    {
        appConfigBuilder.AddCommandLine(args);
    }

    [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Calling IConfiguration.GetValue is safe when the T is bool.")]
    static bool GetReloadConfigOnChangeValue(HostBuilderContext hostingContext) => hostingContext.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true);
}

Unlike your console application, The host builder will register user secrets only if your application runs in development mode. Developers like you don’t usually use user secrets outside local development scenarios. There are other mechanisms to provide secrets to your application that are easier to manage in a production environment.

Conclusion

Hopefully, you now understand how user secrets work in the scope of a .NET application, how you might add it to existing console applications yourself, and how you can use JetBrains Rider to manage your project-specific user secrets.

The complexity of what you can store in your secrets is limited to the JSON format and the keying structure of the IConfiguration object. In a sense, you could store anything in your user secrets file without concern you’ll accidentally expose it to outside parties.

It’s important to remember that the secrets.json file is not encrypted and is still accessible by any bad actor that has compromised your machine. However, they help reduce the risk of accidentally adding secrets into source control. It’s still essential to follow good security habits like rotating keys and limiting access to sensitive information to folks who require access to it. 

From a developer perspective, you’ll likely appreciate that user secrets fit into the existing configuration model and work seamlessly with the tools you love.

As always, thank you for reading this post, and if you have any questions or comments, please leave them in our comments section.

References

image description