Commit Graph

44 Commits

Author SHA1 Message Date
Joseph Doherty 779f8a8d78 feat(secrets-cli): deployment seeding flow — audit ${secret:} refs and fill the gaps 2026-07-19 09:43:17 -04:00
Joseph Doherty fe3ea684f5 feat(secrets-cli): CRUD flows for the interactive console 2026-07-19 09:37:46 -04:00
Joseph Doherty 138e2c1006 fix(secrets-cli): bundle import — verbatim ApplyReplicated writes, format gate, entry-aware errors 2026-07-19 09:35:39 -04:00
Joseph Doherty 4576f73c4c fix(secrets-cli): KEK doctor — fresh-row verdicts, degraded rewrap guard test, vanished-row visibility 2026-07-19 09:33:53 -04:00
Joseph Doherty e60f2d5dad fix(secrets-cli): reference auditor — InvalidName containment, comment-key parity, name normalization 2026-07-19 09:27:13 -04:00
Joseph Doherty 21f6a0df92 fix(secrets-cli): shell — contain UnauthorizedAccess, flow-title guard, cancel-path test 2026-07-19 09:26:04 -04:00
Joseph Doherty e49496c856 feat(secrets-cli): interactive shell skeleton — target picker, degraded-KEK upgrade, flow dispatch 2026-07-19 09:16:06 -04:00
Joseph Doherty 6255409f16 feat(secrets-cli): ciphertext-only bundle export/import with LWW + cross-KEK rewrap 2026-07-19 09:14:43 -04:00
Joseph Doherty f0f2b23d03 feat(secrets-cli): KEK doctor — per-row diagnosis + guided rewrap remedy 2026-07-19 09:11:35 -04:00
Joseph Doherty 38d33cf4b7 feat(secrets-cli): reference auditor — classify every ${secret:} the target app needs 2026-07-19 09:11:21 -04:00
Joseph Doherty 9bc1e5852e fix(secrets-cli): degrade on malformed master-key source + topology note 2026-07-19 09:08:24 -04:00
Joseph Doherty 83af2b2eaf feat(secrets-cli): per-target session factory with degraded-KEK mode + literal KEK provider 2026-07-19 09:01:46 -04:00
Joseph Doherty b8c6c7d432 test(secrets-cli): clean up temp dirs in TargetConfigReaderTests 2026-07-19 08:54:54 -04:00
Joseph Doherty b9a57fe06e feat(secrets-cli): target config reader — operate on the app's own composed Secrets config 2026-07-19 08:50:52 -04:00
Joseph Doherty 1b013a56a7 feat(secrets-cli): recent-targets store for the interactive console 2026-07-19 08:50:03 -04:00
Joseph Doherty fe9d8865fe feat(secrets-cli): wire Spectre.Console + SqlServer store refs for interactive console 2026-07-19 08:48:22 -04:00
Joseph Doherty 62ab8f68d1 fix(secrets): ship the delete modal's own styles under collision-proof class names (0.2.3)
Fixes scadaproj#2. ConfirmDeleteModal used the bare `modal` class and the
Secrets.Ui RCL shipped no CSS at all, silently depending on the host having
no opinion about that selector. Bootstrap 5 does: `.modal { display: none }`
made the delete modal permanently invisible on every Bootstrap host
(ScadaBridge, OtOpcUa, HistorianGateway — confirmed live on the first two)
while every @onclick handler kept working. Only MxGateway, the lone
Bootstrap-free host, could ever have rendered it.

Fix, entirely inside the RCL so a package bump repairs every host with no
host-side changes:
- class vocabulary renamed to zb-secrets-modal / -backdrop / -card / -title
  so no host framework selector can match the elements;
- the component emits its own <style> block alongside the markup. Scoped
  .razor.css was deliberately NOT used: three of the four family hosts never
  link a scoped-CSS bundle, so isolation CSS would silently fail to load —
  the exact defect class being fixed. Theme tokens with fallbacks keep the
  card legible even on a host without the kit stylesheet.
- data-testids unchanged, so existing Playwright gates and bUnit tests keep
  their selectors.

