diff --git a/documentation/audits/permanent-gate-2026-10-01/EXIT-TEST.md b/documentation/audits/permanent-gate-2026-10-01/EXIT-TEST.md new file mode 100644 index 00000000..68faeadd --- /dev/null +++ b/documentation/audits/permanent-gate-2026-10-01/EXIT-TEST.md @@ -0,0 +1,52 @@ +# Permanent household gate with family accounts — EXIT TEST (written before the spike ran) + +Written 2026-10-01 ~19:30 UTC, before anything below was built or measured. Model: `audits/login-gate-2026-09-29/B/B-VERDICT.md`. +Operator ruling `09` §3 decision 63: option A (extend the box's own gate), measured before any build; the build waits for the +operator's go. + +## What is tested + +A gate that stands IN FRONT OF an app permanently (not only until its first setup), letting in only members of the +household's family list, each with their OWN login. Apps: **Grimmory** (e-book library; e-readers sync over OPDS / Kobo / +KOReader) and **MeTube** (yt-dlp web UI with NO login of its own — its fit verdict R-767 was "stop: no login at all"). + +## Venue and method (fences: the spike ships nothing) + +- Scratch guest **9202** only, through its traefik (`https://192.168.0.114` + Host). Grimmory and MeTube are started BY HAND + with `docker compose` from throwaway files in `/root/spike/` on 9202 (Grimmory from the held template in + `audits/new-apps-2026-10-01/wip/grimmory/`, MeTube from upstream's image) — never the live catalog, never a release. +- The gate is a **throwaway auth service** on 9202 (a small Go program in a container on `traefik-public`) answering traefik + `forwardAuth`, with a family list of two members, a sign-in page, a session cookie, logout, and a per-visitor lock that + reads the visitor by the SAME rule as controller v0.286 (`clientaddr.go`). Its source is kept in this folder as evidence. +- The tunnel hop is simulated as in Part A (a container at `172.16.253.2`); one outside address on the real tunnel is not + needed for the gate's logic, which is the same code. +- Everything is removed afterwards (containers, files, traefik labels, images). + +## Exit items — each PASS or FAIL, measured + +1. **A stranger reaches nothing** of Grimmory or MeTube: every path tried (the front page, the API, static files, an unknown + path, MeTube's socket.io websocket upgrade and its download/add API) answers the gate (302 to the sign-in for a browser + GET, 401 otherwise) and never the app. Pass = 0 app answers. +2. **A family member with their OWN login** (not the dashboard password) gets in; the session lasts **days** (cookie + lifetime ≥ 7 days, survives a gate restart); **logout** works (the old cookie is refused afterwards). +3. **A stranger's wrong guesses lock only the stranger**: after N wrong sign-ins from one visitor (through the simulated + tunnel, rotating a forged leftmost address), that visitor is refused; a family member from another address signs in at + once. Pass = both. +4. **Grimmory's e-reader paths** (OPDS, Kobo sync, KOReader sync) work for a faithful `curl` of the client through a per-app + PATH EXCEPTION — and on those paths **the app's own authentication stays in force**: a stranger there meets the app's + own 401 (not the app's data). Pass = the client answers 200 with its own credentials, the stranger 401. +5. **The family login cannot reach the box dashboard**: the gate cookie is host-only to the app host; the dashboard does not + accept it; the family password does not work at the dashboard login. +6. **Cost**, measured: the added time per gated request (ms, gated vs ungated, same request, median of ≥ 50); what happens + while the gate's answerer is DOWN (stop it: the gated app must be refused — closed, not open); the build's cost in + sessions (estimated from the spike's code size and what the controller already has). + +## Also answered (in the verdict) + +Where family accounts live and who manages them; one sign-in for all gated apps on the domain or per app; how +Radicale/Dawarich-style API clients are excepted; whether MeTube becomes publishable behind it. + +## Verdict rule + +The spike PASSES only if items 1–5 all pass. Item 6 is a cost, reported, not a pass/fail. A failed item is reported as +measured, with what a build would have to do about it.