Storing production secrets in ASP.NET Core
In addition to use Azure App Service or docker containers, you can also securely store your app secrets in production using IDataProtector
:
- App secrets are entered with running a -config switch of the application, for example:
dotnet helloworld -config
; InProgram.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); }
}
}
- 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.
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"];