docs(secrets): reconcile 0.2.1 adoption - what was proven vs merely built

- All 4 apps on 0.2.1 (local branches, unpushed).
- Records the corrected security story: the version bump closed NO advisory (all four
  repos already resolved patched 2.1.12); the real live vulnerability was in ScadaBridge,
  masked by a NuGetAuditSuppress, and was fixed by separate work.
- Records the upstream 0.2.0 inert-Akka-replicator defect, its root cause (DI extensions
  with no container-building test - third instance of that class), and the 0.2.1 fix.
- Marks clustered topology WIRED-but-default-OFF and explicitly NOT live-validated;
  Task 9 remains open and the topology is not 'adopted' until it passes.
This commit is contained in:
Joseph Doherty
2026-07-18 12:21:31 -04:00
parent 6e8d346670
commit 62a96e5f22
8 changed files with 670 additions and 26 deletions
@@ -1,5 +1,14 @@
# Secrets Adoption (G-2 … G-6) — Shared Design
> **✅ EXECUTED 2026-07-16 at lib `0.1.2` — all four apps adopted.** Version numbers and the
> Layer-A/Layer-B wiring template below are the **historical record of that adoption** and are
> left as-written; do not read `0.1.2` here as current. **Current version is `0.2.0`**, and the
> clustering guidance in §5 has been superseded — see
> [`2026-07-18-secrets-0.2.0-upgrade-and-clustering.md`](2026-07-18-secrets-0.2.0-upgrade-and-clustering.md)
> for the version bump and the per-app clustered topology. Everything else here (API surface §1,
> the two resolution layers §2, the wiring template §3, the gotchas §8) remains accurate and is
> still the reference for any new consumer.
> **For Claude:** This is the shared design that the three per-repo implementation plans
> depend on. It is NOT itself executed. The executable plans are:
> - [`2026-07-16-secrets-adoption-otopcua.md`](2026-07-16-secrets-adoption-otopcua.md) — G-2, G-4, G-5, G-6
@@ -178,28 +187,40 @@ Both apps resolve secrets on multiple nodes (OtOpcUa driver-role nodes resolve L
secrets; both central nodes of a ScadaBridge pair resolve Layer-A config secrets at boot). Two
constraints follow: (1) identical KEK on every node, (2) identical store contents on every node.
The library today ships a **SQLite-only store** with a **`NoOpSecretReplicator`** — there is no
built-in shared-SQL store and no cross-node replication. So the near-term (G-5) posture is:
> **⚠️ UPDATED 2026-07-18 — the interim posture below is superseded.** This section originally
> said the library "ships a SQLite-only store with a `NoOpSecretReplicator` — there is no built-in
> shared-SQL store and no cross-node replication," and recommended a shared-mounted-SQLite
> workaround. **That is no longer true.** G-7 shipped in `0.2.0` with two real replication
> packages. The KEK guidance below still stands verbatim; the *store* guidance does not.
> The shared-SQLite-on-a-network-share workaround should **not** be adopted — it was always a
> caveat-laden stopgap, and there is now a supported alternative.
>
> Current topology options and the per-app recommendation live in
> [`2026-07-18-secrets-0.2.0-upgrade-and-clustering.md`](2026-07-18-secrets-0.2.0-upgrade-and-clustering.md) §2.
> In brief: **OtOpcUa → Akka peer-to-peer** (one cluster, pub/sub already in product use);
> **ScadaBridge → SQL-Server hub mode** (central and each site are *separate* Akka clusters, so
> `DistributedPubSub` cannot cross that boundary — an Akka replicator there would create secret
> islands that never reconcile while every node reports healthy).
- **Recommended interim: `File` provider with a shared mounted key** (`MasterKey.Source=File`,
`FilePath` = a read-only mounted secret file identical on every node) **+ a single shared
SQLite store on a shared/replicated volume** (`SqlitePath` → the shared mount). Secrets are
written rarely, from one node's UI/CLI; every node reads the same file.
*Best-practice fit:* satisfies the library's documented hard requirement (same KEK per node)
with zero new code; acceptable because secret **writes** are rare and human-driven while
**reads** dominate. *Caveat:* SQLite over a network share has known multi-writer locking
limits — fine for read-mostly, single-writer; not a substitute for real replication.
- **Alternative (more integrated, more work): a ConfigDb-backed `ISecretStore`** mirroring each
app's existing `AddDataProtection().PersistKeysToDbContext<…>()` pattern (OtOpcUa persists its
DP key ring to ConfigDb precisely to share key material across nodes). This is the "right"
long-term shape but requires building a custom `ISecretStore` — it **overlaps G-7** and is
deferred there, not attempted in this cut.
**KEK requirement (unchanged, and independent of store choice):** every node must resolve the
**same** master key. Deliver it via `MasterKey.Source=File` with a read-only mounted key file
identical on every node, or the same `ZB_SECRETS_MASTER_KEY` env value. A node with a different
KEK stores replicated rows fine and then fails closed on every resolve with a `kek_id` mismatch —
it reads like data corruption but is a deployment error.
> **Decision to confirm at execution time:** for the two clustered apps, take the interim
> File-KEK + shared-SQLite posture now (ships G-5 with no new library code) and track the
> ConfigDb/`ISecretStore` integration under G-7 — **unless** you want the integrated store
> built first, which enlarges G-5 into library work. Each clustered plan is written for the
> interim posture and flags the G-7 hand-off.
**Store options as of `0.2.0`** (all three preserve ciphertext-only-at-rest):
- **Shared SQL store** (`AddZbSecretsSqlServerStore`) — one database, one copy of each row, no
replication and no reconciliation. Simplest; the trade is that a node partitioned from the DB
falls back only to the resolver's short TTL cache.
- **Local store + SQL hub** (`AddZbSecretsSqlServerReplication`) — each node keeps a local store
and syncs bidirectionally with a central hub. Survives partition; converges eventually.
- **Akka peer-to-peer** (`AddZbSecretsAkkaReplication`) — no shared database at all; live
broadcast plus periodic manifest anti-entropy **within a single Akka cluster**.
**Rotation is per independent store.** G-8 `rewrap-all` runs once for a shared store; once per
node *and* against the hub in hub mode; once per node in Akka mode. Re-wraps deliberately do not
replicate (they leave `revision`/`updated_utc` untouched so LWW ignores them).
---
@@ -315,6 +336,9 @@ default and push to Gitea.
## 11. Task tracking
Each per-repo plan co-locates its `*.tasks.json` beside the `.md`. Deferred items (G-7 Akka
replicator / shared-SQL `ISecretStore`, G-8 `RewrapAll` KEK-rotation) stay in
[`components/secrets/GAPS.md`](../../components/secrets/GAPS.md) and are out of scope here.
Each per-repo plan co-locates its `*.tasks.json` beside the `.md`. The items this design deferred
— G-7 (replication) and G-8 (`RewrapAll` KEK rotation) — were **both built and shipped in `0.2.0`
on 2026-07-18**, G-7 as *two* packages (`.Replicator.SqlServer` and `.Replicator.AkkaDotNet`)
rather than the single deferred Akka replicator this section anticipated. Adopting them in the
apps is [`2026-07-18-secrets-0.2.0-upgrade-and-clustering.md`](2026-07-18-secrets-0.2.0-upgrade-and-clustering.md);
remaining backlog stays in [`components/secrets/GAPS.md`](../../components/secrets/GAPS.md).