Client Shield & Rotating Secrets
Every internal call — HTTP or NATS — is stamped with a shared secret. That secret rotates every hour; a leaked one is useless within the rotation window. The SDK owns both sides: the consumer slot/cache and the engine the BE instantiates.
The shield secret — auth/client-shield-secret.ts
CLIENT_SHIELD_SECRET_REF— the canonical{scope:"global", name:"service_client_shield_secret"}ref.ClientShieldSecretSlot = { current, previous?, rotated_at }— the live copy each service keeps (setClientShieldSecretSlot/getClientShieldSecretSlot).currentClientShieldSecret(fallback?)— what to stamp on outbound calls (alwayscurrent, else the static fallback).validClientShieldSecrets(graceMs, fallback?)— what to accept on inbound:current+previouswhile still insidegraceMs.SECRET_DEFAULTS = { rotationMs: 60 min, graceMs: 5 min }.
The engine — secrets/rotating-secret.ts
RotatingSecretEngine (instantiated once, in the BE): writes the Redis
slot {current, previous, rotated_at} with TTL rotationMs + graceMs,
rotates on the timer, and broadcasts the shielded secret.rotated event.
SecretProvider / startSecretProvider is the consumer side: fetches the
slot once at boot over secret.get, subscribes to secret.rotated, and
keeps the local slot warm — all stamped outbound headers automatically use
current.
SECRET_SUBJECTS = the governed subjects (secret.get request/reply via
correlation, secret.rotated broadcast).
Serving is allowlisted — nats/service-shield.ts
secret.get is not a public request: the BE handler replies only to an
explicit SERVED_SECRETS list and each call passes verifyNatsPublisher
— the publisher must be a known, enabled, online service from the client
registry (ShieldRegistryLike, wired via setNatsShieldRegistry).
subscribeShielded(handler) wraps any pub/sub subscriber with the same
publisher verification — the standard for every lifecycle/config subject.
Outbound plumbing — http/internal-client.ts
internalFetch(url, init) is the s2s fetch: it injects the service's
identity headers (set via configureInternalClient(provider)) plus the
current shield secret — consumers like fetchDocsPack build on it so
every cross-service HTTP call is stamped correctly by construction.
resetInternalClient exists for tests.
Who does what
| Side | Code | Duty |
|---|---|---|
| BE (owner) | RotatingSecretEngine + SERVED_SECRETS allowlist + publisher verifier | generate, persist, rotate, serve, broadcast |
| US (consumer) | SecretProvider + slot + validClientShieldSecrets on inbound middleware | fetch at boot, refresh on secret.rotated, stamp outbound, accept current+grace inbound |
| Static fallback | config_entries.service_client_shield_secret | boot before first rotation / Redis down — never the production value |