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.
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.
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.
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.
In addition to use Azure App Service or docker containers, you can also securely store your app secrets in production using IDataProtector
:
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); }
}
}
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.
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.
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"];
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With