# Rotating Secrets Engine


Service-to-service authentication does not rely on a static shared secret.
The BE runs a **`RotatingSecretEngine`** (SDK `secrets/rotating-secret.ts`)
that owns every s2s secret and rotates it on a timer.

## The slot model

Each secret lives in Redis as a slot `{ current, previous, rotated_at }`:

- **Rotation**: every `rotationMs` (default **60 min**) a fresh secret
  replaces `current`; the old one becomes `previous`. The Redis key expires
  after `rotationMs + graceMs`.
- **Grace window**: for `graceMs` (default **5 min**) after rotation,
  verifiers accept *both* `current` and `previous`
  (`validClientShieldSecrets`) — so an in-flight request signed just before
  rotation never fails.
- **Broadcast**: each rotation publishes the shielded `secret.rotated`
  event; consumers refresh their cached slot immediately instead of waiting
  for a miss.

## Serving secrets — governed, allowlisted

Consumers fetch a secret over the NATS `secret.get` correlation route. The
route is:

- **publisher-verified** — only registered services may call it
- **allowlisted** — `SERVED_SECRETS` in `modules/secrets/secret-engine.ts`
  declares exactly which `(scope, name)` refs exist; anything else is
  refused with an error log. A service can never fetch an arbitrary secret.

Currently served: `service_client_shield_secret` — the secret stamped on
every s2s call (`x-client-shield-secret` header) and verified by the
client-shield middleware on every inbound internal route.

## Fallbacks and failure modes

- The `config_entries` row `service_client_shield_secret` (type `secret`,
  `__ROTATED_BY_SECRET_ENGINE__` placeholder) is the **static fallback**
  only — it exists so the config loader can boot services before the
  engine's first rotation. It is never the production value.
- If Redis is unavailable the engine logs an explicit error and rotation
  simply does not happen — consumers keep accepting the last slot.
- The engine also keeps the BE's *own* outbound slot warm, so BE→US calls
  (proxy, NATS auth headers) always stamp `current`.

## Why rotation matters

A leaked s2s secret expires within the hour by design — no deploy, no
config change, no coordination. Combined with the allowlist and the
publisher verifier, the blast radius of a compromised service identity is
bounded to one rotation window.
