In this article we will talk about C# secret management.
Sensitive data leakage
Sensitive information that shouldn’t be stored in source code. Different sorts of credentials, API keys, SSH keys, encryption keys, database passwords, are examples of sensitive information that should be avoided from being leaked into source code.
Why not hard-coding sensitive data?
Storing sensitive data such as passwords in source code makes the application vulnerable to security attacks. One of the reasons is that most of the time not all developers need to know all credentials. Even if all developers are allowed to access all credentials, development environments, deployment pipelines, and servers are not designed to keep hard coded secrets safe. Another issue with hard-coding passwords is that the administrator would be unable to change the secrets at runtime. He may be forced to disable the product entirely in case of secret theft.
How to Store Sensitive Data for Development?
Sensitive data should be stored in a different location from the project tree. Source code is best not to depend on the location or format of the data because they may change. Secret Manager is a tool that stores sensitive data during the development of ASP.NET projects, and hides implementation details such as location and format of the data being stored.
Let’s quickly see it in action.
Step by Step Guide to C# Secret Management
Let’s create an empty web project first
1 2 |
dotnet new web |
Now let’s open the project using an IDE.
And to enable secret storage let’s run the init
command in the project’s directory:
1 2 |
dotnet user-secrets init |
Now let’s set a new secret
1 2 |
dotnet user-secrets set "SecretManagementProject:AKeyToASecretString" "some value" |
To check if the secret is stored successfully I can list
all secrets.
1 2 |
dotnet user-secrets list |
The list currently contains a single secret.
1 2 |
SecretManagementProject:AKeyToASecretString = some value |
The secret is stored successfully. But how to access it from code?
Let’s open the Program.cs
file.
1 2 3 4 |
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.Run(); |
Let’s read the secret we just stored using the configuration API, and show its value as the root of the web API is requested.
1 2 3 4 5 6 7 8 9 10 |
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); //Read the secret we stored using Secret Manager var secret = builder.Configuration["SecretManagementProject:AKeyToASecretString"]; //Reveal the secret on web 😉 app.MapGet("/", () => secret); app.Run(); |
And run it. Now I can see the secret’s value in the browser.
Mapping Secrets to Dotnet Classes
The secret can also be directly mapped to the properties of a simple dotnet class.
Let’s create a new class named “Settings”, and add a property with the same name as the secret’s key.
1 2 3 4 5 |
public class Settings { public string? AKeyToASecretString { get; set; } } |
Let’s open the Program.cs
file again, and load the configuration section containing the secret into the Settings file. And reveal the secret by the web API.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); //Read the secret we stored using Secret Manager var settings = builder.Configuration.GetSection("SecretManagementProject").Get<Settings>(); //Reveal the secret on web 😉 app.MapGet("/poco", () => settings.AKeyToASecretString); app.Run(); |
Accessing Secrets From a Controller
Let’s add a controller named SecretController
with an action which returns an empty string.
1 2 3 4 5 6 7 8 9 |
[ApiController] [Route("controller")] public class SecretController : ControllerBase { [HttpGet] public string Get() => ""; } |
The Program.cs
file should look like this:
1 2 3 4 5 6 |
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); var app = builder.Build(); app.MapControllers(); app.Run(); |
To returns the secret let’s inject the Settings
class we built in previous section to the controller’s constructor and instead of returning the empty string return the secret’s value.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[ApiController] [Route("controller")] public class SecretController : ControllerBase { private readonly Settings settings; public SecretController(Settings settings) => this.settings = settings; [HttpGet] public string Get() => settings.AKeyToASecretString ?? "Not found"; } |
We should also configure the web application from Program.cs
file. The Settings
class should be added to the service collection.
1 2 3 4 5 6 7 8 9 10 11 12 |
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); //Read the secret we stored using Secret Manager var settingToBeInjected = builder.Configuration.GetSection("SecretManagementProject").Get<Settings>(); //Add it to the service collection. builder.Services.AddSingleton(settingToBeInjected); var app = builder.Build(); app.MapControllers(); app.Run(); |
Let’s run the application and open the /controller
route in the browser to see the secret.
Storing Connection String Passwords
Rather than storing passwords to database connection strings in plain text in configuration files, I would omit the password from connection strings, and store the password using Secret Manager. To add the password to a connection string at run time I would use the Configuration API to load both the connection string and its password individually. I can then use the ConnectionStringBuilder
class to add the password to the connection string at run time.
Let’s see it in action. Let’s open the appsettings.json
file, and add a connection string to it.
1 2 3 4 5 6 |
{ "ConnectionStrings": { "MyInsecureDb": "Server=(localdb)\\mssqllocaldb;Database=MyDb;User Id=Mohsen;Password=dbPassValue;MultipleActiveResultSets=true", } } |
And remove the password from the connection string
1 2 3 4 5 6 |
{ "ConnectionStrings": { "MySecureDb": "Server=(localdb)\\mssqllocaldb;Database=MyDb;User Id=Mohsen;MultipleActiveResultSets=true" } } |
And add it to the user’s secrets.
1 2 |
dotnet user-secrets set "DbPass" "dbPassValue" |
To read the connection string let’s open the Program.cs file, and read the connection string, the password, add the password to the connection string, and finally reveal it on the web.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); //Load the connection string: var conStrBuilder = new SqlConnectionStringBuilder( builder.Configuration.GetConnectionString("MySecureDb")); //Load the password: var dbPassword = builder.Configuration["SecretManagementProject:AKeyToASecretString"]; //Add the password to connection string to make it usable conStrBuilder.Password = builder.Configuration["DbPass"]; //Reveal the complete connection string via web API app.MapGet("/constr", () => conStrBuilder.ConnectionString); app.Run(); |
Now let’s run it, switch to constr
route, and this is the complete connection string containing the password.
Removing Secrets
To remove a secret go to project’s folder and run this command:
1 2 |
dotnet user-secrets remove SECRET_KEY |
Clearing All Secrets
To clear all secrets go to the project’s folder and run this command:
1 2 |
dotnet user-secrets clear |
Where Are The Secrets Stored Physically?
Whenever I call dotnet user-secrets set ...
the secret is stored in a secrets.json
file by default. secrets.json
file is stored in the local machine’s user profile folder:
Linux/macOS | ~/.microsoft/usersecrets/<user_secrets_id>/secrets.json |
Windows | %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json |
To find the <user_secrets_id> let’s open the project’s file. A new element is added under the <PropertyGroup>
when I ran dotnet user-secrets init
. It contains an arbitrary GUID.
1 2 3 4 5 |
<PropertyGroup> ... <UserSecretsId>18ec44c5-32ba-4b59-ac75-f04bb7d0c0e4</UserSecretsId> </PropertyGroup> |
To open the secrets.json
file replace <user_secrets_id> with the GUID. This id connects the secrets.json
file to the app. Both Secret Manager and Configuration API use this GUID to find the secrets.json
file which belongs to your application.
Summary
Storing sensitive data in the project tree creates a significant whole that allows attackers’ access to the data. Storing the data to a file with a custom path outside of the project tree and hard-coding it’s address on the other hand couples application to the platform.
Dotnet’s Secret Manager tool provides a simple command line interface for developers to store their development time secrets to files that Configuration API loads them by default.
Configuration API provides a simple interface that decouples configuration data from its source so that the data source can be changed without the need to change the source code.
Consequently developers can store secrets to
and read it using Configuration API. In production the secret’s data source can be whatever as long as a configuration provider loads it and makes it accessible through Configuration API.secrets.json
Different databases must have different passwords in different machines for example, but this requirement shouldn’t require the source code to be changed for each machine.