diff --git a/maintenance/librechat-key-feed.md b/maintenance/librechat-key-feed.md new file mode 100644 index 0000000..ce155dd --- /dev/null +++ b/maintenance/librechat-key-feed.md @@ -0,0 +1,52 @@ +# LibreChat key feed (`/admin/librechat/keys`) — design decision (audit M2) + +Audited 2026-11-05. Status: **kept as-is, with monitoring**; a per-member +fetch was considered and deliberately rejected. + +## What the route is + +`GET https://portal.inference.coop/admin/librechat/keys`, authed by a single +static token (`LIBRECHAT_SYNC_TOKEN`, held by the portal and the LibreChat +sandbox app). It returns every ACTIVE member's decrypted chat key so the +sandbox's in-app key-sync (`librechat_key_sync`, every 10 min inside the +LibreChat app; `sync-keys.js`) can seed LibreChat's per-user key store — +members never see an API-key prompt, and chat bills to each member's own +LiteLLM team/budget. + +## Blast radius (why the audit flagged it) + +The sync token unlocks every member's chat key in one response. Anyone holding +the token (sandbox compromise, token leak from the sandbox env) reads the whole +membership's keys. It is a deliberate exception to the admin token — members' +key storage is decrypted in the portal only for this feed and for serving a +member's own dashboard. + +## Why a per-member fetch was rejected (the riskier path) + +The sandbox is the ONLY consumer, and it does not know which member it is +seeding at the API layer — it seeds ALL members in one pass from one identity. +Scoping the feed per member means changing the sandbox's identity model +(per-member credentials or a member list with per-member auth), touching +`sync-keys.js` and the seeding loop. A wrong change there stops key seeding +for every member at once — the exact regression the audit order forbids +("nothing breaks"). The feed also runs on a fixed cadence, so the practical +exposure window is bounded by how fast misuse would show. + +## What is in place instead (2026-11-05) + +1. **Access logging on the route**: every fetch logs a WARNING line + `librechat key feed served: N member key(s) to (ua=…)`. Count only — + never key material, never member emails (the portal's log redactor strips + emails anyway). +2. **Daily-digest watch** (cron job 95e5b8fece86, §7 anomaly list): the + sandbox polls ~every 10 min (~144 serves/day expected). The digest counts + `librechat key feed served` lines and flags (a) a rate far above that + cadence, or (b) any 401 burst on the route — either is a suspected token + misuse and becomes a decision item. + +## What a real fix looks like (if ever needed) + +Move the sandbox to fetch per member: a new portal route keyed by the member +identity the sandbox already knows at seed time, plus a sandbox-side change to +seed from that route. Only worth doing when the sandbox is already being +modified for another reason — never as a standalone risky change.