diff --git a/Directory.Packages.props b/Directory.Packages.props
index 3888b982..99feedbc 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -132,6 +132,9 @@
+
+
+
diff --git a/NuGet.config b/NuGet.config
index 2d7a0dcd..48599f6d 100644
--- a/NuGet.config
+++ b/NuGet.config
@@ -25,6 +25,8 @@
+
+
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts.csproj b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts.csproj
index 6f3fa0bd..c20a5566 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts.csproj
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts.csproj
@@ -13,5 +13,6 @@
+
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj
index 34c57c0d..0226d2fe 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj
@@ -21,6 +21,7 @@
+
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ZB.MOM.WW.OtOpcUa.AdminUI.csproj b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ZB.MOM.WW.OtOpcUa.AdminUI.csproj
index f82ee26d..10a58694 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ZB.MOM.WW.OtOpcUa.AdminUI.csproj
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ZB.MOM.WW.OtOpcUa.AdminUI.csproj
@@ -10,6 +10,7 @@
+
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs
index 49ff37b1..a67afb20 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs
@@ -37,6 +37,11 @@ using ZB.MOM.WW.OtOpcUa.Security.Ldap;
using ZB.MOM.WW.Configuration;
using ZB.MOM.WW.Telemetry.Serilog;
using ZB.MOM.WW.OtOpcUa.AdminUI.Api;
+using Microsoft.Extensions.DependencyInjection;
+using ZB.MOM.WW.Secrets.Abstractions;
+using ZB.MOM.WW.Secrets.Configuration;
+using ZB.MOM.WW.Secrets.DependencyInjection;
+using ZB.MOM.WW.Secrets.Sqlite;
// Roles drive the entire conditional wiring below — see ZB.MOM.WW.OtOpcUa.Cluster.RoleParser.
var roles = RoleParser.Parse(Environment.GetEnvironmentVariable("OTOPCUA_ROLES"));
@@ -72,6 +77,27 @@ if (roleSuffix is not null)
builder.Configuration.AddCommandLine(args);
}
+// Pre-host ${secret:} expansion: rewrites ${secret:name} tokens in the assembled configuration
+// BEFORE the host is built, so every downstream binder/validator sees resolved plaintext.
+// Placement matters: this runs AFTER all config providers are assembled (base appsettings, the
+// role overlay, and the env-vars + command-line re-append above) but BEFORE anything READS a
+// config value (AddZbSerilog's ReadFrom.Configuration, AddOtOpcUaConfigDb, and the first
+// ValidateOnStart bind all follow). With no ${secret:} tokens present this is a no-op pass;
+// it also migrates the secrets SQLite store on startup.
+// ASP0000: DELIBERATE throwaway container — expansion must run before builder.Build(), so it
+// cannot use the app's DI. Fully disposed here; shares no singletons with the host container.
+#pragma warning disable ASP0000
+await using (var secretsProvider = new ServiceCollection()
+ .AddZbSecrets(builder.Configuration, "Secrets")
+ .BuildServiceProvider())
+#pragma warning restore ASP0000
+{
+ await secretsProvider.GetRequiredService().MigrateAsync(default);
+ var resolver = secretsProvider.GetRequiredService();
+ await new SecretReferenceExpander(resolver)
+ .ExpandConfigurationAsync((IConfigurationRoot)builder.Configuration, default);
+}
+
// Anchor the process working directory to the install directory (AppContext.BaseDirectory) so every
// relative runtime path resolves under the install dir rather than the service's startup CWD. A Windows
// service starts with CWD=C:\Windows\System32, which otherwise scattered the Serilog rolling-file sink
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj b/src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj
index a27a1054..7a41b689 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj
@@ -33,6 +33,8 @@
+
+
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.json b/src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.json
index d3c19359..dbeab448 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.json
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.json
@@ -11,6 +11,15 @@
"DisableLogin": false
}
},
+ "Secrets": {
+ "SqlitePath": "otopcua-secrets.db",
+ "MasterKey": {
+ "Source": "Environment",
+ "EnvVarName": "ZB_SECRETS_MASTER_KEY"
+ },
+ "RunMigrationsOnStartup": true,
+ "ResolveCacheTtl": "00:00:30"
+ },
"ServerHistorian": {
"_comment": "Server-side HistoryRead backend (the ZB.MOM.WW.HistorianGateway gRPC client). Disabled => NullHistorianDataSource (historized nodes return GoodNoData). The gateway must run RuntimeDb:EventReadsEnabled=true for alarm-history ReadEvents, and the API key must carry historian:read + historian:write + historian:tags:write scopes.",
"Enabled": false,