Where I work, we are using Docker Swarm to host our containers. Since we don't like to commit sensitive data to our git repo (API Keys, passwords etc) we are storing that kind of data in something called Docker Secrets.

Short intro to Secrets

It basically allow us to retrieve sensitive configuration data at runtime. For each secret that's created in your swarm, Docker will mount a file and you can then access that file and read it's value from your container.
HashiCorp Vault, Azure Key Vault and CyberArk Password Vault are other similar services.

Dotnet core - Configuration

Since we are using dotnet core we handle the application configuration with the help of IConfiguration. In our console application we use the Generic Host to be able to bootstrap our application in a similar way to ASP.NET Core.

A typical example of using IConfiguration/IConfigurationBuilder looks something like this:

var host = new HostBuilder()
    .ConfigureHostConfiguration(configHost =>
    {
        configHost.AddJsonFile("appsettings.json", optional: true);
        configHost.AddEnvironmentVariables();
        configHost.AddCommandLine(args);
    })

Here we first read configuration from a file, appsettings.json, then from EnvironmentVariables and lastly from args that gets passed in when starting the application.

It would be nice to follow this pattern when we're reading the secrets in our swarm, right?

So let's get to work.

DockerSwarmSecretsConfigurationProvider

First, we need to implement the IConfigurationSource interface.

public class DockerSwarmSecretsConfigurationSource : IConfigurationSource
{
    private readonly string _secretsPath;
    private readonly Action<string> _handle;

    public DockerSwarmSecretsConfigurationSource(string secretsPath) : this(secretsPath, null) {}

    public DockerSwarmSecretsConfigurationSource(string secretsPath, Action<string> handle)
    {
        _secretsPath = secretsPath ?? throw new ArgumentNullException(nameof(secretsPath));
        _handle = handle;
    }

    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new DockerSwarmSecretsConfigurationProvider(_secretsPath, _handle);
    }
}

Then we need to create the configuration provider that is responsible for reading the secrets and adding them to the configuration.
Here we are inheriting from ConfigurationProvider so that we don't need to do a bunch of bootstrapping, the Load method is what's important here.

public class DockerSwarmSecretsConfigurationProvider : ConfigurationProvider
{
    public const string DefaultSecretsPath = "/run/secrets";
    private readonly string _secretsPath;
    private readonly Action<string> _handle;

    public DockerSwarmSecretsConfigurationProvider(string secretsPath) : this(secretsPath, null) { }

    public DockerSwarmSecretsConfigurationProvider(string secretsPath, Action<string> handle)
{
    _handle = handle ?? (filePath =>
    {
        var fileName = Path.GetFileName(filePath);
        if (!string.IsNullOrWhiteSpace(fileName))
        {
            var key = fileName.Replace("_", ":");
            var value = File.ReadAllText(filePath);

            Data.Add(key, value);
        }
    });

    _secretsPath = secretsPath ?? throw new ArgumentNullException(nameof(secretsPath));
}

    public override void Load()
    {
        if (Directory.Exists(_secretsPath))
        {
            foreach (var file in Directory.EnumerateFiles(_secretsPath))
            {
                _handle(file);
            }
        }
    }
}

Nothing fancy here, we check that the directory exists, if it exists, read all files in it and add to the Data dictionary that's provided from ConfigurationProvider.
Our secrets uses underscores in the filenames as a convention so we are replacing the underscores with colons instead since that's the format IConfiguration expects.

If you don't follow that convention, you can override the behaviour by passing in your own Action<string>.

The only thing left for us to do now is to create a couple of extension methods for the IConfigurationBuilder and we should be good to go.

public static class DockerSwarmSecretsConfigurator
{
    public static IConfigurationBuilder AddDockerSwarmSecrets(this IConfigurationBuilder configurationBuilder)
    {
        return AddDockerSwarmSecrets(configurationBuilder, DockerSwarmSecretsConfigurationProvider.DefaultSecretsPath);
    }

    public static IConfigurationBuilder AddDockerSwarmSecrets(this IConfigurationBuilder configurationBuilder, string secretsPath, Action<string> handle = null)
    {
        configurationBuilder.Add(new DockerSwarmSecretsConfigurationSource(secretsPath, handle));
        return configurationBuilder;
    }
}

And that's it. Now we can read secrets from our Swarm by just adding the AddDockerSwarmSecrets call like this:

var host = new HostBuilder()
    .ConfigureHostConfiguration(configHost =>
    {
        configHost.AddJsonFile("appsettings.json", optional: true);
        configHost.AddEnvironmentVariables();
        configHost.AddDockerSwarmSecrets();
        configHost.AddCommandLine(args);
    })

All code in this post can be found here.

Cover photo by Georg Wolf / Unsplash