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

feat: add dapr secret provider #412

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
193f8f8
feat: add dapr secret provider
stijnmoreels Jul 5, 2023
85baf0a
pr-fix: unit tests
stijnmoreels Jul 5, 2023
4a619d1
Merge branch 'main' into feature/dapr-secret-provider
stijnmoreels Jul 5, 2023
68a69be
pr-fix: remove `/templates` path in `run-integration-tests.yml`
stijnmoreels Jul 5, 2023
7a50705
pr-fix: use `dapr` directly in Linux environment
stijnmoreels Jul 5, 2023
2089256
pr-style: update w/ correct exception messages
stijnmoreels Jul 5, 2023
f639949
pr-fix: use cross-platform dapr installation
stijnmoreels Jul 6, 2023
c05179f
pr-remove: out-of-context unit test
stijnmoreels Jul 6, 2023
dcad110
Merge branch 'feature/dapr-secret-provider' of https://github.com/sti…
stijnmoreels Jul 6, 2023
09d6cbb
pr-fix: update with correct file name in exception message
stijnmoreels Jul 6, 2023
d8ffedb
pr-fix: add exception logging
stijnmoreels Jul 6, 2023
03f9ca1
pr-fix: increase time-out
stijnmoreels Jul 6, 2023
6539ed3
pr-fix: correct with dapr logging
stijnmoreels Jul 6, 2023
9ebb6ae
pr-fix: update w/o app-logging
stijnmoreels Jul 13, 2023
b1a1d51
Update download-dapr.yml
stijnmoreels Jul 13, 2023
944f3dd
pr-fix: use correct unauthorized service principal
stijnmoreels Jul 13, 2023
a264c0f
Merge branch 'feature/dapr-secret-provider' of https://github.com/sti…
stijnmoreels Jul 13, 2023
013f10b
Update docs/preview/03-Features/secret-store/provider/dapr-secret-sto…
stijnmoreels Jul 13, 2023
e4c9174
Update docs/preview/03-Features/secret-store/provider/dapr-secret-sto…
stijnmoreels Jul 13, 2023
013009f
Merge branch 'main' into feature/dapr-secret-provider
stijnmoreels Oct 2, 2023
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
5 changes: 0 additions & 5 deletions build/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,6 @@ stages:
inputs:
artifact: 'Build'
path: '$(Build.SourcesDirectory)'
- template: 'templates/download-hashicorp-vault.yml'
parameters:
targetFolder: '$(Build.SourcesDirectory)'
version: $(HashiCorp.Vault.Version)
vaultBinVariableName: 'Arcus.HashiCorp.VaultBin'
- template: templates/run-integration-tests.yml
parameters:
dockerProjectName: '$(Project).Tests.Runtimes.AzureFunctions'
Expand Down
5 changes: 0 additions & 5 deletions build/nuget-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,6 @@ stages:
inputs:
artifact: 'Build'
path: '$(Build.SourcesDirectory)'
- template: 'templates/download-hashicorp-vault.yml'
parameters:
targetFolder: '$(Build.SourcesDirectory)'
version: $(HashiCorp.Vault.Version)
vaultBinVariableName: 'Arcus.HashiCorp.VaultBin'
- template: templates/run-integration-tests.yml
parameters:
dockerProjectName: '$(Project).Tests.Runtimes.AzureFunctions'
Expand Down
16 changes: 16 additions & 0 deletions build/templates/download-dapr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
parameters:
- name: targetFolder
type: string
default: '$(Build.SourcesDirectory)'
- name: daprBinVariableName
type: string
default: 'Arcus.Dapr.DaprBin'

