Retroactively author 15 grouped detail docs covering the backend functional (feat/fix) commits made before the change ledger was established (fad48ff), closing the ledger's detail-doc axis for the pre-convention history. Each doc groups a feature's constituent commits, lists their SHAs with subjects, and carries a backfill note stating it was reconstructed from git history on 2026-07-07 and not independently re-verified (current tree green at9911b8c). Add a Detail docs section to INDEX.md linking every detail doc (the 6 existing + 15 backfill) to the commit(s) it covers, so a doc is findable from the index without a column on the auto-generated ledger table. Catch the table up with the missing9911b8crow. Scope: backend (Go/Java/K8s) only, per the ledger's stated convention that frontend/panel commits are the collaborator's UI work; non-functional commits (docs/style/chore/refactor) keep their table row without a dedicated detail doc.
2.9 KiB
felis-api security + robustness hardening (audit sweep) (ledger backfill)
- Type: fix — retroactive ledger entry
- Date: 2026-06-30 – 2026-07-01
- Area:
internal/api(login, request-id, listeners, SSE relays, quota/claim, MyServers),internal/operator - Commits:
7a51c1dfix(api): bound concurrent login bcrypt to shed CPU-pin floods (429auth_busybefore the compare; a cap, not a per-account lockout) — audit #2164ac44fix(api): validate inboundX-Request-Idbefore echo + audit persist (≤64 bytes, log-safe charset) — audit-integrityc6c0772fix(api): read/idle timeouts on all three listeners via anewAPIServerfactory (closes Slowloris viaReadHeaderTimeout;WriteTimeoutleft unset so SSE isn't severed) — audit #33c1d647fix(api): per-principal SSE stream cap (429too_many_streams) — audit #1, blast-radius boundd6e3189fix(api): per-write deadline on SSE relay to sever a stalled reader (the real leak close behind the cap) — audit #18f41a00fix(api): clear the SSE write deadline on return so it can't leak onto a reused keep-alive connection — audit #16368ab1fix(api):COALESCEthe MyServersownedflag so an ownerless row doesn't 500 the listing2a4a81bfix(api): don't burn the wake cooldown when refused at capacity9873904fix(operator): populateStatus.Playersfrom an RCONlistprobe (so the panel doesn't report 0/0)
- Tasks: #33 (wake cooldown), #34 (Status.Players), #41–#46 (audit #1–#4)
What it did
A hardening sweep across the API's abuse and robustness surface: bound the two unbounded CPU/goroutine amplifiers (concurrent bcrypt, per-principal SSE streams), close the SSE relay's real stalled-reader leak with a per-write deadline (and clear it so it can't leak onto a pooled connection), validate the caller-supplied request id before it reaches the audit trail, set listener timeouts to close Slowloris, and fix two functional bugs — the ownerless-row 500 and the wake cooldown burned on a capacity refusal.
Why
Each is a specific, demonstrated failure mode: a login flood pins every core in bcrypt; a
stalled SSE reader leaks a relay goroutine + its upstream kube-apiserver follow for the
life of the process; an unvalidated X-Request-Id is a CR/LF log-forgery vector. The
WriteTimeout-left-unset detail is load-bearing — a blanket write timeout would sever the
healthy long-lived console/build-log streams the platform depends on.
Backfill note. Reconstructed 2026-07-07 from the commit history. Each fix shipped a targeted test at its commit — notably
d6e3189/8f41a00use a deadline-awareResponseWriterthat fails closed if the guard is removed. The quota-claim TOCTOU (audit #4) is a documented KNOWN-LIMITATION (2c56d17), closeable only against a real Postgres. Not independently re-verified for this doc; current tree green at9911b8c.