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:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user