steps:
- bash: |
wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash
dapr init
dapr -h
echo "##vso[task.setvariable variable=Arcus.Dapr.DaprBin]dapr"
workingDirectory: ${{ parameters.targetFolder }}
displayName: 'Download Dapr'
11 changes: 10 additions & 1 deletion build/templates/run-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ steps:
imageName: '${{ parameters.dockerProjectName }}:$(Build.BuildId)'
containerName: '${{ parameters.dockerProjectName }}'
ports: '$(Arcus.AzureFunctions.HttpPort):80'
- template: 'download-dapr.yml'
parameters:
targetFolder: '$(Build.SourcesDirectory)'
daprBinVariableName: 'Arcus.Dapr.DaprBin'
- template: 'download-hashicorp-vault.yml'
parameters:
targetFolder: '$(Build.SourcesDirectory)'
version: $(HashiCorp.Vault.Version)
vaultBinVariableName: 'Arcus.HashiCorp.VaultBin'
- template: test/run-integration-tests.yml@templates
parameters:
dotnetSdkVersion: '$(DotNet.Sdk.Version)'
Expand All @@ -41,4 +50,4 @@ steps:
docker logs ${{ parameters.dockerProjectName }}
failOnStderr: true
displayName: Show ${{ parameters.dockerProjectName }} logs
condition: always()
condition: always()
154 changes: 154 additions & 0 deletions docs/preview/03-Features/secret-store/provider/dapr-secret-store.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
---
title: "Dapr secret provider"
layout: default
---

# Dapr secret provider
Dapr secret provider brings secrets from the Dapr secret store to your application. Dapr is commonly used in Kubernetes environments where there is usually not the same network capabilities as other application environments.
By using this secret provider, you still benefit from all the Arcus secret store features, while still using Dapr as your external secret source.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dapr is not really a secret provider. It has the capabilities to connect to a secret provider and ads a layer of abstraction.
People that are looking for using Dapr will probably understand this, but I'm not sure on how we can improve this text

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should change it to Dapr secret management brings external secrets to your application.? As they talk about 'secret management' in their docs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the suggestion here, @fgheysels better?


⛔ Does not support [synchronous secret retrieval](../../secrets/general.md).

## Installation
Using the Dapr secrets building block with Arcus requires the following package:

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

## Configuration
After installing the package, the extensions methods for using the Dapr components becomes available when building the secret store.

```csharp
using Microsoft.Extensions.Hosting;

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) =>
{
// Adding the Dapr secret provider with the built-in overloads.
builder.AddDaprSecretStore(
// Name of the secret store where Dapr gets its secrets.
secretStore: "mycustomsecretstore",
// Following defaults can be overridden:
configureOptions: options =>
{
// The URI endpoint to use for gRPC calls to the Dapr runtime.
// The default value will be http://127.0.0.1:DAPR_GRPC_PORT where DAPR_GRPC_PORT represents the value of the DAPR_GRPC_PORT environment variable.
options.GrpcEndpoint = "http://127.0.0.1:5001/";

// The URI endpoint to use for HTTP calls to the Dapr runtime.
// The default value will be http://127.0.0.1:DAPR_HTTP_PORT where DAPR_HTTP_PORT represents the value of the DAPR_HTTP_PORT environment variable.
options.HttpEndpoint = "http://127.0.0.1:5002";

// The API token on every request to the Dapr runtime (added to the request's headers).
options.DaprApiToken = "my-api-key";

// Tracking the Dapr secret store dependency which works well together with Application Insights (default: `false`).
// See https://observability.arcus-azure.net/features/writing-different-telemetry-types#measuring-custom-dependencies for more information.
options.TrackDependency = true;

// Additional metadata entry which will be sent to the Dapr secret store on every request.
options.AddMetadata("my-dapr-key", "my-dapr-value");
});
});
}
}
```

### Custom implementation
We allow custom implementations of the Dapr secret provider.
This can come in handy when you want to perform additional actions during the secret retrieval.

**Example**
In this example we'll create a custom implementation for the local Dapr secret store that allows multi-valued secrets.
First, we'll implement the `DaprSecretProvider`:

