# Secrets — GAPS (adoption backlog) The delta between each project's [`current-state`](current-state/) and the [`SPEC`](spec/SPEC.md), 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). ## 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/`get` decrypt round-trip + **zero plaintext in the SQLite store at rest**. (`/admin/secrets` interactive reveal not re-proven on this box — same shared `.Ui` component 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 the `MxGateway__Ldap__ServiceAccountPassword` env override. **G-4/G-5/G-6 below are the remaining OtOpcUa + ScadaBridge scope.** - **OtOpcUa, ScadaBridge — planned, NOT executed** (recommended order: OtOpcUa next, then ScadaBridge). ## 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`](../../docs/plans/2026-07-16-secrets-adoption-design.md) — shared design: library API, the two resolution layers (pre-host `${secret:}` config expander vs runtime `ISecretResolver`), wiring template, master-key/clustering fork. - [`docs/plans/2026-07-16-secrets-adoption-otopcua.md`](../../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`](../../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`](../../docs/plans/2026-07-16-secrets-adoption-mxaccessgw.md) — **G-4 + G-5 + G-6** (8 tasks; single-box, no Layer B). G-7/G-8 remain design-only/deferred (below). ## Cross-cutting observations - **No app has `${secret:}` resolution today.** All three assemble config with the stock `AddJsonFile`+`AddEnvironmentVariables` chain 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-level `GalaxySecretRef` (`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`/`UserCertificatePassword` and the `dev:`/literal Galaxy API key live cleartext in the central config DB; ScadaBridge's MxGateway per-endpoint `ApiKey` lives plaintext in the ConfigDb `DataConnections` JSON. 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 or `ProtectKeysWith*`; key rings are unencrypted-at-rest. `ZB.MOM.WW.Secrets` adds 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) — build `ZB.MOM.WW.Secrets.Akka` Deferred until G-2…G-6 land for a clustered app. Options per SPEC: shared SQL-Server `ISecretStore` (simplest — mirrors the shared Data-Protection key ring) **or** the Akka replicator (LWW, anti-entropy resync, tombstones). Requires a shared KEK. ### G-8 — KEK-rotation runbook + `RewrapAll` The `RewrapAll(oldKek, newKek)` admin primitive + an operator runbook. Deferred (design-only in this cut). ## Out of scope (this component) - Akka remoting authentication/TLS (ScadaBridge plain TCP remoting) — a separate hardening concern, not secret storage. - SQL-Server `ISecretStore` provider construction (seam supports it; build with G-7).