- Shipped
- September 23, 2026 at 11:36 AM UTC
- Author
- Kamo
- Commit
- a3d6885
POST /convert-vector and WS /ws/pipeline took no auth at all — 4820f44 capped upload size and conversion concurrency but left the actual hole open pending a decision on how to authenticate a raw browser WebSocket, since neither a bearer token nor kamo-internal's own X-OTK header is an option there (a WebSocket cannot set custom headers, and a one-time key in the WS query string would land in Traefik/access logs). Traefik routes /vector-ws/* straight to this service, bypassing kamo-internal's APIService gateway entirely, so anyone who could reach this pod — which per **************** meant anyone on the internet — could run it out of CPU/memory for free, or use it as an anonymous image-conversion proxy. The fix: both callers are same-origin browser code on internal.<domain>, so the browser already sends the *** session cookie on every request to this service, same as it does to kamo-internal itself. services/session_auth.py checks that cookie exactly the way kamo-internal checks one server-side (its app/lib/ksemSessionShared.ts / sessionServer.ts / apiProxy.ts all agree on the shape): the cookie value IS the session id, valid only at 128 hex characters, and the session is valid when Redis has anything at "***" + that id — read-only, from the replica, with the same 5-attempt/2s retry kamo-internal's own routes use to tolerate replication lag. A Redis error counts as "not found" rather than waving the caller through. - POST /convert-vector: 401 before the upload is even read. - WS /ws/pipeline: accept() still happens (uvicorn only delivers a real WS close code — here 1008, policy violation — once the handshake has started; closing before accept collapses to a bare HTTP 403 with the code discarded), then closes immediately, before any frame is read. - k8s/configmap.yaml gets the same **************** kamo-internal's own configmap carries — this cluster's Redis has no ACL, so PASSWORD is empty everywhere it's consumed, not a literal standing in for a Secret that doesn't exist. - Never logs the cookie value or the session id (the Redis key IS the id, just prefixed). /health stays open, deliberately — nothing here changes it. Tests: test_session_auth.py. Mutation-checked by reverting main.py's two call sites locally: the HTTP-side rejection tests fail (the mocked converter gets called instead of skipped) and the WebSocket-side test hangs outright (nothing ever closes the connection) — both confirm the guard is load-bearing; restored after. Known gap, out of scope here: kamo-internal's local-dev path (NEXT_PUBLIC_IMAGE_PIPELINE_URL / localhost:8000) calls this service cross-origin, and neither logoVectorize.ts's fetch() nor useImagePipeline.ts's WebSocket sends credentials cross-origin by default, so local dev against a local vectorservice will now 401/1008 on both endpoints. Production is unaffected (same-origin).
