Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a SecretProvider that can be used for Docker Secrets #171

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
80123b9
Added empty Arcus.Security.Providers.KeyPerFile project
fgheysels Jul 8, 2020
26f5e1c
Add KeyPerFileSecretProvider
fgheysels Jul 29, 2020
d8dc3f5
Make sure that the secrets are loaded
fgheysels Jul 29, 2020
22e3e0a
Target .net standard 2.0 and .NET core 3.1
fgheysels Aug 27, 2020
0f0e822
Add integration tests for KeyPerFile secret provider
fgheysels Aug 27, 2020
f7f23c2
Add documentation for KeyPerFile SecretProvider
fgheysels Aug 27, 2020
b78e1e7
KeyPerFile secretprovider: add test for hierarchical keys
fgheysels Aug 27, 2020
9cfd241
Merge branch 'master' into frgh/feature/149_dockersecrets
fgheysels Aug 27, 2020
9c953d3
Update docs/features/secret-store/provider/key-per-file.md
fgheysels Aug 28, 2020
34881bd
Update docs/features/secret-store/provider/key-per-file.md
fgheysels Aug 28, 2020
779cd62
Update docs/features/secret-store/provider/key-per-file.md
fgheysels Aug 28, 2020
4b446f1
Update docs/features/secret-store/provider/key-per-file.md
fgheysels Aug 28, 2020
f85107f
Remove AspNetCore dependency
fgheysels Sep 22, 2020
732b73c
Merge branch 'frgh/feature/149_dockersecrets' of https://github.com/f…
fgheysels Sep 22, 2020
0e51f92
Rename KeyPerFile secretprovider to dockersecretssecretprovider
fgheysels Sep 26, 2020
3b36aae
Merge with master
fgheysels Sep 26, 2020
bd0c1f1
Fix merge errors
fgheysels Sep 26, 2020
9d1480c
Rename refactor (dockersecrets)
fgheysels Sep 26, 2020
4c574b7
Added nuget metadata
fgheysels Sep 26, 2020
4775d6e
Fix remarks from code review
fgheysels Sep 26, 2020
9e57765
Move to DockerSecrets folder
fgheysels Sep 26, 2020
5d30d0e
Fix build error
fgheysels Sep 26, 2020
baadf2d
Moved docker secrets documentation to the preview folder
fgheysels Sep 26, 2020
13bacde
Updated documentation to reflect the name-refactoring
fgheysels Sep 26, 2020
7935b08
Code style
fgheysels Sep 26, 2020
680f505
Update docs/preview/index.md
fgheysels Sep 28, 2020
c8225d8
Remove the optional 'optional' parameter from AddDockerSecrets
fgheysels Sep 28, 2020
5ea8095
Code review remarks: provider should be loaded from builder
fgheysels Sep 28, 2020
069eb8d
Change constructor of DockerSecretsProvider
fgheysels Oct 11, 2020
8c55d12
Added mutateSecretName constructor argument
fgheysels Oct 11, 2020
eecf9ac
Added some additional tests
fgheysels Oct 11, 2020
0882921
Update docs/preview/features/secret-store/provider/docker-secrets.md
fgheysels Oct 12, 2020
a34932b
Update src/Arcus.Security.Providers.DockerSecrets/DockerSecretsSecret…
fgheysels Oct 12, 2020
4fefa78
Throw when the configured secrets directory does not exist
fgheysels Oct 12, 2020
37f40b8
Extended integration-test case
fgheysels Oct 12, 2020
324c6f2
Merge branch 'frgh/feature/149_dockersecrets' of https://github.com/f…
fgheysels Oct 12, 2020
7503c38
Merge branch 'master' into frgh/feature/149_dockersecrets
fgheysels Oct 12, 2020
63080ec
Update docs/preview/features/secret-store/provider/docker-secrets.md
fgheysels Oct 14, 2020
8d458db
Throw ArgumentException when docker secrets path is not rooted
fgheysels Oct 15, 2020
55acae1
Merge branch 'frgh/feature/149_dockersecrets' of https://github.com/f…
fgheysels Oct 15, 2020
1389cb7
Update docs/preview/features/secret-store/provider/docker-secrets.md
fgheysels Oct 16, 2020
7cdb48f
Update docs/preview/features/secret-store/provider/docker-secrets.md
fgheysels Oct 16, 2020
f06dc55
SecretStore providers added in alphabetical order
fgheysels Oct 16, 2020
25f38a7
Update docs/preview/features/secret-store/provider/docker-secrets.md
fgheysels Oct 16, 2020
cdbac41
Merge branch 'master' into frgh/feature/149_dockersecrets
fgheysels Oct 16, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions docs/preview/features/secret-store/provider/docker-secrets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
title: "Docker Secrets secret provider"
layout: default
---

