Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Storing production secrets in ASP.NET Core

Tags:

asp.net-core

I try to figure out where to best store application production secrets for an ASP.NET Core app. There are two similar questions Where should I store the connection string for the production environment of my ASP.NET Core app? and How to deploy ASP.NET Core UserSecrets to production which both recommend using environment variables.

My problem is that I want to run several instances of my web app with different databases and different database credentials. So there should be some per-instance configuration including secrets.

How could this be achieved in a safe way?

Note that the application should be able to self-host and be hostable under IIS! (Later we also plan to run it on Linux if that is of any importance for the question)

Update

This question is not about trying to use ASP.NET user secrets in production! UserSecrets are ruled out for production.

like image 977
NicolasR Avatar asked Oct 19 '16 12:10

NicolasR


People also ask

Where do you store production secrets?

You can store your secrets into a file in your development environment, but you should avoid committing them to your source control system. The Secret Manager helps you with this purpose. In your production environment, you should avoid storing your secrets in a file.

Where are secrets stored in .NET Core?

User Secrets is a great feature in ASP.NET Core that is an excellent alternative to using environment variables. User Secrets ensures that there is no sensitive data included in the source code. Instead, the user secrets are stored outside of the project folder — inside the user's profile folder in the file system.

How would you enable secret storage in an ASP.NET Core project?

Enable secret storage The inner text is arbitrary, but is unique to the project. In Visual Studio, right-click the project in Solution Explorer, and select Manage User Secrets from the context menu.


3 Answers

In addition to use Azure App Service or docker containers, you can also securely store your app secrets in production using IDataProtector:

  1. App secrets are entered with running a -config switch of the application, for example: dotnet helloworld -config; In Program.Main, detects this switch to let user to enter the secrets and store in a separate .json file, encrypted:
public class Program
{
    private const string APP_NAME = "5E71EE95-49BD-40A9-81CD-B1DFD873EEA8";
    private const string SECRET_CONFIG_FILE_NAME = "appsettings.secret.json";

    public static void Main(string[] args)
    {
        if (args != null && args.Length == 1 && args[0].ToLowerInvariant() == "-config")
        {
            ConfigAppSettingsSecret();
            return;
        }
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((builder, options) =>
            {
                options.AddJsonFile(ConfigFileFullPath, optional: true, reloadOnChange: false);
            })
            .UseStartup<Startup>();

    internal static IDataProtector GetDataProtector()
    {
        var serviceCollection = new ServiceCollection();
            
        serviceCollection.AddDataProtection()
            .SetApplicationName(APP_ID)
            .PersistKeysToFileSystem(new DirectoryInfo(SecretsDirectory));
        var services = serviceCollection.BuildServiceProvider();
        var dataProtectionProvider = services.GetService<IDataProtectionProvider>();
        return dataProtectionProvider.CreateProtector(APP_ID);
    }

    private static void ConfigAppSettingsSecret()
    {
        var protector = GetDataProtector();

        string dbPassword = protector.Protect("DbPassword", ReadPasswordFromConsole());
        ... // other secrets
        string json = ...;  // Serialize encrypted secrets to JSON
        var path = ConfigFileFullPath;
        File.WriteAllText(path, json);
        Console.WriteLine($"Writing app settings secret to '${path}' completed successfully.");
    }

    private static string CurrentDirectory
    {
        get { return Directory.GetParent(typeof(Program).Assembly.Location).FullName; }
    }

    private static string ConfigFileFullPath
    {
        get { return Path.Combine(CurrentDirectory, SECRET_CONFIG_FILE_NAME); }
    }
}
  1. In Startup.cs, read and decrypt the secret:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    ...
    if (env.IsProduction())
    {
        var protector = Program.GetDataProtector();
        var builder = new SqlConnectionStringBuilder();
        builder.Password = protector.Unprotect(configuration["DbPassword"]);
        ...
    }
}

