eleven / messenger Public

docs: the secrets inventory — every server-side secret, blast radius, rotation (#550 Phase 0) (#639)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 5c26ce0f90b0506bb8d32f2589fc8808d245ce56 parent 3190e89
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.