Packing Secrets 0.1.3 surfaced NU1903: Microsoft.Data.Sqlite 10.0.7 pulls SQLitePCLRaw.lib.e_sqlite3 2.1.11, which carries high-severity advisory GHSA-2m69-gcr7-jv3q. Auth.ApiKeys had the same exposure and was already shipped at 0.1.4 to three consumers, so both libs are fixed rather than only the one being published. Fixed the way ZB.MOM.WW.LocalDb already had it: CentralPackageTransitivePinningEnabled plus a pin to the patched 2.1.12. Bumping Microsoft.Data.Sqlite does not help -- even 10.0.10 still resolves 2.1.11 -- so the pin is the actual fix. The pin reaches consumers: both nuspecs now declare SQLitePCLRaw.lib.e_sqlite3 >= 2.1.12 as a direct dependency, verified by restoring the published packages into a scratch project, which resolves 2.1.12 and scans clean. Auth goes 0.1.4 -> 0.1.5 with no API change (0.1.4 plus the pin). Suites re-run after the native-lib swap with identical counts, so no behavioral regression: Secrets 97 pass/1 skip, Auth 215 pass/1 skip (both skips are Windows-only DPAPI and opt-in LDAP). Consumers are NOT yet bumped and remain on vulnerable versions: mxaccessgw 0.1.4, HistorianGateway 0.1.4, ScadaBridge 0.1.3. Secrets consumers all sit at 0.1.2 and also lack KEK rotation. Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
18 KiB
Secrets — GAPS (adoption backlog)
The delta between each project's current-state and the
SPEC, as prioritized work. The library itself is built + published +
reference-consumer-proven; everything here is per-app adoption of an existing lib (not lib
construction).
Current state (verified 2026-07-18)
0.1.3PUBLISHED to the Gitea feed 2026-07-18 (all three packages; restore-verified from a scratch consumer). Carries the G-8 KEK-rotation work (KekRotationService,RewrapReport,secret rewrap-all,docs/operations/kek-rotation.md), committed onmainatd82d345. (An earlier note claiming G-8 was "not yet committed/pushed" was stale.)0.1.3also carries a transitive security fix that0.1.2does not. Packing surfaced NU1903:Microsoft.Data.Sqlite 10.0.7pullsSQLitePCLRaw.lib.e_sqlite3 2.1.11, which has a high-severity advisory (GHSA-2m69-gcr7-jv3q). Fixed the wayZB.MOM.WW.LocalDbalready did —CentralPackageTransitivePinningEnabled+ a pin to the patched2.1.12. BumpingMicrosoft.Data.Sqlitedoes not help (even10.0.10still pulls2.1.11); the pin is the fix. Verified: the shipped.nuspecdeclaresSQLitePCLRaw.lib.e_sqlite3 >= 2.1.12as a direct dependency, so consumers get the patched native lib without needing their own pin, and a fresh consumer restoring0.1.3scans clean.- All four apps are still pinned at
0.1.2— they get neither KEK rotation nor the security fix until bumped. - All four apps are pinned to
0.1.2— OtOpcUa, MxAccessGateway, ScadaBridge takeSecrets+.Abstractions+.Ui; HistorianGateway takesSecrets+.Ui. - G-7 clustered replication is designed + planned, not built — plan at
docs/plans/2026-07-17-secrets-g7-*(resolved to build the shared SQL-ServerISecretStore; Akka replicator deferred to phase 2). NoISecretStoreSQL-Server implementation exists yet. - Library suite: 97 passing, 1 skipped, green 2026-07-18.
Open
- ⬜ Bump the four apps
0.1.2→0.1.3to pick up KEK rotation + the SQLitePCLRaw security fix. - ⬜ Execute the G-7 plan (shared SQL-Server
ISecretStore). - ⬜ Per-app live wonder gates for the G-8 rotation path.
Execution status (2026-07-16)
- mxaccessgw — G-4 + G-5 + G-6 DONE. Executed via the plan below (8 tasks, subagent-driven,
classification-driven reviews), merged + pushed to
origin/main(mxaccessgw @e088dfa). Offline: 0-warning build, full suite 781 pass / 44 pre-existing worker-COM (0 non-worker), behavioral claim-type authz test, no Data-Protection disturbance. Box-verified on windev (real .NET 10.0.8): fail-closed on unseeded${secret:}(SecretNotFoundException, refuses to start) + CLI seed/getdecrypt round-trip + zero plaintext in the SQLite store at rest. (/admin/secretsinteractive reveal not re-proven on this box — same shared.Uicomponent already live-proven via HistorianGateway; box gateway runs LDAP-disabled/anonymous so a real LDAP-via-secret login wasn't exercised there.) Two review-caught fixes folded in: the LDAP password change's test/doc blast radius (validator-helper + host-test-bootstrap + doc truth), and the live-LDAP integration suite now honors theMxGateway__Ldap__ServiceAccountPasswordenv override. G-4/G-5/G-6 below are the remaining OtOpcUa + ScadaBridge scope. - ScadaBridge — G-3 + G-4 + G-5 + G-6 DONE. Executed via the plan below (10 tasks, subagent-driven,
classification-driven reviews), FF-merged + pushed to
origin/main(ScadaBridge @128f1596). Offline: 0-warning build, all suites green (Host 279, DCL 235 incl. 5 G-3 resolver tests, Security 181, CentralUI 925, InboundAPI 269, SiteRuntime 522/523 [1 pre-existing ScriptActor timing-flake, passes isolated + branch touched no SiteRuntime src], Commons/ConfigDb/ManagementService/Communication/ClusterInfra all green). G-3 LIVE-PROVEN vs the REAL production MxGateway gateway (wonder-app-vd03.zmr.zimmer.com:5120): a throwaway harness drove the realMxGatewayDataConnectionthrough the DI factory +AddZbSecretswith asecret:-refApiKey→ dummy keyUnauthenticated(proves the resolved value is transmitted, not fail-closed first) → real read-scoped key →Connected+ browsed the real Galaxy root (10 nodes). Temp key created via the gateway dashboard, then revoked + deleted + functionally confirmed dead. Notable deltas from the plan: T3 registersAddZbSecretson both the Central branch (UI) andSiteServiceRegistration(DCL adapter) — the plan assumed one container; T6 claim-type resolved MATCH (ZbClaimTypes.Role == ClaimTypes.Role, adversarially verified) →AddSecretsAuthorization()used directly; T8 (G-5) shipped as a docs-only production-posture runbook (docs/operations/2026-07-16-secrets-clustered-master-key.md) because hardcodingSource=File+/sharedpaths would break the Central-boot Host.Tests + local dev (the pre-host expander migrates the store unconditionally every Central boot). One review-flagged non-blocking follow-up: a pre-existing xUnit env-mutation CI-flake that T4 amplifies (single[Collection]/ disable parallelization for Host.Tests). - OtOpcUa — G-2 + G-4 + G-5 + G-6 DONE (the LAST app — all three now adopted). Executed 2026-07-16 via the plan below (10 tasks, 2 slices, subagent-driven, classification-driven reviews), FF-merged + pushed to
origin/master(lmxopcua @872cf7e3,ec6598ce..872cf7e3). Clean build (0 errors). The full offline suite caught one real regression —AddOtOpcUaDriverFactories' registration now resolvesISecretResolver(Galaxy/OpcUaClient secret: refs), so twoResilienceInvokerFactoryRegistrationTeststhat built a minimal container threwNo service for ISecretResolver; fixed test-only by registering a stub resolver (production always has it via the unconditionalAddZbSecrets, andGetRequiredServicecorrectly fails-fast for a genuinely misconfigured host). Touched-area suites green (Galaxy 338, OpcUaClient 142, AdminUI 662); remaining non-passes are unrelated (flaky Akka/Roslyn timing tests pass in isolation; the environmentalAbCip_Green_AgainstSimsim-fixture test — zero AbCip code touched). Three deliberate deviations from the plan's literal text (verified corrections, same class as the CPM miss the plan itself caught): (1) G-4 does NOT commit a${secret:}token into the default appsettings — the expander is fail-closed AND section-agnostic and the only committed secret-shaped value isServerHistorian:ApiKey=""(empty, disabled-by-default), so a committed token would break every dev/CI/TwoNodeClusterHarnessboot for zero at-rest benefit; instead documented the token-delivery convention (docs/security.md) + updated the ServerHistorian comments, and proved fail-closed live (unseeded →SecretNotFoundExceptionat pre-host expansion; seeded → resolves). (2) G-5 shipped docs-only (docs/operations/2026-07-16-secrets-clustered-master-key.md) — hardcodingSource=File+shared paths into the committed role-overlay appsettings (also consumed by dev + integration) would break those boots; base staysSource=Environment. Mirrors the ScadaBridge G-5 resolution. (3) Task 8 resolves in the factory/driver only, NOT the probe — the v3OpcUaClientDriverProbeis unauthenticatedGetEndpoints-only and never readsPassword/UserCertificatePassword, so probe resolution would be dead code. Layer-B resolution mirrors Galaxy: the sync driver-registry factory means resolution happens lazily at the driver's async session-open (GalaxyDriver.BuildClientOptionsAsync/OpcUaClientDriver.InitializeAsync), threaded via ctor +DriverFactoryBootstrap(real resolver from DI;NullSecretResolvernull-object backs test/parse paths only, fail-closed onsecret:).OpcUaClientDriverOptionswas convertedsealed class→sealed recordfor thewith-expression (no positional params → identical JSON; verified no reference-equality/dict-key/ToString-log usages → no leak/behavior change). AdminUI G-2c is structurally leak-proof:DriverConfigpersists as an opaque JSON string viaRawTreeService, which has NO resolver dependency. Live wonder gate PROVEN 2026-07-16 (VPN up): a throwaway harness drove OtOpcUa's realGalaxyDriverBrowser(resolves thesecret:ApiKeySecretRefviaISecretResolver→GalaxySecretRef.ResolveApiKeyAsync→ connects) against the REAL production MxGatewaywonder-app-vd03:5120. Dummysecret:ref →MxGatewayAuthenticationException(proves the resolved value is transmitted, not fail-closed-first); realmetadata:readkey → CONNECTED + browsed the real Galaxy root (10 nodes: ControlSystem/EUR/KPI_CNC/NAM/_TestArea/MESReceiver…). Temp key minted via the gateway dashboard, then revoked (functionally proven — the same key that browsed 10 nodes then returned Unauthenticated) + deleted (row gone; no prod key touched; the dashboard key-list is stale-cached so revoke/delete were verified FUNCTIONALLY, not by the list UI). Harness + scratch stores destroyed. All of G-2/G-4/G-5/G-6 now DONE + live-proven across all three apps + mxaccessgw. Plan re-verified 2026-07-16 againstorigin/master@ec6598ce(the merged v3.0 dual-namespace rewrite, PR #472). The v3.0 rewrite did NOT touch the driver-secret flow, so the approach held; the plan was corrected for: OtOpcUa is on Central Package Management (was mis-stated as inline-versioned),OpcUaClientDriverOptionslives in…Driver.OpcUaClient.Contracts+GalaxyDriverBrowserin…Driver.Galaxy.Browser, andDriverHostActor.csline shifts (1704/1756/1790 → 1766/1818/1852). Program.cs / Security / AdminUI anchors survived essentially unchanged. - G-8 (KEK rotation) — BUILT; G-7 (clustered replication) — DESIGNED + PLANNED (2026-07-17).
G-8 shipped into
ZB.MOM.WW.Secrets(lib0.1.3): theRewrapDEK primitive, CAS-guardedApplyRewrapAsync,KekRotationService.RewrapAllAsync, thesecret rewrap-allCLI verb, and an operator runbook — full suite green (82+15, 0 regressions) + end-to-end CLI smoke, adversarial crypto review PASS with a review-caught TOCTOU closed via compare-and-swap. G-7's SQL-store-vs-Akka fork is resolved (build the shared SQL-ServerISecretStore; Akka replicator deferred phase-2) with a design doc + executable plan +.tasks.json, no code built. See the G-7/G-8 backlog entries below. Not yet committed/pushed (awaiting go-ahead).
Design + implementation plans (2026-07-16)
G-2 … G-6 are now planned. Shared design + three per-repo executable plans (task-metadata'd,
code-verified anchors, co-located .tasks.json):
docs/plans/2026-07-16-secrets-adoption-design.md— shared design: library API, the two resolution layers (pre-host${secret:}config expander vs runtimeISecretResolver), wiring template, master-key/clustering fork.docs/plans/2026-07-16-secrets-adoption-otopcua.md— G-2 + G-4 + G-5 + G-6 (10 tasks, 2 slices; Layer-A config + Layer-B driver secrets).docs/plans/2026-07-16-secrets-adoption-scadabridge.md— G-3 + G-4 + G-5 + G-6 (10 tasks; incl. claim-type verification + committed-plaintext cleanup).docs/plans/2026-07-16-secrets-adoption-mxaccessgw.md— G-4 + G-5 + G-6 (8 tasks; single-box, no Layer B).
G-8 (KEK rotation) is now BUILT; G-7 (clustered replication) is now DESIGNED + PLANNED (2026-07-17, below).
Cross-cutting observations
- No app has
${secret:}resolution today. All three assemble config with the stockAddJsonFile+AddEnvironmentVariableschain and read secrets verbatim. Two already lean on the pattern the expander formalizes: ScadaBridge's non-functional${SCADABRIDGE_*}placeholders (whole-key env override) and OtOpcUa's driver-levelGalaxySecretRef(env:/file:/dev:/literal). These are the natural first swaps. - The common high-value gap is plaintext credentials in appsettings/env: LDAP service-account passwords (all three), SQL connection-string passwords (OtOpcUa ConfigDb, ScadaBridge ConfigDb + MachineDataDb), JWT/HS256 signing keys (OtOpcUa, ScadaBridge), and API-key peppers (all three, runtime-supplied but plaintext).
- Secrets already stored in a DB in plaintext are the sharpest edge: OtOpcUa's OpcUaClient
Password/UserCertificatePasswordand thedev:/literal Galaxy API key live cleartext in the central config DB; ScadaBridge's MxGateway per-endpointApiKeylives plaintext in the ConfigDbDataConnectionsJSON. These leak at rest to anyone with DB read. - Existing at-rest crypto is Data Protection, and only ScadaBridge applies it to secrets
(4 encrypted ConfigDb columns via
EncryptedStringConverter). OtOpcUa/mxaccessgw use Data Protection only for cookies/tokens. None use DPAPI orProtectKeysWith*; key rings are unencrypted-at-rest.ZB.MOM.WW.Secretsadds envelope encryption with a KEK held outside the store — a stronger boundary than "the DB is the only protection." - Peppered-HMAC API-key stores stay bespoke (mxaccessgw + ScadaBridge): they are hashed, not reversibly stored — already correct. Secrets only takes over the pepper value supply.
Backlog (prioritized)
G-1 — HistorianGateway reference consumer ✅ DONE
Adopted + live-proven 2026-07-16 (historian password via ${secret:}, authenticated read
against the real wonder historian). This is the wiring template for the three apps.
G-2 — OtOpcUa: cleartext-in-DB driver secrets (highest value)
Add a secret: arm to GalaxySecretRef (its own comment anticipates this) and to the
OpcUaClient driver options, resolving through ISecretResolver — retire the dev:/literal
and plaintext Password/UserCertificatePassword DB paths.
G-3 — ScadaBridge: MxGateway ApiKey plaintext-in-ConfigDb
Resolve via ISecretResolver or extend the existing EncryptedStringConverter to that JSON
column.
G-4 — Pre-host ${secret:} for plaintext config secrets (all three)
LDAP passwords, SQL connstr passwords, JWT signing keys, deploy API key (OtOpcUa),
peppers — swap to ${secret:…} tokens, expander running before each app's existing
validator (ConfigPreflight / Ldap+OpcUa validators / GatewayOptionsValidator).
Delete committed dev plaintext and the loose *_login.txt files (ScadaBridge).
G-5 — Master-key provider per deployment
Env (ZB_SECRETS_MASTER_KEY) for containers; DPAPI for the Windows boxes; File (shared
mounted key) for clustered pairs (hard requirement — same KEK on every node).
G-6 — Mount /admin/secrets UI (all three)
Add the RCL page to each dashboard (OtOpcUa AdminUI, MxGateway Server, ScadaBridge CentralUI)
and map each app's admin role onto secrets:manage / secrets:reveal.
G-7 — Clustered replication (ScadaBridge, OtOpcUa) — DESIGNED + PLANNED (2026-07-17)
The SPEC's "shared SQL-Server store vs Akka replicator" fork is resolved: build Option A —
a shared SQL-Server ISecretStore (both clustered apps already run shared SQL + a shared
Data-Protection key ring, so it delivers cluster-wide secrets with zero distributed-systems failure
modes); the Akka replicator (ZB.MOM.WW.Secrets.Akka, LWW/anti-entropy/tombstones) is a deferred
phase-2 to build only on a stated partition-tolerance requirement. Both require a shared KEK; the
ISecretReplicator seam stays in place so Option B remains a drop-in later. Design + executable
plan (+ .tasks.json): docs/plans/2026-07-17-secrets-g7-clustered-replication-design.md
docs/plans/2026-07-17-secrets-g7-sqlserver-store.md. No code built yet — plan is ready to execute.
G-8 — KEK-rotation RewrapAll + runbook ✅ BUILT (2026-07-17, lib 0.1.3)
The RewrapAll(oldKek, newKek) admin primitive + operator runbook are built + fully tested in
ZB.MOM.WW.Secrets. Because the envelope wraps a per-secret DEK under the KEK, rotation re-wraps
DEKs only — bodies are never re-encrypted, revision/timestamps are preserved (invisible to cluster
LWW). Shipped: ISecretCipher.Rewrap(row, oldKek, newKek) (fail-closed on wrong old KEK; DEK zeroed
on all paths; also catches malformed-length wraps → SecretDecryptionException),
ISecretStore.ApplyRewrapAsync(rewrappedRow, expectedCurrentWrappedDek) (updates only the 4 wrap
columns + kek_id, compare-and-swap on the current wrapped DEK so a concurrent write can't
corrupt a row — review-caught TOCTOU), KekRotationService.RewrapAllAsync + RewrapReport
(enumerates all rows incl. tombstones, idempotent/resumable skip-already-current, bounded CAS-retry,
fail-closed on unknown/identical KEK), the secret rewrap-all CLI verb (key material only via
env-var-name/file-path — never a literal), README section, and the operator runbook
ZB.MOM.WW.Secrets/docs/operations/kek-rotation.md.
Verified: full offline suite green (82 core + 15 UI, 0 regressions) + an end-to-end CLI smoke
(migrate → decrypts under new KEK, fail-closed under old → idempotent re-run → identical-KEK guard).
Version bumped 0.1.2→0.1.3 (additive interface members); republish to the Gitea feed is a
separate opt-in step (the apps don't need rewrap — it's a library/CLI admin primitive). Reviewed
by an adversarial crypto/correctness pass (all 7 categories PASS); the one real finding (concurrent-
write TOCTOU) is fixed by the CAS above, plus two low/nit fixes folded in.
Out of scope (this component)
- Akka remoting authentication/TLS (ScadaBridge plain TCP remoting) — a separate hardening concern, not secret storage.
- SQL-Server
ISecretStoreprovider construction (seam supports it; build with G-7).