# Docker Secrets secret provider
This provider allows you to work with Docker secrets. When using Docker secrets in Docker Swarm, the secrets are injected in the Docker container as files.
The Docker secrets secret provider provides access to those secrets via the secret store.

This secret provider offers functionality which is equivalent to the _KeyPerFile_ Configuration Provider, but instead of adding the secrets to the Configuration, this secret provider allows access to the Docker Secrets via the _ISecretProvider_ interface.

## Installation
Adding secrets from the User Secrets manager into the secret store requires following package:

```shell
PM > Install-Package Arcus.Security.Providers.DockerSecrets
```

## Configuration
After installing the package, the addtional extensions becomes available when building the secret store.

```csharp
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.ConfigureSecretStore((context, config, builder) =>
{
// Adds the secrets that exist in the "/run/secrets" directory to the ISecretStore
// Docker secrets are by default mounted into the /run/secrets directory
// when using Linux containers on Docker Swarm.
builder.AddDockerSecrets(directoryPath: "/run/secrets");
})
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());
}
}
```

## Retrieving secrets

Suppose you have the following docker-compose file:

```yaml
version: '3.8'
services:
person-api:
image: person-api:latest
ports:
- 5555:80
secrets:
- ConnectionStrings__PersonDatabase

secrets:
ConnectionStrings__PersonDatabase:
external: true
```

After adding the Docker Secrets secret provider to the secret store, the Docker secrets can simply be retrieved by calling the appropriate methods on the `ISecretProvider`:

```csharp
public class PersonController
{
private readonly ISecretProvider _secrets;

public PersonController(ISecretProvider secrets)
{
_secrets = secrets;
}

[HttpGet]
public async Task GetPerson(Guid personId)
{
string connectionstring = await _secrets.GetRawSecretAsync("ConnectionStrings:PersonDatabase")

using (var connection = new SqlDbConnection(connectionstring))
{
var person = new PersonRepository(connection).GetPersonById(personId);
return Ok(new { Id = person.Id, Name = person.Name });
}
}
}
```

[&larr; back](/)
3 changes: 2 additions & 1 deletion docs/preview/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ PM > Install-Package Arcus.Security.Providers.AzureKeyVault
- Providers
- [Azure Key Vault](features/secret-store/provider/key-vault)
- [Configuration](features/secret-store/provider/configuration)
- [Environment variables](features/secret-store/provider/environment-variables)
- [Docker secrets](features/secret-store/provider/docker-secrets)
tomkerkhove marked this conversation as resolved.
Show resolved Hide resolved
- [Environment variables](features/secret-store/provider/environment-variables)
- [HashiCorp Vault](features/secret-store/provider/hashicorp-vault)
- [User Secrets](features/secret-store/provider/user-secrets)
- [Creating your own secret provider](features/secret-store/create-new-secret-provider)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<Authors>Arcus</Authors>
<Description>Provides support for Docker Secrets</Description>
<Copyright>Copyright (c) Arcus</Copyright>
<PackageLicenseUrl>https://github.com/arcus-azure/arcus.security/blob/master/LICENSE</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/arcus-azure/arcus.security</PackageProjectUrl>
<PackageIconUrl>https://raw.githubusercontent.com/arcus-azure/arcus/master/media/arcus.png</PackageIconUrl>
<RepositoryUrl>https://github.com/arcus-azure/arcus.security</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<PackageTags>Docker;Docker secrets;Docker Secrets Manager;Key Per File;Security</PackageTags>
<AssemblyName>Arcus.Security.Providers.DockerSecrets</AssemblyName>
<RootNamespace>Arcus.Security.Providers.DockerSecrets</RootNamespace>
<PackageId>Arcus.Security.Providers.DockerSecrets</PackageId>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Arcus.Security.Core\Arcus.Security.Core.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.KeyPerFile" Version="3.1.7" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using Arcus.Security.Core;
using GuardNet;
using Microsoft.Extensions.Configuration.KeyPerFile;
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.FileProviders;

