# 🤖 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. The storage and recovery
scheme for these secrets — the sealed vault repo, and why it is deliberately
private — is `docs/secrets-vault.md`.

## 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 a 0600 sidecar keyfile next to the DB (`<db>.vapid`) —
  deliberately *not* in the DB, so hibernation's S3 replica of `lchat.db`
  never carries a private key (docs/no-plaintext.md § 2; pre-keyfile
  instances migrate the legacy `config` rows into the file on first boot).
  Corollary: a hibernated instance restored onto a *different* box loses the
  file and re-mints on wake — see *Rotate* for what that costs. *Blast
  radius*: junk web pushes to that instance's subscribers. *Rotate*: delete
  the keyfile 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, sign-in-method
  add/remove) 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).
- **Identity-anchor peppers** (`LCHAT_HOME_SIWA_PEPPER`,
  `LCHAT_HOME_EMAIL_PEPPER`, per-provider `LCHAT_HOME_<PROVIDER>_PEPPER`) —
  HMAC the provider's stable subject (or lowercased email) into the blind
  index that maps a sign-in anchor to an account; identity anchors can never
  decrypt. *Blast radius*: pepper plus a DB copy lets an attacker test
  whether a *known* email/Apple ID has an account (membership probing) —
  content stays sealed. *Rotate*: last-resort only — a new pepper orphans
  every existing anchor of that kind; users re-link via a remaining sign-in
  method.
- **SES sender key** (`AWS_*` on home when the email door is on; IAM user
  `ses-sender-production`, dev twin `ses-sender-dev`) — scoped to
  `ses:SendEmail` from `sign-in@elevenmessenger.com` only. *Blast radius*:
  send mail as the sign-in address (phishing paper). *Rotate*: standard IAM
  two-key rotation. Note it shares the `AWS_*` env with the S3 store below —
  an instance running both needs one IAM user carrying both scopes.
- **GitHub OAuth client secret** (`LCHAT_HOME_GITHUB_CLIENT_SECRET`) — the
  one sign-in door whose exchange home runs itself (github.go; the OIDC
  providers verify id_tokens and need no secret). *Blast radius*: initiate
  GitHub OAuth flows as the Eleven app — no stored user material to read.
  *Rotate*: regenerate at the GitHub OAuth app, swap the env, restart; the
  per-provider pepper (covered above) is untouched.
- **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 envs, plus (since the AI opt-in, #858)
  `/data/control.env`: controld's attach-bot step writes it into each fresh
  sidecar env it stages, so the provisioning daemon must hold it — bot code
  still never runs in the server process, and no *instance* env ever carries
  it. *Blast radius*: spend on the Anthropic account. *Rotate*: mint at the
  Anthropic console, swap sidecar envs + control.env, restart sidecars and
  `lchat-control` (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 + per-instance
  broker tokens** — one client id/secret pair per OAuth-capable service in the
  integrations registry (DNSimple under its legacy env names; every other
  service — GitHub, Stripe, HubSpot, … — via `LCHAT_OAUTH_<ID>_CLIENT_ID` /
  `_CLIENT_SECRET`). 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.

## Billing (Stripe)

- **Stripe secret key** (`LCHAT_STRIPE_SECRET_KEY`) — the **live** key the
  cloud provisioner uses for space-upgrade Checkout, the Billing Portal, and
  subscription webhook reconciliation. *Blast radius*: full API access to the
  Stripe account — customer emails and subscription state, refunds,
  cancellations. No message content (Stripe never sees the product), but the
  worst non-signing secret to lose. *Rotate*: roll the key in the Stripe
  dashboard (pick a grace window so the swap is zero-downtime), swap the cloud
  env, restart.
- **Stripe webhook signing secret** (`LCHAT_STRIPE_WEBHOOK_SECRET`) —
  authenticates Stripe's event posts to the cloud webhook endpoint. *Blast
  radius*: forge checkout/subscription events → mint or revoke space
  entitlements without payment; no access to the Stripe account itself.
  *Rotate*: roll the endpoint's signing secret in the Stripe dashboard, swap
  the cloud env, restart.
- **Stripe Connect platform key + webhook secret**
  (`LCHAT_STRIPE_CONNECT_KEY`, `LCHAT_STRIPE_CONNECT_WEBHOOK_SECRET`) —
  per-*instance* env powering paid access levels (member subscriptions via
  Stripe Connect). Deliberately tri-state: both unset → feature off; a partial
  config warns and stays off. *Blast radius*: the platform key can also act on
  connected accounts (sellers' Stripe accounts) within the platform's
  permissions; the webhook secret forges membership entitlements on that
  instance. *Rotate*: same shapes as the cloud pair above.
- **Enterprise Edition checkout key** (`LCHAT_STRIPE_ENTERPRISE_KEY`) — the
  cloud env's *separate* Stripe key for the elevenmessenger.com/enterprise/
  page (enterprise.go). Deliberately independent of
  `LCHAT_STRIPE_SECRET_KEY` with no fallback: it holds the **sandbox** key
  while real billing stays live, and the live flip is swapping this one
  value. *Blast radius*: whatever account the key belongs to — sandbox ≈
  none; if flipped live, same as the live key above. *Rotate*: roll in the
  matching Stripe mode's dashboard, swap the cloud env, restart.
- Kept alongside but **not secrets**: `LCHAT_STRIPE_PRICES` (the price book —
  price ids are public by the time Checkout renders) and
  `LCHAT_STRIPE_PORTAL_CONFIG` (a portal configuration id).

## 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.
- **`dev-agent-james` access key** — James's own AdministratorAccess user on
  the same account, same purpose (IaC/tofu runs), so each operator has a
  revocable identity and CloudTrail attribution. Sealed in the secrets vault
  (`aws-dev-agent-james.env`, bundled with the DNSimple token so one file
  covers an `infra/` run). *Rotate*: IAM two-key rotation + re-seal.
  *Revoke*: delete the IAM user — independent of `dev-agent`.
- **`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.