BTW, appsettings.production.json or environment variable is really not an option. Secrets, as its name suggests, should never be stored in plain text.

like image 54
Weifen Luo Avatar answered Sep 30 '22 12:09

Weifen Luo


As they state, user secrets is only for development (to avoid commiting credentials accidentally into the SCM) and not intended for production. You should use one connection string per database, i.e. ConnectionStrings:CmsDatabaseProduction,ConnectionStrings:CmsDatabaseDevelopment, etc.

Or use docker containers (when you're not using Azure App Service), then you can set it on per container basis.

Alternatively you can also use environment based appsetting files. appsettings.production.json, but they must not be included in the source control management (Git, CSV, TFS)!

In the startup just do:

    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)
            .AddEnvironmentVariables();
        Configuration = builder.Build();
    }

This way, you can load specific stuff from the appsettings.production.json and can still override it via environment variable.

like image 35
Tseng Avatar answered Sep 30 '22 12:09

Tseng


If your application is hosted on AWS, this solution will probably be the simplest. It relies on the SecretConfiguration.AwsKms NuGet package.

Secrets are stored in a separate configuration file in encrypted form. The secrets are then decrypted at runtime using AWS Key Management Service. This way you can version your secrets along with your application's source code, while avoiding storing secrets in clear text.

Concretely, it's a configuration provider that integrates with the Microsoft.Extensions.Configuration stack.

Here are the steps:

1. Create a KMS key

Use the AWS console or CLI to create a KMS key (symmetric encryption). Make sure your developers have the permission to encrypt but not decrypt, and that only the role used to run your application has decrypt permissions.

2. Encrypt your secrets

Use the AWS CLI to encrypt your secrets. The command to use is the following (replace the key-id with the key ID of your KMS key):

aws kms encrypt --cli-binary-format raw-in-base64-out --key-id "11111111-0000-0000-0000-000000000000" --plaintext "SECRET_TO_ENCRYPT"

The output will contain the ciphertext:

{
    "CiphertextBlob": "AQICAHhDR/VQh6Ap...rfyKsKCG2h6WVK8=",
    "KeyId": "arn:aws:kms:eu-west-1:123456789:key/11111111-0000-0000-0000-000000000000",
    "EncryptionAlgorithm": "SYMMETRIC_DEFAULT"
}

Repeat this step for all the secrets you have in your application.

3. Create a separate configuration file for secrets

This file looks like a normal configuration file you would have in an ASP.NET Core application, except the string values are encrypted. For example, it may looks like:

{
  "Database": {
    "Password": "AQICAHhDR/VQh6Ap...rfyKsKCG2h6WVK8="
  },
  "Redis": {
    "Password": "AQICAHhDR/VQh6Ap...47iiHg/XifWcxvQ="
  }
}

You can also have one file per environment if you like (e.g. secrets.Staging.json and secrets.Production.json).

4. Register the configuration at startup

In your application startup, where configuration sources are configured, add this new configuration source:

string keyId = "arn:aws:kms:eu-west-1:123456789:key/11111111-0000-0000-0000-000000000000";

builder.Configuration.AddAwsKmsEncryptedConfiguration(
    new AmazonKeyManagementServiceClient(),
    keyId,
    encryptedSource => encryptedSource
        .SetBasePath(builder.Environment.ContentRootPath)
        .AddJsonFile($"secrets.{builder.Environment.EnvironmentName}.json"));

This will have the effect of transparently decrypting the secrets at runtime, and merging the key/value configuration pairs with the rest of the configuration sources already configured.

5. Access your secrets the same way you normally access configuration settings

Now you can use the configuration just like you are used to doing with IConfiguration:

IConfiguration configuration;

// Comes from your regular configuration file
string databaseLogin = configuration["Database:Login"];
// Comes from the encrypted configuration file
string databasePassword = configuration["Database:Password"];
like image 30
Flavien Avatar answered Sep 30 '22 11:09

Flavien