fix(host): follow an in-place site PSK rotation without a central restart (ScadaBridge#96)

SitePskProvider cached each site's gRPC preshared key forever; only site removal
invalidated it, so rotating SB-GRPC-PSK-{site} broke the central<->site link on
both central nodes until they restarted. The cached key is now dropped when:

- the secret is written through /admin/secrets: central replaces the secrets
  library's ISecretCacheInvalidator with SitePskRotationInvalidator, which also
  publishes SitePskInvalidated on a new DistributedPubSub topic
  (site-psk-invalidation) so the peer central node drops its copy and its
  resolver cache entry too (SitePskInvalidationSubscriber);
- a site rejects a central->site call with PermissionDenied/Unauthenticated:
  WithSiteCredentials wraps every per-site channel's handler in
  PskRejectionRefreshHandler, so the next call re-reads the key (covers
  out-of-process writes such as the secret CLI);
- a site->central call presents a token that does not match the cached key:
  CentralControlAuthInterceptor invalidates and re-reads once before refusing;
- ScadaBridge:Communication:SitePsks reloads.

Invalidation is case-insensitive because SecretName lowercases names. Rotation
runbook (topology-guide), Component-Communication/Host and the #85 cache audit
(D2) updated.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 14:27:14 -05:00
parent 7f479f3a5c
commit 3a31dd2de7
15 changed files with 666 additions and 26 deletions
@@ -411,6 +411,10 @@ try
// options validation) and why the connection string can never itself be a ${secret:}
// reference.
builder.Services.AddScadaBridgeSecrets(builder.Configuration, SecretsNodeRole.Central);
// An in-place rotation of a site's SB-GRPC-PSK-{siteId} through /admin/secrets must drop
// central's cached copy of that key on BOTH central nodes (Gitea #96). Wraps the library's
// ISecretCacheInvalidator; the peer half is wired in AkkaHostedService.RegisterCentralActors.
builder.Services.AddSitePskRotationInvalidation();
// Secrets UI authorization: adds the named policies secrets:manage + secrets:reveal
// (role-based) consumed by the /admin/secrets page. AddSecretsAuthorization only ADDS
// these two policies via Configure<AuthorizationOptions> — it composes additively with