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,