New bUnit pins: no Bootstrap-reserved class name appears in the rendered
markup, and the component ships a style block that both positions the
overlay and gives it a display mode. Suite 182 pass / 0 fail / 0.2.3.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-19 01:08:08 -04:00
Joseph Doherty 51bef634d0 test(secrets): make the context-after-await pin deterministic (fixes flake)
ActorContextAfterAwaitTests asserted that a ConfigureAwait(false)
continuation on the shared thread pool always throws NotSupportedException
when reading Self/Context. That property is a timing accident, not an Akka
guarantee: pool threads are exactly where Akka and the TestKit legitimately
install the [ThreadStatic] actor cell during mailbox runs and async
continuations, and Akka 1.5.62 has two non-throwing states besides — a
cleared cell makes ActorBase.Context return null (NullReferenceException on
.Self, not NotSupportedException) and ActorBase.Self return _clearedSelf
without any throw. Under parallel suite load the assertion failed once at
exactly that seam (2026-07-18); ironically the test's own doc comment said
the behaviour "does not reliably throw" and then asserted reliability.

The illegal reads now run on a dedicated new thread (LongRunning), the one
place the no-context state is guaranteed, while the realistic
ConfigureAwait(false) escape from the mailbox is kept. If the reads ever
unexpectedly succeed again, the failure message reports whose context the
thread was carrying. Verified 3 consecutive full-project runs green (38/38)
after 8 instrumented runs hunting the original repro.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-18 14:54:30 -04:00
Joseph Doherty a1df7ef26c fix(secrets): root-cause + fix the Akka replicator's hosted-process DI deadlock (0.2.2)
Closes the defect in scadaproj#1. The hang was never Akka: the package's DI
wiring closed a circular singleton dependency the container cannot see through
factory lambdas — ISecretStore (ReplicatingSecretStore decorator) ->
ISecretReplicator -> SecretReplicationActorProvider -> ISecretCacheInvalidator
-> DefaultSecretResolver -> ISecretStore. Resolution recurses around the loop
until MS.DI's StackGuard hops it onto a fresh thread-pool thread, which then
blocks forever on a singleton call-site lock the first thread still holds:
a silent permanent hang instead of a stack overflow. Managed stacks from
dotnet-dump show the repeating cycle and both parked threads; both candidate
causes in the issue (DistributedPubSub.Get vs the Lazy lock, missing
Akka.Cluster.Tools HOCON) are disproven — the actor constructor was never
reached, and the deadlock reproduces on a single non-clustered node.

Fix: defer the one cycle-closing edge. The provider now gets a
DeferredSecretCacheInvalidator that resolves the real invalidator on first
eviction — which only happens when a replicated row is applied, strictly after
graph resolution. Severing the edge instead is wrong: a null-invalidator
experiment ran the live gate at 5/6, with deleted secrets still resolving on
the peer. The SqlServer package never had the cycle (its replicator chain
never touches the invalidator), which is why the hub gate always passed.

Verified: live 2-node convergence gate now 6/6 (was: infinite hang), including
the delete-visibility check that proves the deferred invalidator really evicts.
New HostedProcessResolutionTests builds the graph as a host does (container-
registered ActorSystem, hosted services, watchdogged resolves) and fails on
0.2.1; DeferredSecretCacheInvalidatorTests pins the wrapper contract. Full
suite 180 passed / 0 failed / 15 skipped (env-gated live SQL).

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-18 14:39:40 -04:00
Joseph Doherty 6e8d346670 fix(secrets): Akka replication was inert in 0.2.0 - registration order bug (0.2.1)
AddZbSecretsAkkaReplication called AddZbSecrets FIRST, which does
TryAddSingleton<ISecretReplicator, NoOpSecretReplicator>(). The package's own
TryAddSingleton<ISecretReplicator> therefore found a descriptor already present
and was silently discarded.

Consequence: ISecretReplicator resolved to the no-op sink, so every write
published into nothing; and because SecretReplicationActor is only spawned as a
side effect of constructing AkkaSecretReplicator, no actor was ever created
either. No exception, no log line - a cluster that reports healthy and silently
never converges. The worst available failure mode for a secrets store.

Fix: register ISecretReplicator BEFORE AddZbSecrets, matching what the SQL-Server
package already did. Found during OtOpcUa adoption (Task 6), which is the first
code that ever built a container around this extension.

Root cause of the gap: the SQL-Server package had a DI test asserting its
replicator type (AddZbSecretsSqlServerTests:58) and the correct order; the Akka
package had neither. Every Akka test exercised the actor, serializer and
reconciler in isolation - none built a container, so nothing could see it. This
is the third instance of the same defect class in this library (the inert
ISecretReplicator seam, the unregistered concrete SqliteSecretStore, and now
this), all of which share one cause: unit tests that never construct the DI graph.

Adds AddZbSecretsAkkaReplicationTests (5 tests) asserting registration at the
ServiceCollection level. Verified to discriminate: with the 0.2.0 order restored,
2 of the 5 fail; with the fix, all 5 pass. They assert descriptors rather than
resolving from a provider on purpose - resolving ISecretReplicator eagerly spawns
the actor, whose PreStart needs DistributedPubSub and therefore a joined cluster,
which would make the test hang rather than fail.

