Introducing secret store
As an alternative to the usage of placing secrets into Microsoft's IConfiguration instance in your application, Arcus Security provides a alternative concept called 'secret store'.
We provide an approach similar to how IConfiguration is built, but with a focus on secrets. You can register one or more 'secret providers' where you want your application secrets to be retrieved from.
โก Caching
We provide caching so the secret providers will not be called upon every secret retrieval. This helps you avoiding hitting service limitations.
๐ Plug & play
We support using multiple secret providers and combinations of secret providers so that with a single secret retrieval, you can query multiple secret providers (also multiple Azure Key Vaults).
๐ก๏ธ Security by design
Vulnerabilities are introduced when secrets are seen as data (logs or when expired secrets don't get transient handling upon retrieval). The secret store provides a single place to retrieve secrets instead of scattering the integration across the application.
๐งฉ Extensibility
Arcus secret store is highly extensible and can be extended with your own custom secret providers.
Register the secret storeโ
The secret store is configured during the initial application build-up with one ore more secret providers:
using Arcus.Security;
var builder = Host.CreateDefaultBuilder(args);
// #1 Via the host builder.
builder.ConfigureSecretStore((config, store) =>
{
store.AddEnvironmentVariables(...);
});
builder.ConfigureServices(services =>
{
// #2 Via the services
services.AddSecretStore(store =>
{
store.AddAzureKeyVault(...);
});
});
The secret store is directly available via the ISecretStore interface which provides multiple secret retrieval options. Upon retrieval, the previously registered secret providers are one-by-one (in the order as they are registered) queried. A failure is returned when no secret is found.
using Arcus.Security;
public class MySqlService
{
private readonly ISecretStore _store;
public MySqlService(ISecretStore store)
{
_store = store;
}
public async Task ConnectAsync()
{
// #1 synchronously
SecretResult result = _store.GetSecret("Contoso_Sql_ConnectionString");
string secretValue = result.Value;
// #2 asynchronously
SecretResult result = await _store.GetSecretAsync("Contoso_Sql_ConnectionString");
if (!result.IsSuccess)
{
SecretFailure failure = result.Failure; // The type of failure that occurred.
Exception cause = result.FailureCause; // When the failure was caused by an exception.
}
}
}
Customization the secret storeโ
โก Secret caching
To limit possible throttling during external secrets retrieval, the secret store can be customized to cache secrets for a period of time.
builder.ConfigureServices(services =>
{
services.AddSecretStore(store =>
{
store.UseCaching(TimeSpan.FromMinutes(1));
})
});
All secrets are cached by default, but this can be ignored on a by-secret basis.
using Arcus.Security;
ISecretStore store = ...
SecretResult result = store.GetSecret("Contoso_Sql_ConnectionString", options =>
{
options.UseCache = false;
});
Invalidating cached secrets when secrets are externally rotated also happens on the secret store itself.
using Arcus.Security;
ISecretStore store = ...
await store.Cache.InvalidateSecretAsync("Contoso_Sql_ConnectionString");
๐ Secret mapping
In some cases, you want the application secret name to be something different than the actual name that gets stored externally. Environment variables, for example might be always upper case, but this knowledge might not be something that you want to expose directly to your application -- especially if you want to override certain secrets during development.
All available secret secret providers can be given a custom secret name mapping upon registration. This mapping happens just before actually retrieving the secret (and independently from secret caching).
builder.ConfigureServices(services =>
{
services.AddSecretStore(store =>
{
store.AddEnvironmentVariables(..., options =>
{
options.MapSecretName((string name) => name.ToUpper());
});
})
});
๐ท๏ธ Naming secret providers
All available secret providers can be given a custom name upon registration to identify themselves.
builder.ConfigureServices(services =>
{
services.AddAzureKeyVault(..., options =>
{
options.ProviderName = "AdminSecrets";
});
});
This name can later be used to retrieve back a secret provider in the secret store. This can be useful when you want to run a specific action that is only available on a single secret provider (ex. secret versions).
using Arcus.Security;
ISecretStore store = ...
var provider = store.GetProvider<KeyVaultSecretProvider>("AdminSecrets");
Multiple secret providers can be registered with the same name. Upon retrieval, the matching secret providers are returned as a 'subset'.
using Arcus.Security;
ISecretProvider provider = store.GetProvider("AdminSecrets");
Built-in secret providersโ
The following secret providers are always available in your application, no matter which secret provider NuGet package you install.
- App configuration
- Environment variables
Include the application configuration (IConfiguration) as a secret provider in the secret store. This is useful for development, but SHOULD NOT be used in production.
builder.ConfigureSecretStore((IConfiguration config, store) =>
{
#if DEBUG
store.AddConfiguration(config);
#endif
});
Include environment variables as a secret provider in the secret store. Some additional options (besides the default ones) are available to pinpoint which variables should be included as application secrets.
builder.ConfigureSecretStore((_, store) =>
{
// Uses the environment variables associated with the current process.
store.AddEnvironmentVariables();
store.AddEnvironmentVariables(options =>
{
// Uses only the environment variables starting with a prefix.
// Upon secret retrieval, the prefix should not be included in the secret name.
// (default: none)
options.Prefix = "ARCUS_";
// Uses only the environment variables from a given target.
// (default: Process)
options.Target = EnvironmentVariableTarget.Machine;
});
});