chore(secrets): adopt ZB.MOM.WW.Secrets 0.6.2 and close the pre-host guard gap
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.
This commit is contained in:
@@ -21,11 +21,13 @@ x-secrets-hub-env: &secrets-hub-env
|
||||
# ciphertext replicates, so a node with a different KEK fails closed on resolve with a
|
||||
# kek_id mismatch that reads like corruption but is a deployment error.
|
||||
ZB_SECRETS_MASTER_KEY: "zZiBWuoaVMbJmGXToLk9Lakw0iJozXoL/7Gxac3GwJ4="
|
||||
# The appsettings default is the relative "scadabridge-secrets.db", which resolves to
|
||||
# /app — inside the image's writable layer, so it is destroyed by any container
|
||||
# recreate and unreachable from the host. /app/data is the node's own mounted volume
|
||||
# (the one LocalDb already uses on sites; added to the central pair for this).
|
||||
Secrets__SqlitePath: "/app/data/scadabridge-secrets.db"
|
||||
# Must be absolute AND outside the content root (/app) — Secrets 0.6.2 validates both
|
||||
# at startup, because a store inside the deployment directory is destroyed by an
|
||||
# ordinary upgrade (this wiped the MxGateway key store on 2026-08-09). /data is the
|
||||
# node's own bind mount, the SAME host directory as /app/data, so the existing store
|
||||
# is found unmoved; only the path used to reach it changes. Do NOT point this back at
|
||||
# /app/data — absolute is not sufficient, and the boot will fail.
|
||||
Secrets__SqlitePath: "/data/scadabridge-secrets.db"
|
||||
Secrets__Replication__Enabled: "true"
|
||||
Secrets__Replication__Mode: "Grpc"
|
||||
# DEV-ONLY shared bearer token — NOT a real secret. Presented by every follower and
|
||||
@@ -116,6 +118,10 @@ services:
|
||||
# scadaproj#4) the scadabridge-secrets.db here is a pre-0.5.0 residue, but the
|
||||
# volume is still needed (inbound-api-keys.sqlite lives on it).
|
||||
- ./central-node-a/data:/app/data
|
||||
# Same host directory, mounted a second time OUTSIDE the content root (/app).
|
||||
# Secrets 0.6.2 refuses a store path under the content root; this bind mount is
|
||||
# not touched by an image rebuild, but the guard is lexical and cannot know that.
|
||||
- ./central-node-a/data:/data
|
||||
- ./central-node-a/logs:/app/logs
|
||||
networks:
|
||||
- scadabridge-net
|
||||
@@ -173,6 +179,10 @@ services:
|
||||
# scadaproj#4) the scadabridge-secrets.db here is a pre-0.5.0 residue, but the
|
||||
# volume is still needed (inbound-api-keys.sqlite lives on it).
|
||||
- ./central-node-b/data:/app/data
|
||||
# Same host directory, mounted a second time OUTSIDE the content root (/app).
|
||||
# Secrets 0.6.2 refuses a store path under the content root; this bind mount is
|
||||
# not touched by an image rebuild, but the guard is lexical and cannot know that.
|
||||
- ./central-node-b/data:/data
|
||||
- ./central-node-b/logs:/app/logs
|
||||
networks:
|
||||
- scadabridge-net
|
||||
@@ -197,6 +207,10 @@ services:
|
||||
volumes:
|
||||
- ./site-a-node-a/appsettings.Site.json:/app/appsettings.Site.json:ro
|
||||
- ./site-a-node-a/data:/app/data
|
||||
# Same host directory, mounted a second time OUTSIDE the content root (/app).
|
||||
# Secrets 0.6.2 refuses a store path under the content root; this bind mount is
|
||||
# not touched by an image rebuild, but the guard is lexical and cannot know that.
|
||||
- ./site-a-node-a/data:/data
|
||||
- ./site-a-node-a/logs:/app/logs
|
||||
networks:
|
||||
- scadabridge-net
|
||||
@@ -221,6 +235,10 @@ services:
|
||||
volumes:
|
||||
- ./site-a-node-b/appsettings.Site.json:/app/appsettings.Site.json:ro
|
||||
- ./site-a-node-b/data:/app/data
|
||||
# Same host directory, mounted a second time OUTSIDE the content root (/app).
|
||||
# Secrets 0.6.2 refuses a store path under the content root; this bind mount is
|
||||
# not touched by an image rebuild, but the guard is lexical and cannot know that.
|
||||
- ./site-a-node-b/data:/data
|
||||
- ./site-a-node-b/logs:/app/logs
|
||||
networks:
|
||||
- scadabridge-net
|
||||
@@ -242,6 +260,10 @@ services:
|
||||
volumes:
|
||||
- ./site-b-node-a/appsettings.Site.json:/app/appsettings.Site.json:ro
|
||||
- ./site-b-node-a/data:/app/data
|
||||
# Same host directory, mounted a second time OUTSIDE the content root (/app).
|
||||
# Secrets 0.6.2 refuses a store path under the content root; this bind mount is
|
||||
# not touched by an image rebuild, but the guard is lexical and cannot know that.
|
||||
- ./site-b-node-a/data:/data
|
||||
- ./site-b-node-a/logs:/app/logs
|
||||
networks:
|
||||
- scadabridge-net
|
||||
@@ -263,6 +285,10 @@ services:
|
||||
volumes:
|
||||
- ./site-b-node-b/appsettings.Site.json:/app/appsettings.Site.json:ro
|
||||
- ./site-b-node-b/data:/app/data
|
||||
# Same host directory, mounted a second time OUTSIDE the content root (/app).
|
||||
# Secrets 0.6.2 refuses a store path under the content root; this bind mount is
|
||||
# not touched by an image rebuild, but the guard is lexical and cannot know that.
|
||||
- ./site-b-node-b/data:/data
|
||||
- ./site-b-node-b/logs:/app/logs
|
||||
networks:
|
||||
- scadabridge-net
|
||||
@@ -284,6 +310,10 @@ services:
|
||||
volumes:
|
||||
- ./site-c-node-a/appsettings.Site.json:/app/appsettings.Site.json:ro
|
||||
- ./site-c-node-a/data:/app/data
|
||||
# Same host directory, mounted a second time OUTSIDE the content root (/app).
|
||||
# Secrets 0.6.2 refuses a store path under the content root; this bind mount is
|
||||
# not touched by an image rebuild, but the guard is lexical and cannot know that.
|
||||
- ./site-c-node-a/data:/data
|
||||
- ./site-c-node-a/logs:/app/logs
|
||||
networks:
|
||||
- scadabridge-net
|
||||
@@ -305,6 +335,10 @@ services:
|
||||
volumes:
|
||||
- ./site-c-node-b/appsettings.Site.json:/app/appsettings.Site.json:ro
|
||||
- ./site-c-node-b/data:/app/data
|
||||
# Same host directory, mounted a second time OUTSIDE the content root (/app).
|
||||
# Secrets 0.6.2 refuses a store path under the content root; this bind mount is
|
||||
# not touched by an image rebuild, but the guard is lexical and cannot know that.
|
||||
- ./site-c-node-b/data:/data
|
||||
- ./site-c-node-b/logs:/app/logs
|
||||
networks:
|
||||
- scadabridge-net
|
||||
|
||||
Reference in New Issue
Block a user