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:
richard-churchman
2026-03-22 14:33:49 +01:00
parent 7c9919bb51
commit 5e2195e58d
9 changed files with 87 additions and 6 deletions
+1
View File
@@ -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"
+1
View File
@@ -8,6 +8,7 @@
<ItemGroup>
<ProjectReference Include="..\Jube.DynamicEnvironment\Jube.DynamicEnvironment.csproj"/>
<ProjectReference Include="..\Jube.Tokenisation\Jube.Tokenisation.csproj"/>
</ItemGroup>
<ItemGroup>
+53 -2
View File
@@ -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>
@@ -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;
+6
View File
@@ -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
+14 -2
View File
@@ -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.