docs: the secrets inventory — every server-side secret, blast radius, rotation (#550 Phase 0) (#639)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+119
−0
docs/secrets.md+119 −0
diff --git a/docs/secrets.md b/docs/secrets.md| new file mode 100644 |
| index 0000000..8864244 |
| --- /dev/null |
| +++ b/docs/secrets.md |
| @@ -0,0 +1,119 @@ |
| +# 🤖 Secrets inventory |
| + |
| +The authoritative list of every server-side secret in the system (issue #550 |
| +Part 12, operability item 11), with what each one authorizes, where it lives, |
| +its blast radius, and how to rotate it. Update this doc in the same PR that |
| +adds or retires a secret. |
| + |
| +**The boundary that matters most is what's *not* here.** User keypairs, thread |
| +keys, and the instance key are client-side only — the server never holds them, |
| +so they can't appear in this inventory. Every secret below is *auth plumbing*: |
| +compromising it lets an attacker impersonate infrastructure or vandalize |
| +state, but never decrypt a message. (The two scoped exceptions are bot |
| +identity keys and the Anthropic key, held by sidecars — machines that are |
| +deliberately *members*, never the server.) |
| + |
| +Operator-specific locations (which box, which env file, which 1Password item) |
| +live in `CLAUDE.local.md` and 1Password, not here. |
| + |
| +## Per-instance |
| + |
| +- **Admin token** (`LCHAT_ADMIN_TOKEN`) — authorizes roster management and the |
| + admin console; explicitly not a decryption key. Server stores only its |
| + hash. Lives in the instance's deployment env + 1Password. *Blast radius*: |
| + one instance's membership (add/remove members, invites) — not its content. |
| + *Rotate*: set a new token in the env and restart; saved admin links die, |
| + recover with `lchat admin-link`. |
| +- **Push relay token** (`LCHAT_PUSH_RELAY_TOKEN`) — bearer auth to the APNs |
| + relay. *Blast radius*: can send junk pushes as that instance. *Rotate*: |
| + mint a replacement via the relay's admin API, swap the env, restart, revoke |
| + the old — no Apple interaction needed. |
| +- **VAPID keypair** — Web Push self-identification, generated on first use |
| + and stored in the instance DB (`config` table). *Blast radius*: junk web |
| + pushes to that instance's subscribers. *Rotate*: delete the config rows and |
| + restart to regenerate; existing push subscriptions break and members |
| + re-subscribe on next visit. |
| + |
| +## Home |
| + |
| +- **Session key** (`LCHAT_HOME_SESSION_KEY`) — HMAC ceremony proofs + sealed |
| + WebAuthn session params (both keys derived from it with distinct labels). |
| + Cannot open any vault. *Blast radius*: forge a 5-minute ceremony proof — |
| + i.e. the destructive-op gate (device revocation, blob delete) for a user |
| + whose uid you already know; vault *content* stays ciphertext. *Rotate*: |
| + swap the env and restart; in-flight ceremonies fail once, harmless. Unset, |
| + home mints an ephemeral key per boot (self-hoster default). |
| +- **S3 backend credentials** (`AWS_*` when `LCHAT_HOME_STORE=s3`) — object |
| + access to the home bucket. *Blast radius*: read/overwrite vault ciphertext |
| + and WebAuthn public credentials; versioning keeps the last 5, so vandalism |
| + is recoverable. *Rotate*: standard IAM key rotation (mint second key, swap, |
| + delete first). |
| + |
| +## Release & update chain |
| + |
| +- **Server release signing key** — signs the release feed the whole fleet |
| + auto-updates from. **The highest-blast-radius secret in the system**: a |
| + forger ships code to every instance. Kept offline (1Password), used only at |
| + publish time via `LCHAT_RELEASE_KEY_FILE`. *Rotate*: instances verify |
| + against a pinned public key, so rotation is a two-step — ship an update |
| + (signed by the old key) that adds the new public key, then start signing |
| + with the new one. Treat loss-of-control as an incident, not a rotation. |
| +- **Sparkle EdDSA key** (Mac app updates) — same shape and severity for the |
| + Mac app population; public key pinned in `ios/project.yml`. Same two-step |
| + rotation via a dual-trusted interim release. |
| +- **App Store Connect API key** — TestFlight uploads + notarization. |
| + *Blast radius*: publish builds to the ASC account. *Rotate*: revoke + mint |
| + in ASC → Integrations. |
| + |
| +## Push relay |
| + |
| +- **APNs `.p8` signing key** — auth to Apple, not a decryption key; lives |
| + only on the relay. *Blast radius*: send pushes as the app (Apple sees the |
| + same ciphertext envelope regardless). *Rotate*: Apple allows two active |
| + keys — mint the new one in the developer portal, swap the relay env, |
| + restart, revoke the old. |
| +- **Relay admin token** — mints/revokes per-instance relay tokens. *Rotate*: |
| + swap the env and restart the relay; existing instance tokens keep working. |
| + |
| +## Bots & integrations |
| + |
| +- **`ANTHROPIC_API_KEY`** — sidecar env only; bot code never runs in the |
| + server process, so a server compromise doesn't reach it. *Blast radius*: |
| + spend on the Anthropic account. *Rotate*: mint at the Anthropic console, |
| + swap sidecar envs, restart sidecars (note: a server release alone never |
| + restarts sidecars). |
| +- **Bot identity keys** — the deliberate exception: real E2EE member keys, |
| + held in sidecar session state, able to decrypt exactly the threads the bot |
| + was added to. *Rotate*: there is no key rotation — remove the bot member |
| + (admin API) and re-provision a fresh identity via `add-bot`. |
| +- **Connect broker: admin token + provider OAuth client secrets (DNSimple, |
| + GitHub) + per-instance broker tokens** — the broker persists no OAuth user |
| + material, so the client secrets gate only the ability to *initiate* flows. |
| + *Rotate*: provider secrets at the provider dashboard + broker env swap; |
| + instance tokens via the broker admin API. |
| + |
| +## Provisioning & DNS |
| + |
| +- **DNS-01 token** (DNSimple, product account) — issues the wildcard cert |
| + and controls product DNS. *Blast radius*: DNS takeover of product domains = |
| + cert issuance for them; a top-tier secret. *Rotate*: mint a new account |
| + token at DNSimple, swap the router env, restart the router, revoke the old. |
| +- **Cloud provisioner: admin token** (mints caller tokens), **caller tokens** |
| + (e.g. home's), and **controld's root-only control token**. *Blast radius*: |
| + create spaces up to quota/capacity caps (the caps are the real defense). |
| + *Rotate*: admin/control tokens by env swap + restart; caller tokens via |
| + the cloud admin API. |
| + |
| +## AWS (from the #550 build-out) |
| + |
| +- **`dev-agent` access key** — AdministratorAccess on the dev account, for |
| + IaC; in 1Password. *Rotate*: IAM two-key rotation. To be replaced by scoped |
| + roles once the OpenTofu skeleton defines them. |
| +- **`litestream-fleet` access key** — object-rw on the fleet-backup bucket |
| + only, lives in the router box's env file. *Blast radius*: read/delete |
| + backup ciphertext — versioning (which this user cannot touch) keeps 14 days |
| + of noncurrent versions, so deletion is recoverable. *Rotate*: IAM two-key |
| + rotation + env swap + `systemctl restart lchat-litestream`. |
| +- **EC2 pool boxes carry no AWS keys at all** — bucket access rides instance |
| + roles; there is nothing to rotate or leak. This is the pattern all later |
| + boxes follow. |