docs(secrets): record the P3 batch and #49 in findings, CHANGELOG 0.6.6 and the review index (scadaproj#45)

Secrets-004–009, 011, 012, 014–016, 018 and 019 marked Resolved with commit, fix and regression
evidence; Secrets open findings 13 -> 0; code-reviews/README.md regenerated (--check passes).
CHANGELOG 0.6.6 records the fixes, behaviour changes and the #49 follow-ups.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 15:31:01 -05:00
parent fc81cbbd36
commit 4a3c4e9b7e
4 changed files with 88 additions and 49 deletions
+54 -1
View File
@@ -7,7 +7,8 @@ one of them.
## 0.6.6 — unreleased (NOT published to the feed)
Fixes six scadaproj#15 code-review findings (`code-reviews/Secrets/findings.md`).
Fixes the scadaproj#15 code-review findings (`code-reviews/Secrets/findings.md`): six P2s (scadaproj#30–#35),
the P3 batch (scadaproj#45: Secrets-004–009, 011, 012, 014–016, 018, 019) and the scadaproj#49 follow-ups.
### Fixed
@@ -51,6 +52,58 @@ Fixes six scadaproj#15 code-review findings (`code-reviews/Secrets/findings.md`)
failure is logged as a warning (`ILogger<SecretEditor>` / `ILogger<ConfirmDeleteModal>`, when the host
registers logging).
- **A malformed row fails as `SecretDecryptionException`** (scadaproj#45, Secrets-004). A wrong-length
nonce, tag or wrapped DEK made `AesGcmEnvelopeCipher.Decrypt` throw a raw `ArgumentException`, which
skipped the resolver's `decryption-failed` audit.
- **One master-key reading per cipher operation, zeroed afterwards** (scadaproj#45, Secrets-007). The KEK
id and the key used to be two separate reads of the source, so a key file replaced mid-`set` could stamp
a row with the old key's id and wrap it under the new key — permanently undecryptable. New
`MasterKeySnapshot` + default-implemented `IMasterKeyProvider.GetSnapshot()` (additive; existing
implementations keep working, their key is copied, never zeroed in place); `MasterKeyProviderBase`
resolves once and derives the id from that key. **Stored data is unchanged** — a golden row written by
the 0.6.5 cipher is pinned by a test. `MasterKeyProviderBase.ResolveKeyBytes` must return a fresh
buffer (it is zeroed after use); every shipped provider already did.
- **Store-path rules compare locations, not spellings** (scadaproj#45, Secrets-005). Rooted now means
fully qualified (Windows `C:secrets.db` / `\secrets.db` are rejected), and the under-content-root rule
also resolves symlinks/junctions, `\\?\` / `\\.\` prefixes, loopback admin shares and 8.3 names.
**Behaviour change:** a store that reaches the content root through a link — including macOS's
`/var` → `/private/var` — now fails startup validation where it used to pass.
- **Resolver cache** (scadaproj#45, Secrets-006): a resolve in flight across an `Invalidate` no longer
re-caches the old plaintext for a full TTL, expired entries are evicted (on lookup and by a periodic
sweep), and a non-positive TTL caches nothing. `DefaultSecretResolver` is now `IDisposable`.
- **Hub push sweep is isolated per row** (scadaproj#45, Secrets-008): one row the hub refuses (a
>450-char name) no longer stops every later name from being pushed; it is logged (EventId 4) and retried.
- **Malformed `${secret:}` references fail closed** (scadaproj#45, Secrets-009): `${secret:}`, a blank
name, an unterminated `${secret:name` or a nested marker throws `FormatException` naming the key
(never the value) instead of passing the literal through as the credential. **Behaviour change** for a
host whose configuration holds such a typo: it now fails at startup. The README sample no longer uses a
relative `SqlitePath`.
- **Akka announce carries a sender; anti-entropy is chunked** (scadaproj#45, Secrets-011/012). The
periodic announce is published from the actor's mailbox (it lost its sender under a genuinely async
store, so peers replied to deadLetters); a failed manifest read is logged. Pushed rows, pull replies and
pull requests are split into ≤64 KB messages, so a cold node catches up past Akka.Remote's 128000-byte
frame. The announce itself is not split (see `docs/operations/clustered-secrets.md`).
- **UI** (scadaproj#45, Secrets-014/015/016): "Add secret" refuses a name that already exists (use
Rotate) and restores a deleted one only on a second submit; Reveal **fails closed** — no value is shown
unless its `secret.reveal` audit was written, and the failure is logged; `RevealButton`, `SecretEditor`
and `ConfirmDeleteModal` re-check their own policy (`RevealPolicy` / `ManagePolicy`) and refuse without
it, instead of relying on the host's `AuthorizeView`.
- **Bundle import validates the whole bundle first** (scadaproj#45, Secrets-018): a malformed or tampered
entry anywhere aborts with nothing written; a store fault mid-apply names the rows already written.
- **Hub-call cancellation is cancellation** (scadaproj#45, Secrets-019): the gRPC hub channels set
`ThrowOperationCanceledOnCancellation`, so a shutdown mid-sweep is no longer recorded in
`ISecretsHubSyncStatus` as a failure, and the failover reader no longer dials every fallback with a
cancelled token.
- **The CLI's shipped `appsettings.json` no longer sets a relative `SqlitePath`** (scadaproj#49), which
failed validation; the per-user default applies unless the working directory's `appsettings.json` or
`Secrets__SqlitePath` says otherwise.
### Documentation
- scadaproj#49: the plain-`IConfigurationRoot` reload gap is documented, not fixed (README "Reload"); no
family host is exposed. `clustered-secrets.md` notes that OtOpcUa's own `SecretReplicationStarter` is
redundant once it adopts 0.6.6, and documents the Akka message-size bounds.
### Changed
- **CLI `set`/`rotate` take the value on stdin or a masked prompt** (scadaproj#35, Secrets-017).
+1 -1
View File
@@ -4,7 +4,7 @@ Shared encrypted secret store + `${secret:}` resolution for the **ZB.MOM.WW SCAD
**Backlog / open work:** lives in Gitea issues on `dohertj2/scadaproj` (`tea issues list --repo dohertj2/scadaproj`) — currently #7 (ignitionoee router pins), #8 (Replicator.Grpc / `0.6.3` fate), #18 (migrator retry jitter). The history below is a record, not a status tracker; file new follow-ups as issues.
**`0.6.6` — built, NOT published (2026-11-09, scadaproj#30–#35):** six scadaproj#15 review fixes — see `CHANGELOG.md` and `../code-reviews/Secrets/findings.md` (Secrets-001/002/003/010/013/017).
**`0.6.6` — built, NOT published (2026-11-09, scadaproj#30–#35, #45, #49):** every scadaproj#15 review finding resolved (Secrets-001–019; 0 open) plus the #49 follow-ups (CLI default `SqlitePath`, plain-root reload gap documented) — see `CHANGELOG.md` and `../code-reviews/Secrets/findings.md`. Behaviour changes worth knowing before adopting: store paths through symlinks into the content root now fail validation, malformed `${secret:}` tokens fail startup, UI Reveal fails closed on audit failure.
**`0.6.5` — built, NOT published (2026-11-09, scadaproj#26/#27):** `Microsoft.Data.Sqlite` 10.0.7 -> 10.0.13 (efcore#39009 pool fault) with `SQLitePCLRaw.lib.e_sqlite3` pin 2.1.12 -> 2.1.13; Windows test fixes. See `CHANGELOG.md`.