namespace Arcus.Security.Providers.DockerSecrets
{
/// <summary>
/// Represents an <see cref="ISecretProvider" /> that provides access to the Docker secrets mounted into the Docker container as files.
/// </summary>
public class DockerSecretsSecretProvider : ISecretProvider
{
private readonly KeyPerFileConfigurationProvider _provider;

/// <summary>
/// Initializes a new instance of the <see cref="DockerSecretsSecretProvider"/> class.
/// </summary>
/// <param name="secretsDirectoryPath">The path inside the docker container where the secrets are located.</param>
/// <exception cref="ArgumentException">Thrown when the <paramref name="secretsDirectoryPath"/> is blank.</exception>
public DockerSecretsSecretProvider(string secretsDirectoryPath)
{
Guard.NotNullOrWhitespace(secretsDirectoryPath, nameof(secretsDirectoryPath));

stijnmoreels marked this conversation as resolved.
Show resolved Hide resolved
if (!Path.IsPathRooted(secretsDirectoryPath))
{
throw new ArgumentException($"The {nameof(secretsDirectoryPath)} must be an absolute path", nameof(secretsDirectoryPath));
}

if (!Directory.Exists(secretsDirectoryPath))
{
throw new DirectoryNotFoundException($"The directory {secretsDirectoryPath} which is configured as secretsDirectoryPath does not exist.");
}

KeyPerFileConfigurationSource configuration = new KeyPerFileConfigurationSource
{
FileProvider = new PhysicalFileProvider(secretsDirectoryPath),
Optional = false
};

var provider = new KeyPerFileConfigurationProvider(configuration);
provider.Load();

_provider = provider;
}

/// <summary>
/// Retrieves the secret value, based on the given name
/// </summary>
/// <param name="secretName">The name of the secret key</param>
/// <returns>Returns the secret key.</returns>
/// <exception cref="ArgumentException">The <paramref name="secretName"/> must not be empty</exception>
/// <exception cref="ArgumentNullException">The <paramref name="secretName"/> must not be null</exception>
/// <exception cref="SecretNotFoundException">The secret was not found, using the given name</exception>
public Task<string> GetRawSecretAsync(string secretName)
{
Guard.NotNullOrWhitespace(secretName, nameof(secretName), "Requires a non-blank secret name to retrieve a Docker secret");

if (_provider.TryGet(secretName, out string value))
{
return Task.FromResult(value);
}

return Task.FromResult<string>(null);
}

/// <summary>
/// Retrieves the secret value, based on the given name
/// </summary>
/// <param name="secretName">The name of the secret key</param>
/// <returns>Returns a <see cref="Secret"/> that contains the secret key</returns>
/// <exception cref="ArgumentException">The <paramref name="secretName"/> must not be empty</exception>
/// <exception cref="ArgumentNullException">The <paramref name="secretName"/> must not be null</exception>
/// <exception cref="SecretNotFoundException">The secret was not found, using the given name</exception>
public async Task<Secret> GetSecretAsync(string secretName)
{
Guard.NotNullOrWhitespace(secretName, nameof(secretName), "Requires a non-blank secret name to retrieve a Docker secret");

string secretValue = await GetRawSecretAsync(secretName);
if (secretValue == null)
{
return null;
}

return new Secret(secretValue);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using Arcus.Security.Providers.DockerSecrets;
using GuardNet;
using Microsoft.Extensions.Configuration.KeyPerFile;
using Microsoft.Extensions.FileProviders;

// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.Hosting
{
/// <summary>
/// Extensions on the <see cref="SecretStoreBuilder" /> to easily provide access to Docker secrets in the secret store.
/// </summary>
public static class SecretStoreBuilderExtensions
{
/// <summary>
/// Adds Docker secrets (mounted as files in the Docker container) to the secret store.
/// </summary>
/// <param name="builder">The builder to add the Docker secrets provider to.</param>
/// <param name="directoryPath">The path inside the container where the Docker secrets are located.</param>
/// <param name="mutateSecretName">The optional function to mutate the secret name before looking it up.</param>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="builder"/> is <c>null</c></exception>
/// <exception cref="ArgumentException">Throw when the <paramref name="directoryPath"/> is blank</exception>
public static SecretStoreBuilder AddDockerSecrets(this SecretStoreBuilder builder, string directoryPath, Func<string, string> mutateSecretName = null)
stijnmoreels marked this conversation as resolved.
Show resolved Hide resolved
{
Guard.NotNull(builder, nameof(builder), "Requires a secret store builder to add the Docker secrets to");
Guard.NotNullOrWhitespace(directoryPath, nameof(directoryPath), "Requires a non-blank directory path to locate the Docker secrets");

return builder.AddProvider(new DockerSecretsSecretProvider(directoryPath), mutateSecretName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Arcus.Security.Providers.AzureKeyVault\Arcus.Security.Providers.AzureKeyVault.csproj" />
<ProjectReference Include="..\Arcus.Security.Providers.DockerSecrets\Arcus.Security.Providers.DockerSecrets.csproj" />
<ProjectReference Include="..\Arcus.Security.Providers.HashiCorp\Arcus.Security.Providers.HashiCorp.csproj" />
<ProjectReference Include="..\Arcus.Security.Providers.UserSecrets\Arcus.Security.Providers.UserSecrets.csproj" />
<ProjectReference Include="..\Arcus.Security.Tests.Core\Arcus.Security.Tests.Core.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Arcus.Security.Providers.DockerSecrets;
using System;
using System.IO;
using Xunit;

namespace Arcus.Security.Tests.Integration.DockerSecrets
{
public class DockerSecretsProviderTests
{
[Fact]
public void Instantiate_WithNonExistingSecretLocation_Throws()
{
Assert.Throws<DirectoryNotFoundException>(() => new DockerSecretsSecretProvider("/foo/bar"));
}

[Fact]
public void Instantiate_WithRelativePath_Throws()
{
Assert.Throws<ArgumentException>(() => new DockerSecretsSecretProvider("./foo"));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using Arcus.Security.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.IO;
using System.Threading.Tasks;
using Arcus.Security.Providers.DockerSecrets;
using Xunit;
using Xunit.Abstractions;

namespace Arcus.Security.Tests.Integration.DockerSecrets
{
public class SecretStoreBuilderExtensionTests : IntegrationTest, IDisposable
{
private readonly string _secretLocation = Path.Combine(Path.GetTempPath(), "dockersecretstests");

public SecretStoreBuilderExtensionTests(ITestOutputHelper testOutput) : base(testOutput)
{
Directory.CreateDirectory(_secretLocation);
}

[Fact]
public async Task AddDockerSecrets_WithPath_ResolvesSecret()
{
// Arrange
var expectedValue = Guid.NewGuid().ToString();
var secretKey = "MySuperSecret";
await SetSecretAsync(secretKey, expectedValue);

var hostBuilder = new HostBuilder();

// Act
hostBuilder.ConfigureSecretStore((config, stores) => stores.AddDockerSecrets(_secretLocation));

// Assert
IHost host = hostBuilder.Build();
var secretProvider = host.Services.GetRequiredService<ISecretProvider>();

string actualValue = await secretProvider.GetRawSecretAsync(secretKey);
Assert.Equal(expectedValue, actualValue);
}

[Fact]
public async Task DockerSecretsProvider_ReturnsNull_WhenSecretNotFound()
stijnmoreels marked this conversation as resolved.
Show resolved Hide resolved
{
var provider = new DockerSecretsSecretProvider(_secretLocation);
await SetSecretAsync("MyExistingSecret", "foo");

var secret = await provider.GetRawSecretAsync("MyNonExistingSecret");

Assert.Null(secret);
}

[Fact]
public async Task DockerSecrets_HierarchicalKeys_AreSupported()
{
// Arrange
var expectedValue = Guid.NewGuid().ToString();
var secretKey = "ConnectionStrings__PersonDb";
await SetSecretAsync(secretKey, expectedValue);

var hostBuilder = new HostBuilder();

// Act
hostBuilder.ConfigureSecretStore((config, stores) => stores.AddDockerSecrets(_secretLocation));

// Assert
IHost host = hostBuilder.Build();
var secretProvider = host.Services.GetRequiredService<ISecretProvider>();

string actualValue = await secretProvider.GetRawSecretAsync("ConnectionStrings:PersonDb");
Assert.Equal(expectedValue, actualValue);
}

private async Task SetSecretAsync(string secretKey, string secretValue)
{
await File.WriteAllTextAsync(Path.Combine(_secretLocation, secretKey), secretValue);
}

/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
Directory.Delete(_secretLocation, true);
}
}
}
Loading