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:
@@ -557,8 +557,14 @@ One key **per site**, never one for the fleet: a compromised site must not yield
|
||||
key. And never share it with `LocalDb:Replication:ApiKey` — that authenticates the *pair partner*
|
||||
for database replication, a different trust relationship on the same listener.
|
||||
|
||||
**Rotation:** set the new value on both sides, then restart the pair (pairs restart together
|
||||
anyway — see above). **Upgrading to a build that has this gate requires seeding the key first**,
|
||||
**Rotation:** set the new value on both sides, then restart the site pair (pairs restart together
|
||||
anyway — see above; the site reads its key once, at boot). **Central needs no restart** (Gitea #96):
|
||||
writing `SB-GRPC-PSK-<siteId>` through `/admin/secrets` on either central node drops the cached key
|
||||
on both central nodes at once, and a key changed any other way (the `secret` CLI, a `SitePsks`
|
||||
edit) is picked up on the first call the site rejects, or the first site call central's own gate
|
||||
cannot match — within `Secrets:ResolveCacheTtl` (30 s) of
|
||||
the change. Expect the link to that site to fail between the two halves of the change; doing
|
||||
central first and the site straight after keeps that window short. **Upgrading to a build that has this gate requires seeding the key first**,
|
||||
including in the on-host `deploy/` overlays.
|
||||
|
||||
The key is a bearer token over plaintext h2c, so it is readable and replayable by anyone on the
|
||||
|
||||
Reference in New Issue
Block a user