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:
Joseph Doherty
2026-08-11 09:16:31 -04:00
parent 3eb7df74eb
commit fdc6b0c2bb
8 changed files with 251 additions and 17 deletions
+39 -5
View File
@@ -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