Full suite Release-green: 175 passed, 15 skipped, no new warnings.
2026-07-18 11:36:45 -04:00
Joseph Doherty 15ef1f32a8 fix(secrets): anti-entropy never ran — Self read after await in the replication actor
I dismissed this finding from the code review as a false positive, reasoning
that Akka's ActorBase caches Self in a field and that three passing
anti-entropy tests traverse the path. Both premises were wrong. Self resolves
through Context, which is [ThreadStatic], and throws NotSupportedException once
a continuation resumes on a thread-pool thread.

The tests passed because a local SQLite store usually completes await
SYNCHRONOUSLY, so the continuation stayed on the mailbox thread and the context
was still intact. Correctness therefore depended on store latency and
thread-pool timing: green here, broken under a slower or contended store, with
the only symptom a per-peer warning every announce interval while nodes
silently stopped converging. The live-broadcast fast path masked it further —
only the anti-entropy repair path was dead.

Captures self on the actor thread and passes it in. Adds
GenuinelyAsyncSecretStore to force the async path, a regression test that fails
on the unfixed code (20s timeout) and passes in 3s after, and
ActorContextAfterAwaitTests pinning the underlying Akka behaviour so the wrong
assumption cannot be made again. Audited every remaining Self/Sender access in
the actor.

170 pass offline / 184 with the live SQL suite / 1 skip / 0 warnings.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-18 04:42:16 -04:00
Joseph Doherty dd0a846b64 feat(secrets): cluster replication via SQL Server and Akka.NET (G-7, 0.2.0)
Secrets were per-node SQLite, so a secret written on one node was invisible to
the rest of a cluster. G-7's design resolved the "shared SQL store vs Akka
replicator" fork to build only the former; both are built here so the choice is
a deployment decision (availability vs partition tolerance) rather than a
library limitation.

Two new packages — ZB.MOM.WW.Secrets.Replicator.SqlServer (shared store, plus a
local-store-with-hub mode) and .Replicator.AkkaDotNet (peer-to-peer over
distributed pub/sub). Core gains ISecretsStoreMigrator, one shared
SecretLastWriterWins predicate so no two stores can disagree on a tie, the
transport-agnostic reconciler, and ReplicatingSecretStore — which closes a real
gap: nothing had ever called ISecretReplicator.PublishAsync, so the seam was
inert and local writes would not have propagated at all.

Verified 182 pass / 1 skip / 0 warnings, including 15 live tests against a real
SQL Server 2022 (the SQLite suite ported case-for-case, so any behavioural
divergence between the stores fails) and a 9-test in-process 2-node Akka
cluster over real remoting. A post-build review caught six defects, all fixed
and now covered: both replication modes could not resolve from the container
(no test had built one), an unbounded fetch that broke past SQL Server's
2100-parameter cap, a poison row that aborted the rest of its batch forever,
Enum.Parse on peer input that could restart the actor in a loop, null crypto
blobs crossing the trust boundary, and a silently dropped pull-read failure.

Packed at 0.2.0 and vulnerability-scanned clean; not yet published to the feed.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-18 04:08:23 -04:00
Joseph Doherty 5e9cbd3ecf fix(deps)!: pin patched SQLitePCLRaw in Secrets + Auth; publish Secrets 0.1.3, Auth 0.1.5
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
2026-07-18 03:14:25 -04:00
Joseph Doherty d82d3451e7 feat(secrets): build G-8 KEK rotation (RewrapAll); design+plan G-7 clustered replication
G-8 (KEK rotation) — built in ZB.MOM.WW.Secrets, lib 0.1.2->0.1.3:
- ISecretCipher.Rewrap(row, oldKek, newKek): re-wraps the per-secret DEK only
  (bodies never re-encrypted; revision/timestamps preserved -> invisible to
  cluster LWW). Fail-closed on wrong old-KEK id, wrong key bytes, and malformed
  wraps; DEK zeroed on all paths.
- ISecretStore.ApplyRewrapAsync(rewrappedRow, expectedCurrentWrappedDek):
  updates only the 4 wrap columns + kek_id, compare-and-swap on the current
  wrapped DEK so a concurrent set/rotate cannot corrupt a row (closes a
  review-caught TOCTOU).
- KekRotationService.RewrapAllAsync + RewrapReport: enumerate all rows incl.
  tombstones, idempotent/resumable skip-already-current, bounded CAS-retry,
  fail-closed on unknown/identical KEK.
- `secret rewrap-all` CLI verb: key material only via env-var name / file path,
  JSON counts report; README section + operator runbook.