```csharp
using Arcus.Security.Providers.Dapr;

public class MultiValuedLocalDaprSecretProvider : DaprSecretProvider
{
public MultiValuedLocalDaprSecretProvider(
string secretStore,
DaprSecretProviderOptions options,
ILogger<DaprSecretProvider> logger) : base(secretStore, options, logger)
{
}
}
```

👀 Notice that we require to take in the name of the Dapr secret store and the additional user-defined options which can be configured during the registration of the secret provider.

To control how Dapr secrets be retrieved, we need to implement the `DetermineDaprSecretName` method which takes in the secret name like it comes into the secret provider, and implement the multi-valued implementation:

```csharp
using Arcus.Security.Providers.Dapr;

public class MultiValuedLocalDaprSecretProvider : DaprSecretProvider
{
// Constructor truncated...

/// <summary>
/// Determine the Dapr secret key and section based on the user passed-in <paramref name="secretName"/>.
/// </summary>
/// <remarks>
/// The key of the secret in the Dapr secret store can be the same as the section for single-valued Dapr secrets, but is different in multi-valued Dapr secrets.
/// Therefore, make sure to split the <paramref name="secretName"/> into the required (key, section) pair for your use-case.
/// </remarks>
/// <param name="secretName">The user passed-in secret which gets translated to a Dapr secret key and section.</param>
protected override (string daprSecretKey, string daprSecretSection) DetermineDaprSecretName(string secretName)
{
const string nestedSeparator = ":";

string[] subKeys = secretName.Split(nestedSeparator, StringSplitOptions.RemoveEmptyEntries);
if (subKeys.Length >= 2)
{
string remaining = string.Join(nestedSeparator, subKeys.Skip(1));
return (subKeys[0], remaining);
}

return (secretName, secretName);
}
}
```

