fdc6b0c2bb
0.6.x refuses a secret store whose path is relative or inside the content root,
because a store in the deployment directory is destroyed by an ordinary upgrade —
the failure that wiped the MxGateway API-key store on 2026-08-09 and read as an
auth outage rather than a deployment error.
The pin alone would not have protected this repo. Program.cs expands ${secret:}
before the host exists, composing secrets into a throwaway ServiceCollection with
no IHostEnvironment, so the guard would not run at the moment the migrator creates
the store. That composition now lives in SecretsRegistration with an explicit
content root — resolved to match what the host resolves later, including the
Windows-Service case where the pre-host CWD is still system32 — and is covered by
PreHostSecretsContentRootTests, verified by simulating the regression and
confirming it fails on the leftover file rather than on the exception.
The docker rig needed a fix too: /app/data is absolute but inside the container's
content root, so all 8 nodes would have failed to boot. Each node's data directory
is now mounted a second time at /data; same host directory, so existing stores
carry over untouched.
Verified: build clean, 29 test assemblies green (Playwright's 159 failures are the
pre-existing SEC-36 login baseline). Not yet deployed — the rig runs the old
config until someone redeploys.
95 lines
5.1 KiB
JSON
95 lines
5.1 KiB
JSON
{
|
|
"ScadaBridge": {
|
|
"_secretsClusterPosture": "Central pair KEK/store posture (production): see docs/operations/2026-07-16-secrets-clustered-master-key.md. Committed default is Source=Environment with SqlitePath UNSET (Secrets 0.6.2 rejects a relative path, and a path inside the content root, at startup — so the library's absolute per-user default is taken in dev); production uses a File KEK + shared store volume delivered out-of-band. Do NOT hardcode /shared paths here — the pre-host expander migrates the store at every Central boot and would break dev/test.",
|
|
"_nodeName": "Host-018: NodeName stamps SourceNode on AuditLog/Notifications/SiteCalls rows (CLAUDE.md 'Centralized Audit Log' decision) and backs IX_AuditLog_Node_Occurred. Convention: 'central-a'/'central-b' for central nodes, 'node-a'/'node-b' for site nodes. Override per-node in multi-node deployments (the docker per-node configs do this). When left at the default below, single-node dev rows are stamped with 'central-a'; an empty value normalises to a NULL SourceNode.",
|
|
"Node": {
|
|
"Role": "Central",
|
|
"NodeHostname": "localhost",
|
|
"RemotingPort": 8081,
|
|
"NodeName": "central-a"
|
|
},
|
|
"Cluster": {
|
|
"SeedNodes": [
|
|
"akka.tcp://scadabridge@localhost:8081",
|
|
"akka.tcp://scadabridge@localhost:8082"
|
|
],
|
|
"SplitBrainResolverStrategy": "auto-down",
|
|
"StableAfter": "00:00:15",
|
|
"HeartbeatInterval": "00:00:02",
|
|
"FailureDetectionThreshold": "00:00:10",
|
|
"MinNrOfMembers": 1,
|
|
"_bootstrapGuard": "Gitea #33 simultaneous-cold-start split-brain guard. Dark switch, default off (Akka's config-driven self-first auto-join, unchanged). Enable ONLY where both pair VMs can power up together (shared power/hypervisor event) with no start-order serialization: then the lower host:port founds self-first and the higher probes-then-joins, so the pair converges to one cluster instead of two 1-node clusters. Timings validated > 0 when enabled.",
|
|
"BootstrapGuard": {
|
|
"Enabled": false,
|
|
"PartnerProbeSeconds": 25,
|
|
"PartnerProbeIntervalMs": 500,
|
|
"ProbeConnectTimeoutMs": 1000
|
|
}
|
|
},
|
|
"_secrets": "Host-003: Secrets are NOT committed in this file. The ${secret:...} references below are resolved at startup by the pre-host secrets expander (before StartupValidator runs) from the encrypted secrets store (SQLite, seeded via the ZB.MOM.WW.Secrets store/CLI/UI). The KEK is supplied out-of-band via the ZB_SECRETS_MASTER_KEY environment variable and never committed; an unseeded reference fails closed (SecretNotFoundException) before any SQL/LDAP/cluster wiring. ROLLBACK: the whole-key environment override still wins — set ScadaBridge__Database__ConfigurationDb / ScadaBridge__Security__Ldap__ServiceAccountPassword / ScadaBridge__Security__JwtSigningKey and .AddEnvironmentVariables() overlays the concrete value before expansion runs, so the ${secret:...} token is never evaluated. NOTE (Task 1.4): the LDAP settings moved into the nested Security:Ldap sub-section (bound to the shared ZB.MOM.WW.Auth LdapOptions) — the service-account-password env var is ScadaBridge__Security__Ldap__ServiceAccountPassword (was ScadaBridge__Security__LdapServiceAccountPassword).",
|
|
"Database": {
|
|
"ConfigurationDb": "${secret:sql/scadabridge/configdb-connection}"
|
|
},
|
|
"Security": {
|
|
"Ldap": {
|
|
"Server": "localhost",
|
|
"Port": 3893,
|
|
"Transport": "None",
|
|
"AllowInsecure": true,
|
|
"SearchBase": "dc=zb,dc=local",
|
|
"ServiceAccountDn": "cn=admin,dc=zb,dc=local",
|
|
"ServiceAccountPassword": "${secret:ldap/scadabridge/service-account-password}"
|
|
},
|
|
"JwtSigningKey": "${secret:security/scadabridge/jwt-signing-key}",
|
|
"JwtExpiryMinutes": 15,
|
|
"IdleTimeoutMinutes": 30
|
|
},
|
|
"Communication": {
|
|
"DeploymentTimeout": "00:02:00",
|
|
"LifecycleTimeout": "00:00:30",
|
|
"QueryTimeout": "00:00:30",
|
|
"TransportHeartbeatInterval": "00:00:05",
|
|
"TransportFailureThreshold": "00:00:15",
|
|
"CentralFetchBaseUrl": "http://localhost:5000"
|
|
},
|
|
"HealthMonitoring": {
|
|
"ReportInterval": "00:00:30",
|
|
"OfflineTimeout": "00:01:00"
|
|
},
|
|
"InboundApi": {
|
|
"DefaultMethodTimeout": "00:00:30"
|
|
},
|
|
"Notification": {
|
|
"SmtpServer": "localhost",
|
|
"SmtpPort": 1025,
|
|
"AuthMode": "None",
|
|
"FromAddress": "scada-notifications@company.com"
|
|
},
|
|
"NotificationOutbox": {
|
|
"DispatchInterval": "00:00:10",
|
|
"DispatchBatchSize": 100,
|
|
"StuckAgeThreshold": "00:10:00",
|
|
"TerminalRetention": "365.00:00:00",
|
|
"PurgeInterval": "1.00:00:00",
|
|
"DeliveredKpiWindow": "00:01:00"
|
|
},
|
|
"KpiHistory": {
|
|
"SampleInterval": "00:01:00",
|
|
"RetentionDays": 90,
|
|
"PurgeInterval": "1.00:00:00",
|
|
"DefaultMaxSeriesPoints": 200
|
|
},
|
|
"Transport": {
|
|
"BundleSessionTtlMinutes": 30,
|
|
"MaxBundleSizeMb": 100,
|
|
"MaxUnlockAttemptsPerSession": 3,
|
|
"MaxUnlockAttemptsPerIpPerHour": 10,
|
|
"Pbkdf2Iterations": 600000,
|
|
"SchemaVersionMajor": 1
|
|
},
|
|
"Logging": {
|
|
"MinimumLevel": "Information"
|
|
}
|
|
}
|
|
}
|