Verified: full offline suite green (82 core + 15 UI, 0 regressions) + end-to-end
CLI smoke + adversarial crypto review (all 7 categories PASS; TOCTOU fixed).

G-7 (clustered replication) — designed + planned, no code:
- Fork resolved to build Option A (shared SQL-Server ISecretStore); Akka
  replicator ZB.MOM.WW.Secrets.Akka is a deferred phase-2. Design doc +
  executable plan + .tasks.json under docs/plans/2026-07-17-secrets-g7-*.

Tracking: components/secrets/GAPS.md + CLAUDE.md secrets row updated.
2026-07-17 02:55:43 -04:00
Joseph Doherty 9bd3c76d96 fix(secrets): ${secret:} expander skips comment keys (_-prefixed leaf) — 0.1.2
Live HistorianGateway boot caught that the expander scanned a '_secretsComment' appsettings
value documenting the ${secret:...} syntax, tried to resolve the literal example token, and
crashed boot. The family uses _*Comment keys pervasively, so skip any config key whose leaf
segment starts with '_' (the _comment convention). Functional keys never lead with '_'.
2026-07-16 04:16:25 -04:00
Joseph Doherty 76124590f0 fix(secrets-ui): admin role must be canonical 'Administrator' (case-sensitive) — 0.1.1
Reference-consumer (HistorianGateway) caught that SecretsAuthorization.AdminRole was
'administrator' (lowercase) while the family canonical role is CanonicalRole.Administrator.
ASP.NET role claims compare case-sensitively (Ordinal), so a real admin got 403 on the
secrets UI. Now nameof(CanonicalRole.Administrator).
2026-07-16 04:11:20 -04:00
Joseph Doherty 2c64683303 docs(secrets): package README + embed in nupkgs 2026-07-16 04:00:32 -04:00
Joseph Doherty e20812cbae chore(secrets): wire cache invalidation on rotate/delete, drop dead RevealEnabled, remove scaffold leftovers 2026-07-15 17:39:55 -04:00
Joseph Doherty 294f53b49a fix(secrets-ui): preserve metadata on rotate + audit unexpected mutation/reveal failures 2026-07-15 17:31:34 -04:00
Joseph Doherty 0ab276dac0 feat(secrets-cli): set/get/list/rm/rotate commands 2026-07-15 17:26:14 -04:00
Joseph Doherty 21556cc1a7 feat(secrets-ui): add/rotate/delete modal + audited gated reveal 2026-07-15 17:20:01 -04:00
Joseph Doherty d7f8ca455b feat(secrets-ui): RCL + authz policies + metadata-only secrets list 2026-07-15 17:10:40 -04:00
Joseph Doherty 7f1db1b214 feat(secrets): SecretsOptions + AddZbSecrets DI + migration hosted service 2026-07-15 17:05:30 -04:00
Joseph Doherty 8c4175d8ce fix(secrets): audit decryption failures in resolver (fail-loud, no plaintext) 2026-07-15 17:01:10 -04:00
Joseph Doherty 0bb553212a feat(secrets): ${secret:} config expander (fail-closed) 2026-07-15 16:58:34 -04:00
Joseph Doherty 79ebd14baa feat(secrets): default resolver with TTL cache + audit (never logs plaintext) 2026-07-15 16:55:24 -04:00
Joseph Doherty 10455ec53b feat(secrets): sqlite secret store (overwrite-in-place, tombstone, manifest, LWW) 2026-07-15 16:50:59 -04:00
Joseph Doherty e99a8731e9 feat(secrets): sqlite connection factory + schema v1 + migrator 2026-07-15 16:46:15 -04:00
Joseph Doherty 1c13b4af99 feat(secrets): pluggable master-key providers (env/file/dpapi) + factory 2026-07-15 16:43:10 -04:00
Joseph Doherty 01911508b9 fix(secrets): harden SecretName (reject rooted paths, fail-fast on default) + keep SecretDecryptionException sealed 2026-07-15 16:37:21 -04:00
Joseph Doherty b784c7117f feat(secrets): AES-256-GCM envelope cipher with AAD binding 2026-07-15 16:34:46 -04:00
Joseph Doherty 16b32257f5 feat(secrets): abstractions seam interfaces 2026-07-15 16:30:19 -04:00
Joseph Doherty d78b533045 feat(secrets): abstractions value types, enums, exceptions 2026-07-15 16:28:11 -04:00
Joseph Doherty 824facab39 feat(secrets): scaffold ZB.MOM.WW.Secrets solution + 4 projects 2026-07-15 16:24:38 -04:00