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.
Record the OtOpcUa live wonder gate (item 2). A throwaway harness drove OtOpcUa's
real GalaxyDriverBrowser (secret: ApiKeySecretRef resolved via ISecretResolver) against
the real production MxGateway: dummy secret: ref -> MxGatewayAuthenticationException,
real metadata:read key -> CONNECTED + browsed the real Galaxy root (10 nodes). Temp key
minted via the dashboard, then revoked (functionally proven) + deleted; no prod key
touched; harness + scratch stores destroyed. All four apps now G-2..G-6 done + live-proven.
Record the OtOpcUa secrets adoption (G-2/G-4/G-5/G-6, the last app): FF-merged +
pushed to lmxopcua origin/master @872cf7e3. Update the components/secrets/GAPS.md
OtOpcUa bullet (execution record + the three verified plan deviations + the regression
the gate caught) and the CLAUDE.md secrets-component-table row. All four apps
(HistorianGateway, mxaccessgw, ScadaBridge, OtOpcUa) now adopt ZB.MOM.WW.Secrets.
The v3.0 dual-namespace rewrite (PR #472) moved anchors and reshaped the
build, so the plan is refreshed to stay executable: CPM correction (OtOpcUa
is on Central Package Management, not inline versions), OpcUaClient options
in .Contracts + GalaxyDriverBrowser in .Browser, DriverHostActor line
shifts, and appsettings/owner precision. Driver-secret flow itself was
untouched by v3.0, so the two-layer approach holds.
Update the OtOpcUa umbrella entry for v3.0: the single .../ns namespace is
replaced by .../raw (s=<RawPath>) + .../uns (s=<Area>/<Line>/<Equipment>/<Eff>),
single-source fan-out to both NodeIds, Organizes UNS->Raw, writes/HistoryRead via
either NodeId, multi-notifier alarms, EquipmentNodeIds retired. Flags the wire-
contract change as a pending ScadaBridge Data-Connection-Layer cutover.
Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
Add components/secrets/ (SPEC, realized shared-contract, code-verified
current-state for OtOpcUa/mxaccessgw/ScadaBridge, GAPS adoption backlog) and
register the Secrets row in CLAUDE.md + components/README.md. The
ZB.MOM.WW.Secrets lib is built + published 0.1.2 + reference-consumer-proven
(HistorianGateway, live vs the wonder historian); per-app adoption is the
tracked follow-on.
7th shared ZB.MOM.WW.* library: AES-256-GCM envelope-encrypted secret store
(ISecretStore/SQLite), pluggable IMasterKeyProvider, ${secret:name} config
resolution, ISecretResolver runtime cache, Blazor /admin/secrets management UI
on the Theme kit, and a headless 'secret' CLI. Published 0.1.2 to the Gitea feed;
HistorianGateway adopted as reference consumer (live-proven end-to-end against
the real wonder historian). 80 tests pass.
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 '_'.
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).
Full *.IntegrationTests sweep done 2026-07-15: every driver + server suite GREEN,
zero OtOpcUa production regressions (all fixture/harness/test rot). PRs #446-#452.
Cross-repo highlights: Host harness LDAP swapped bitnami/openldap:2.6 -> GLAuth
(PR #451, own :3894 container, unifies the family on GLAuth); deploy-E2E 'emulated-
SQL timing' was misdiagnosed -> missing FK-parent seed + SQL/LDAP host overrides
(PR #452).
R2-04 T13/T15 (PR #444) close the last behavior-affecting round-2
verification gate (S4 primary-gate default-deny, live-verified on the
2-node rig + in-process delivery-path test). S7.Cli phrase-scan test
fixed (PR #443). Round-2 remainder is now infra-gated only.
The R2-06 live gate found the historian's Int1 analog-tag creation path
is server-degenerate; filed gateway-side as historiangw #11 (documented
as an explicit exclusion, PR #16), routed back as lmxopcua #441.
OtOpcUa's HistorianTypeMapper now maps Boolean → Int2. Updates the
OtOpcUa entry: master tip 7f79cd59 → 38e21df2, live gate 6/6.
HistorianGateway's Later tier (roadmap 16-21) landed on main via a coordinated
two-PR merge (origin/main @ 6a24a30); update the umbrella index from the stale
'open PRs pending merge' framing to merged. Only the 1.0.0 TLS-default flip remains.
ApiKeyRecord/ApiKeyListItem gain a nullable ExpiresUtc (NULL = never
expires); ApiKeyFailure gains KeyExpired. ApiKeyVerifier rejects a key
whose ExpiresUtc is at-or-before now (inclusive, injected clock), before
the secret comparison. CreateKeyAsync gets an expiresUtc overload; rotate
preserves existing expiry. SQLite schema bumps to v3: a nullable
expires_utc column, added to fresh DBs via CREATE and to existing v1/v2
DBs via an idempotent guarded ALTER — donor v2 gateway-auth.db upgrades
in place, no key invalidated. Version 0.1.3 -> 0.1.4 (not yet published;
nuget push is human-gated). Consumers (HistorianGateway, mxaccessgw) bump
to 0.1.4 to set/enforce expiry. 146 Auth.ApiKeys tests pass.
histsdk is now owned/imported (not vendored) — retire the 'vendored/re-vendor'
language; add the Client.Cli smoke CLI and record origin/main @ 93c6051 (the
repo-wide XML-doc sweep).
The ScadaBridge entry showed a `_git_` placeholder in the Repo column; set it
to the actual remote (gitea.dohertylan.com/dohertj2/ScadaBridge) to match the
peer rows. Keeps the umbrella index in sync with the repo, per the reciprocal
propagate-cross-repo-changes rule now recorded in ScadaBridge's CLAUDE.md.
Record the new HistorianGateway/docker/ local container deployment in the
umbrella inventory: HistorianGateway table row (real wonder datasources,
login + gRPC API-key auth disabled, runtime-only image / VPN / isolated
compose project) + a publish+compose recipe in the per-project commands.