> 💡 Dapr allows for multi-valued secrets for the local Dapr secret store. This means that while single-valued secrets have the same 'key' as 'section' in the returned dictionary, multi-valued secrets are retrieved differently. For more information on the Dapr .NET SDK, see [their official documentation](https://docs.dapr.io/developing-applications/sdks/dotnet/).

Such a custom implementation can easily be registered with a dedicated extension on the secret store:

```csharp
using Microsoft.Extensions.Hosting;

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

public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args).ConfigureSecretStore((config, context, stores) =>
{
stores.AddDaprSecretStore(
(IServiceProvider provider, DaprSecretProviderOptions options) =>
{
var logger = provider.GetService<ILogger<DaprSecretProvider>>();
return new MultiValuedLocalDaprSecretProvider("mycustomsecretstore", options, logger);
},
(DaprSecretProviderOptions options) =>
{
// Configure additional options which can be passed in the implementation factory function of the custom implementation.
});
});
}
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Authors>Arcus</Authors>
<Description>Provides support for Dapr Secrets with Arcus Secret Store</Description>
<Copyright>Copyright (c) Arcus</Copyright>
<PackageProjectUrl>https://security.arcus-azure.net/</PackageProjectUrl>
<RepositoryUrl>https://github.com/arcus-azure/arcus.security</RepositoryUrl>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageIcon>icon.png</PackageIcon>
<RepositoryType>Git</RepositoryType>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>Kubernetes;Secrets;Dapr</PackageTags>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
<None Include="..\..\LICENSE" Pack="true" PackagePath="\" />
<None Include="..\..\docs\static\img\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Dapr.Client" Version="1.10.0" />
</ItemGroup>

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

</Project>
137 changes: 137 additions & 0 deletions src/Arcus.Security.Providers.Dapr/DaprSecretProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Arcus.Observability.Telemetry.Core;
using Arcus.Security.Core;
using Dapr.Client;
using GuardNet;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace Arcus.Security.Providers.Dapr
{
/// <summary>
/// Represents an <see cref="ISecretProvider"/> retrieving secrets from the Dapr secret store.
/// </summary>
public class DaprSecretProvider : ISecretProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="DaprSecretProvider" /> class.
/// </summary>
/// <param name="secretStore">The name of the Dapr secret store from which the secrets should be retrieved from.</param>
/// <param name="options">The optional set of options to manipulate the basic behavior of how the secrets should be retrieved.</param>
/// <param name="logger">The logger instance to write diagnostic trace messages during the retrieval of the Dapr secrets.</param>
/// <exception cref="ArgumentException">Thrown when the <paramref name="secretStore"/> is blank.</exception>
public DaprSecretProvider(string secretStore, DaprSecretProviderOptions options, ILogger<DaprSecretProvider> logger)
{
Guard.NotNullOrWhitespace(secretStore, nameof(secretStore));

SecretStore = secretStore;
Options = options ?? new DaprSecretProviderOptions();
Logger = logger ?? NullLogger<DaprSecretProvider>.Instance;
}

/// <summary>
/// Gets the name of the Dapr secret store for which this secret provider is configured.
/// </summary>
protected string SecretStore { get; }

/// <summary>
/// Gets the optional set of configured options to manipulated the basic behavior of how the secrets should be retrieved.
/// </summary>
/// <remarks>
/// Options set when configuring this secret provider in the secret store.
/// </remarks>
protected DaprSecretProviderOptions Options { get; }


/// <summary>
/// Gets the logger instance to write diagnostic trace messages during the Dapr secret retrieval.
/// </summary>
protected ILogger Logger { get; }

/// <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));

string secretValue = await GetRawSecretAsync(secretName);
return new Secret(secretValue);
}

/// <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 async Task<string> GetRawSecretAsync(string secretName)
{
Guard.NotNullOrWhitespace(secretName, nameof(secretName));

Logger.LogTrace("Getting a secret '{SecretName}' from Dapr secret store '{StoreName}'...", secretName, SecretStore);

(string daprSecretName, string daprSecretSection) = DetermineDaprSecretName(secretName);
string secretValue = await GetDaprSecretValueAsync(daprSecretName, daprSecretSection);

Logger.LogTrace("Got secret '{SecretName}' from from Dapr secret store '{StoreName}'", secretName, SecretStore);
return secretValue;
}

/// <summary>
/// Determine the Dapr secret key and section based on the user passed-in <paramref name="secretName"/>.
/// </summary>
/// <remarks>
/// The key of the secret in the Dapr secret store can be the same as the section for single-valued Dapr secrets, but is different in multi-valued Dapr secrets.
/// Therefore, make sure to split the <paramref name="secretName"/> into the required (key, section) pair for your use-case.
/// </remarks>
/// <param name="secretName">The user passed-in secret which gets translated to a Dapr secret key and section.</param>
protected virtual (string daprSecretKey, string daprSecretSection) DetermineDaprSecretName(string secretName)
{
return (secretName, secretName);
}

private async Task<string> GetDaprSecretValueAsync(string daprSecretName, string daprSecretSection)
{
Guard.NotNullOrWhitespace(daprSecretName, nameof(daprSecretName));
Guard.NotNullOrWhitespace(daprSecretSection, nameof(daprSecretSection));

using var measurement = DurationMeasurement.Start();
bool isSuccessful = false;

try
{
using DaprClient client = Options.CreateClient();
Dictionary<string, string> daprSecrets = await client.GetSecretAsync(SecretStore, daprSecretName);

if (!daprSecrets.TryGetValue(daprSecretSection, out string secretValue))
{
throw new SecretNotFoundException(daprSecretSection);
}

isSuccessful = true;
return secretValue;
}
finally
{
if (Options.TrackDependency)
{
Logger.LogDependency("Dapr secret store", daprSecretName, isSuccessful, measurement, new Dictionary<string, object>
{
["SecretStore"] = SecretStore,
["SecretSection"] = daprSecretSection
});
}
}
}
}
}
Loading
Loading