Added secrets tokenisation with Docker Secrets compatibility:
* Resolve secrets from file topology where file name is key and trimmed file contents are value, compatible with Docker Secrets tmpfs mounting. * Support [@Key@] token pattern inside Environment Variable strings, allowing verbose connection strings to live outside declared secrets. * SecretsPath configurable via environment variable or appSettings, falling back to working directory for local development convenience. * Secrets processing occurs at startup before logging, with diagnostics written to STDOUT. * Tokenisation extracted into standalone library and included in Dockerfile * Documentation added covering token pattern and Docker Secrets compatibility.
This commit is contained in:
@@ -30,6 +30,7 @@ COPY ["Jube.TaskCancellation/Jube.TaskCancellation.csproj", "Jube.TaskCancellati
|
||||
COPY ["Jube.Tests/Jube.Tests.csproj", "Jube.Tests/"]
|
||||
COPY ["Jube.ResilientNpgsql/Jube.ResilientNpgsql.csproj", "Jube.ResilientNpgsql/"]
|
||||
COPY ["Jube.ResilientRedisConnection/Jube.ResilientRedisConnection.csproj", "Jube.ResilientRedisConnection/"]
|
||||
COPY ["Jube.Tokenisation/Jube.Tokenisation.csproj", "Jube.Tokenisation/"]
|
||||
COPY ["Jube.CLI/Jube.CLI.csproj", "Jube.CLI/"]
|
||||
|
||||
RUN dotnet restore "Jube.App/Jube.App.csproj"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Jube.DynamicEnvironment\Jube.DynamicEnvironment.csproj"/>
|
||||
<ProjectReference Include="..\Jube.Tokenisation\Jube.Tokenisation.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace Jube.DynamicEnvironment
|
||||
using log4net.Core;
|
||||
using log4net.Layout;
|
||||
using log4net.Repository.Hierarchy;
|
||||
using Tokenisation;
|
||||
|
||||
public class DynamicEnvironment
|
||||
{
|
||||
@@ -310,9 +311,22 @@ namespace Jube.DynamicEnvironment
|
||||
},
|
||||
{
|
||||
"RedisBackplane", "False"
|
||||
},
|
||||
{
|
||||
"SecretsPath", ""
|
||||
}
|
||||
};
|
||||
|
||||
var secretsPath = Environment.GetEnvironmentVariable("SecretsPath") ?? String.Empty;
|
||||
if (String.IsNullOrEmpty(secretsPath))
|
||||
{
|
||||
secretsPath = appSettings["SecretsPath"] ?? String.Empty;
|
||||
|
||||
Console.WriteLine(String.IsNullOrEmpty(secretsPath) ?
|
||||
"Environment Variable SecretsPath not available. Will resolve to working directory." :
|
||||
$"Environment Variable SecretsPath not available. Set to default prefix of {secretsPath}.");
|
||||
}
|
||||
|
||||
foreach (var environmentVariable in from DictionaryEntry environmentVariable in
|
||||
Environment.GetEnvironmentVariables()
|
||||
where appSettings.ContainsKey(Convert.ToString(environmentVariable.Key) ?? String.Empty)
|
||||
@@ -320,8 +334,45 @@ namespace Jube.DynamicEnvironment
|
||||
appSettings[environmentVariable.Key.ToString() ?? String.Empty]
|
||||
select environmentVariable)
|
||||
{
|
||||
appSettings[environmentVariable.Key.ToString() ?? String.Empty] =
|
||||
Convert.ToString(environmentVariable.Value);
|
||||
if (environmentVariable.Value == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var replaced = Convert.ToString(environmentVariable.Value);
|
||||
if (replaced != null)
|
||||
{
|
||||
foreach (var token in Tokenisation.ReturnTokens(replaced))
|
||||
{
|
||||
var path = Path.Combine(secretsPath, token);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Console.WriteLine($@"For environment variable {environmentVariable.Key} could not find {path} in container.");
|
||||
continue;
|
||||
}
|
||||
|
||||
string secret;
|
||||
try
|
||||
{
|
||||
secret = File.ReadAllText(path).Trim();
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Console.WriteLine($@"For environment variable {environmentVariable.Key} could not read {path}: {ex.Message}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (String.IsNullOrEmpty(secret))
|
||||
{
|
||||
Console.WriteLine($@"For environment variable {environmentVariable.Key} path {path} in container but it is empty.");
|
||||
continue;
|
||||
}
|
||||
|
||||
replaced = replaced.Replace($"[@{token}@]", secret);
|
||||
}
|
||||
|
||||
appSettings[environmentVariable.Key.ToString() ?? String.Empty] = replaced;
|
||||
}
|
||||
}
|
||||
|
||||
InstantiateLog4Net();
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Jube.Data\Jube.Data.csproj"/>
|
||||
<ProjectReference Include="..\Jube.Tokenisation\Jube.Tokenisation.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+1
-1
@@ -16,7 +16,7 @@ namespace Jube.Engine.EntityAnalysisModelInvoke.Models.Payload.EntityAnalysisMod
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Case;
|
||||
using Tokenisation;
|
||||
|
||||
public static class EntityAnalysisModelInstanceEntryPayloadExtensions
|
||||
{
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -11,7 +11,7 @@
|
||||
* see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Jube.Case
|
||||
namespace Jube.Tokenisation
|
||||
{
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -61,6 +61,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jube.ResilientNpgsql", "Jub
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jube.ResilientRedisConnection", "Jube.ResilientRedisConnection\Jube.ResilientRedisConnection.csproj", "{BB76862D-4122-42D6-9A47-34A32A5DDBC9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jube.Tokenisation", "Jube.Tokenisation\Jube.Tokenisation.csproj", "{D69B24BA-7DE7-42D4-8FBB-67CB0A179B8B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -183,6 +185,10 @@ Global
|
||||
{BB76862D-4122-42D6-9A47-34A32A5DDBC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BB76862D-4122-42D6-9A47-34A32A5DDBC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BB76862D-4122-42D6-9A47-34A32A5DDBC9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D69B24BA-7DE7-42D4-8FBB-67CB0A179B8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D69B24BA-7DE7-42D4-8FBB-67CB0A179B8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D69B24BA-7DE7-42D4-8FBB-67CB0A179B8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D69B24BA-7DE7-42D4-8FBB-67CB0A179B8B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
EndGlobalSection
|
||||
|
||||
@@ -105,7 +105,19 @@ following hardcoded Environment Variables, this would be to say defaults, are av
|
||||
| MaxInvokeControllerRequestBytes | 4000 | The maximum number of bytes that can be JSON parsed for the invocation pipeline, beyond which an exception will be thrown. |
|
||||
| PartialResponseMessageSerialisation | True | When true, only return elements in the response payload on the basis of ResponsePayload switch being true in varius model entities, otherwise return the same comprehensive payload as stored in the Archive table. Disabling PartialResponseMessageSerialisation is useful when developing configurations where testing needs total visibility of invocation context payload. |
|
||||
| UseRedisBackplane | True | In a load balanced environment the preference is not to use server affinity for the user interface, but this cases breakage in the Activation Watcher page. In the absence of server affinity Redis can be used as a backplane such that the connection does not need to be stateful. |
|
||||
| SecretsPath | null | The file system path to look up secrets, where the file name is the key and the trimmed contents of the file is the value. This has special compatibility with Docker Secrets. If not provided, defaults to the application's working directory. Secrets are only tokenised where the [@Key@] pattern exists inside the Environment Variable string. |
|
||||
|
||||
Environment Variables passed to Jube at startup will override the hardcoded defaults above. Environment Variables are
|
||||
not written to disk in any form, and their contents are available only in the Jube applications memory and operating
|
||||
system session.
|
||||
not written to disk in any form, and their contents are available only in the Jube application's memory and operating
|
||||
system session.
|
||||
|
||||
The Environment Variable string can be tokenised, and secrets can be swapped in from a file topology at a given path,
|
||||
where the file name is the key and the contents of the file is the value. This approach provides full compatibility with
|
||||
Docker Secrets, which mounts secrets as files using tmpfs (memory-mounted file system). For example, given the
|
||||
token [@RedisPassword@], a file called RedisPassword and the contents of that file being localhost, the Environment
|
||||
Variable may be passed as RedisConnectionString=[@RedisPassword@]. At startup the token is detected, extracted and
|
||||
replaced with the contents of the file RedisPassword mounted by Docker Secrets. Tokens that resolve are replaced in
|
||||
place; those that do not are left unchanged and an error is written to STDOUT. This approach ensures that only
|
||||
genuinely secret information is stored in Docker Secrets, with more verbose Environment Variable strings being safe to
|
||||
live outside declared secrets, allowing for clearer intent and greater maintainability. Secrets are processed at startup
|
||||
and before logging, with any errors written to STDOUT rather than the logging library.
|
||||
Reference in New Issue
Block a user