eleven / messenger Public
π€ 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 withlchat 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 oflchat.dbnever carries a private key (docs/no-plaintext.md Β§ 2; pre-keyfile instances migrate the legacyconfigrows 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-providerLCHAT_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 userses-sender-production, dev twinses-sender-dev) β scoped toses:SendEmailfromsign-in@elevenmessenger.comonly. Blast radius: send mail as the sign-in address (phishing paper). Rotate: standard IAM two-key rotation. Note it shares theAWS_*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_*whenLCHAT_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
.p8signing 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 andlchat-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 ofLCHAT_STRIPE_SECRET_KEYwith 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) andLCHAT_STRIPE_PORTAL_CONFIG(a portal configuration id).
AWS (from the #550 build-out)
dev-agentaccess 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-jamesaccess 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 aninfra/run). Rotate: IAM two-key rotation + re-seal. Revoke: delete the IAM user β independent ofdev-agent.litestream-fleetaccess 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.