From 8cadacb553ba5003800eb6c861f766054b36c8c1 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Tue, 29 Sep 2026 17:19:43 +0200 Subject: [PATCH] Sign-up lock 2026-09-29 evening: decisions 48/49 outcomes, two locks, wanderer closable, R-714/715/716 closed, R-717/718 opened; STATUS, CONTEXT, report Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- CONTEXT.md | 8 + REPORT-signup-lock-2026-09-29.md | 73 +++++++ STATUS.md | 27 ++- .../architecture/01-topology-and-trust.md | 4 + .../architecture/09-update-architecture.md | 18 +- .../A/A1-deploy-locked.txt | Bin 0 -> 3706 bytes .../A/A2-first-admin-with-lock-on.txt | 10 + .../A/A3-native-lock-direct.txt | 8 + .../signup-lock-2026-09-29/A/A4-remove-2.txt | 21 ++ .../signup-lock-2026-09-29/A/A4-remove.txt | 27 +++ .../A/A5-native-lock-after-gate.txt | 15 ++ .../A/A6-window-native-homebox.txt | 4 + .../A/A7-native-lock-X2.txt | 7 + .../A/A8-window-closed-homebox.txt | 3 + .../signup-lock-2026-09-29/A/X1-deploy.txt | 14 ++ .../signup-lock-2026-09-29/A/X1-household.txt | 7 + .../signup-lock-2026-09-29/A/X1-press.txt | 4 + .../signup-lock-2026-09-29/A/X1-setup.txt | 7 + .../signup-lock-2026-09-29/A/X2-deploy.txt | 12 ++ .../signup-lock-2026-09-29/A/X2-household.txt | 6 + .../signup-lock-2026-09-29/A/X2-setup.txt | 7 + .../A/aftersetuppatch.py | 19 ++ .../signup-lock-2026-09-29/A/floor-0282.txt | 4 + .../signup-lock-2026-09-29/A/gatepatch.py | 31 +++ .../signup-lock-2026-09-29/A/probecomment.py | 12 ++ .../signup-lock-2026-09-29/A/signuppatch.py | 28 +++ .../audits/signup-lock-2026-09-29/A/spec.json | 170 +++++++++++++++ .../signup-lock-2026-09-29/A/wandererpatch.py | 35 +++ .../B/B1-case-check.txt | 7 + .../B/B1-tricks-X1-rerun.txt | 13 ++ .../signup-lock-2026-09-29/B/B1-tricks-X1.txt | 54 +++++ .../signup-lock-2026-09-29/B/B2-tricks-X2.txt | 49 +++++ .../B/B3-login-after-block.txt | 7 + .../B/B4-tricks-all-final.txt | 113 ++++++++++ .../B/B5-case-holes-caught-by-native.txt | 11 + .../B/B6-tricks-all-regex.txt | 113 ++++++++++ .../B/B7-login-after-regex.txt | 4 + .../B/B8-reset-and-share-not-blocked.txt | 8 + .../audits/signup-lock-2026-09-29/B/direct.sh | 6 + .../signup-lock-2026-09-29/B/direct2.sh | 2 + .../audits/signup-lock-2026-09-29/B/gt2.sh | 1 + .../audits/signup-lock-2026-09-29/B/hb.sh | 1 + .../audits/signup-lock-2026-09-29/B/native.sh | 17 ++ .../signup-lock-2026-09-29/B/native2.sh | 2 + .../audits/signup-lock-2026-09-29/B/reset.sh | 11 + .../audits/signup-lock-2026-09-29/B/ro.py | 121 +++++++++++ .../signup-lock-2026-09-29/B/ro_setup.py | 203 ++++++++++++++++++ .../audits/signup-lock-2026-09-29/B/tricks.py | 87 ++++++++ .../audits/signup-lock-2026-09-29/B/tx.sh | 1 + .../audits/signup-lock-2026-09-29/B/wd.sh | 6 + .../audits/signup-lock-2026-09-29/B/wd2.sh | 1 + .../audits/signup-lock-2026-09-29/B/wl.sh | 1 + .../audits/signup-lock-2026-09-29/B/wtr.sh | 6 + .../audits/signup-lock-2026-09-29/B/wtr2.sh | 8 + .../C/C1-demo-boxes-close-signup.txt | 33 +++ .../audits/signup-lock-2026-09-29/C/c.sh | 35 +++ .../D/D0-wanderer-pb-url.txt | 10 + .../D/D1-wanderer-deploy.txt | 3 + .../D/D2-wanderer-open.txt | 9 + .../signup-lock-2026-09-29/D/D3-remove.txt | 3 + .../signup-lock-2026-09-29/D/D4-deploy.txt | 3 + .../D/D5-wanderer-close-signup.txt | 10 + .../D/D6-wanderer-tricks.txt | 8 + .../D/D7-wanderer-tricks-2.txt | 4 + .../D/D8-wanderer-tricks-after-fix.txt | 12 ++ .../E/E1-press-before-setup.txt | 3 + .../E/E2-probe-before.txt | 3 + .../E/E3-open-by-probe.txt | 3 + .../signup-lock-2026-09-29/redproofs/RP25.txt | 9 + .../signup-lock-2026-09-29/redproofs/RP26.txt | 9 + .../signup-lock-2026-09-29/redproofs/RP27.txt | 9 + .../signup-lock-2026-09-29/redproofs/RP28.txt | 9 + .../signup-lock-2026-09-29/redproofs/RP29.txt | 10 + .../signup-lock-2026-09-29/redproofs/RP30.txt | 9 + documentation/backlog/OPEN-ITEMS.md | 8 +- 75 files changed, 1615 insertions(+), 21 deletions(-) create mode 100644 REPORT-signup-lock-2026-09-29.md create mode 100644 documentation/audits/signup-lock-2026-09-29/A/A1-deploy-locked.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/A2-first-admin-with-lock-on.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/A3-native-lock-direct.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/A4-remove-2.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/A4-remove.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/A5-native-lock-after-gate.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/A6-window-native-homebox.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/A7-native-lock-X2.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/A8-window-closed-homebox.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/X1-deploy.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/X1-household.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/X1-press.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/X1-setup.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/X2-deploy.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/X2-household.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/X2-setup.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/aftersetuppatch.py create mode 100644 documentation/audits/signup-lock-2026-09-29/A/floor-0282.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/A/gatepatch.py create mode 100644 documentation/audits/signup-lock-2026-09-29/A/probecomment.py create mode 100644 documentation/audits/signup-lock-2026-09-29/A/signuppatch.py create mode 100644 documentation/audits/signup-lock-2026-09-29/A/spec.json create mode 100644 documentation/audits/signup-lock-2026-09-29/A/wandererpatch.py create mode 100644 documentation/audits/signup-lock-2026-09-29/B/B1-case-check.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/B/B1-tricks-X1-rerun.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/B/B1-tricks-X1.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/B/B2-tricks-X2.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/B/B3-login-after-block.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/B/B4-tricks-all-final.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/B/B5-case-holes-caught-by-native.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/B/B6-tricks-all-regex.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/B/B7-login-after-regex.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/B/B8-reset-and-share-not-blocked.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/B/direct.sh create mode 100644 documentation/audits/signup-lock-2026-09-29/B/direct2.sh create mode 100644 documentation/audits/signup-lock-2026-09-29/B/gt2.sh create mode 100644 documentation/audits/signup-lock-2026-09-29/B/hb.sh create mode 100644 documentation/audits/signup-lock-2026-09-29/B/native.sh create mode 100644 documentation/audits/signup-lock-2026-09-29/B/native2.sh create mode 100644 documentation/audits/signup-lock-2026-09-29/B/reset.sh create mode 100644 documentation/audits/signup-lock-2026-09-29/B/ro.py create mode 100644 documentation/audits/signup-lock-2026-09-29/B/ro_setup.py create mode 100644 documentation/audits/signup-lock-2026-09-29/B/tricks.py create mode 100644 documentation/audits/signup-lock-2026-09-29/B/tx.sh create mode 100644 documentation/audits/signup-lock-2026-09-29/B/wd.sh create mode 100644 documentation/audits/signup-lock-2026-09-29/B/wd2.sh create mode 100644 documentation/audits/signup-lock-2026-09-29/B/wl.sh create mode 100644 documentation/audits/signup-lock-2026-09-29/B/wtr.sh create mode 100644 documentation/audits/signup-lock-2026-09-29/B/wtr2.sh create mode 100644 documentation/audits/signup-lock-2026-09-29/C/C1-demo-boxes-close-signup.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/C/c.sh create mode 100644 documentation/audits/signup-lock-2026-09-29/D/D0-wanderer-pb-url.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/D/D1-wanderer-deploy.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/D/D2-wanderer-open.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/D/D3-remove.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/D/D4-deploy.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/D/D5-wanderer-close-signup.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/D/D6-wanderer-tricks.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/D/D7-wanderer-tricks-2.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/D/D8-wanderer-tricks-after-fix.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/E/E1-press-before-setup.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/E/E2-probe-before.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/E/E3-open-by-probe.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/redproofs/RP25.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/redproofs/RP26.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/redproofs/RP27.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/redproofs/RP28.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/redproofs/RP29.txt create mode 100644 documentation/audits/signup-lock-2026-09-29/redproofs/RP30.txt diff --git a/CONTEXT.md b/CONTEXT.md index f01f5452..34cab30a 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -16,6 +16,14 @@ > and holds nothing of its own; this file does hold its own content, namely the standing rulings below. +> **2026-09-29 evening — operator rulings 48 (wanderer stays, A) and 49 ("close sign-up now", A).** Controller +> **v0.282.0**: `after_setup:` (the app's own sign-up switch, env merged + one `compose up -d`, when the gate opens or on +> the press; the window lifts it, the loop restores it), `POST /apps//close-signup` (lock record `opened_by: +> close-signup`, never a gate), probes read list indexes + `done_status:`. Floor 0.282.0. Catalog `6446197`: 9 own switches +> (`SIGNUP_CLOSED`/`SIGNUP_OPEN`, compose default open), every `signup_block` `PathRegexp((?i)…)`, wanderer closable +> (decision 48 outcome), ghost/HA/gramps probes, new gate `check-probe-measured.py`. Pressed on demo-hp adventurelog + +> opengist, demo-felhom opengist. Open: R-717, R-718. Report: `REPORT-signup-lock-2026-09-29.md`. + > **2026-09-29 afternoon — operator ruling decision 47 (R-711 option A) + one CC-unattended decision (operator may > reverse):** sign-up is closed by a box-side block of the app's own sign-up address once the gate opens, with a > household 15-minute window (`internal/stacks/signup_block.go`, `.felhom.yml` `signup_block:` + `app_info.add_people`) diff --git a/REPORT-signup-lock-2026-09-29.md b/REPORT-signup-lock-2026-09-29.md new file mode 100644 index 00000000..2d0a6517 --- /dev/null +++ b/REPORT-signup-lock-2026-09-29.md @@ -0,0 +1,73 @@ +# REPORT — 2026-09-29 evening: sign-up locked twice; "close sign-up now"; wanderer closable; three more probes + +Architecture read first: `09` §3 decisions 45–49, `01-topology-and-trust.md` §5, `audits/gate-rollout-2026-09-29/`, +rows R-714, R-715, R-716. Controller **v0.282.0** (one release), floor 0.282.0, both demo boxes on it. Catalog +`6446197`. Evidence: `documentation/audits/signup-lock-2026-09-29/` (A own switch, B tricks, C demo boxes, D wanderer, +E probes, redproofs). + +## The Parts + +| Part | Step | State | Note | +|---|---|---|---| +| — | decisions 48, 49 recorded first | done | `09` §3 | +| A1 | own switch per app (spike) | done | 9 of 11 have an env switch (below); opengist, wishlist only in their database (R-717) | +| A1 | "can be set only after the first admin" | measured | 6 of 9 refuse the household's own first account while on; 3 (calcom, gitea, gramps-web) do not | +| A2 | `after_setup:` (controller) | done | env merged + one `compose up -d` when the gate opens; command form with after_install's argv rules; an old compose reported, not faked; retries every 30 min at most | +| A2 | live | done | all 9 apps: record `ok`, running container carries the switch, a stranger straight at the app refused | +| A3 | the window with an own switch | done — **lift and restore** | homebox: the window turned the switch off (one restart), a family member joined; after 15 min both locks back, a stranger refused through the web and straight at the app | +| B | trick table | done, **changed** | termix's router ignores case: `/users/CREATE` got past the old prefix block (its own switch refused it). All blocks now `PathRegexp((?i)…)`, slash-tolerant. Final: 113 tries on 11 apps, 0 got in, 106 refused by the block, 7 were vikunja's plain HTML page (its API blocked) | +| B | login / reset / sharing | done | household sign-in on 7 apps; password reset and share routes reach the apps, not the block | +| C | "close sign-up now" (controller) | done | lock record `opened_by: close-signup`, block, own switch; offered once; never a gate | +| C3 | demo boxes | done, **changed** | pressed on demo-hp adventurelog + opengist, demo-felhom opengist. "Before" proven WITHOUT making an account (the app answered an invalid sign-up with its own validation) — the apps' admin passwords are the operator's, so a test account could not be deleted through their admin pages. After: refused; login pages answer. adventurelog's backend restarted once (~30 s) for its own switch, same images (R-718: the card does not say so) | +| D | wanderer | done, **changed** | no gate: one DB URL for server and browser (measured), and no first-admin screen for a stranger (PocketBase's installer needs the log link). Closed by "close sign-up now": two holes found and closed (collection id `_pb_users_auth_`, collection name in capitals); proven on 9202 | +| E1 | probe reads lists / a done status | done | RP29, RP30 | +| E2 | ghost, home-assistant, gramps-web | done | before/after measured on fresh installs; each gate opened by itself within seconds of the setup; a press before the setup refused on all three | +| E3 | catalog gate `probe-measured` | done | 5 decoys seen failing; it caught immich/n8n/audiobookshelf (their note sat one block too high) | + +### Part A / B — one row per app + +| app | own switch | blocks the household's first account? | own switch live after the gate | tricks (8 shapes per route) | +|---|---|---|---|---| +| adventurelog | `DISABLE_REGISTRATION` | yes | on; `is_disabled: true` | 0 in | +| calcom | `NEXT_PUBLIC_DISABLE_SIGNUP` | no | on; "Signup is disabled" | 0 in | +| gitea | `GITEA__service__DISABLE_REGISTRATION` | no | on; "Registration is disabled" | 0 in | +| gramps-web | `GRAMPSWEB_REGISTRATION_DISABLED` | no | on; 405 "Registration is disabled" | 0 in | +| homebox | `HBOX_OPTIONS_ALLOW_REGISTRATION` | yes | on; "user registration disabled" | 0 in | +| papra | `AUTH_IS_REGISTRATION_ENABLED` | yes | on | 0 in | +| sparkyfitness | `SPARKY_FITNESS_DISABLE_SIGNUP` | yes | on | 0 in | +| termix | `ALLOW_REGISTRATION` | yes | on — and it caught the case hole | 0 in | +| vikunja | `VIKUNJA_SERVICE_ENABLEREGISTRATION` | yes | on | 0 in | +| opengist | none reachable (DB) | — | block only | 0 in | +| wishlist | none reachable (DB) | — | block only | 0 in | +| wanderer | `PUBLIC_DISABLE_SIGNUP` (web only) | — | on after the press | 0 in after the fix (2 holes before) | + +## Claims in the brief that turned out wrong (or right), named + +- **The three env names given from memory** — **right**: gitea `GITEA__service__DISABLE_REGISTRATION` (through its + env-to-ini), homebox `HBOX_OPTIONS_ALLOW_REGISTRATION`, papra `AUTH_IS_REGISTRATION_ENABLED` — each measured working. +- **"A native setting can be set only after the first admin exists"** — **true for 6 of 9**, **wrong for 3** (calcom, + gitea, gramps-web make their first admin by another route). +- **"The address block can be passed by case or encoding tricks"** — **right for case, wrong for encoding**: termix + (router ignores case) and PocketBase (collection name in any case, and by id) got past the old blocks; percent-encoding, + double slashes, trailing slashes and query strings never did (traefik decodes and cleans before matching). +- **"wanderer separates its internal and public database URL"** — **wrong**: one `PUBLIC_POCKETBASE_URL` for both. +- **"gramps-web answers 405 only after its setup"** — **right**: 200 with an owner token before, 405 after. + +## Also found + +- The scratch box's Docker disk was full of old images (1 GB free) — the install check refused correctly; 44 unused + images removed by name (no prune). +- `test_gate_decoys.py` had stopped running any case after docmost moved to PostgreSQL 18 (a typed "16"); fixed to read it. +- **My own slip:** I restarted the scratch box's controller while a removal job was running; one removal was cut off + (502). Re-run; nothing left behind. + +## Rows + +Closed: R-714, R-715, R-716. Opened: R-717 (opengist/wishlist own switch in their DB), R-718 (the close card should say +the app restarts). **Register 353 → 355 rows.** + +## Teardown + +Machines: 9202 — every test app removed through the product; no gate or block file left; back on the live catalog; the +drill catalog reset. Demo boxes — the floor, and the three "close sign-up now" presses (ruled). Host: nothing. Hub: floor +0.282.0. ep0: untouched. diff --git a/STATUS.md b/STATUS.md index 7aa4ec60..d56810b3 100644 --- a/STATUS.md +++ b/STATUS.md @@ -1,25 +1,22 @@ # STATUS — what works, what's broken, what's next -**Updated 2026-09-29 afternoon. Both demo boxes run controller 0.281.0 and host agent 0.137.0. Hub 0.125.0. New installs get golden 0.276.0 with agent 0.137.0.** +**Updated 2026-09-29 evening. Both demo boxes run controller 0.282.0 and host agent 0.137.0. Hub 0.125.0. New installs get golden 0.276.0 with agent 0.137.0.** -**Decisions today.** Yours: after an app's first admin exists, strangers can no longer sign up. Mine (you may reverse it): most apps have no switch the box can flip to close sign-up. So the box blocks just the app's sign-up address after the setup. The household can open it for 15 minutes from the app page, so a family member can join. +**Decisions today** (yours, recorded): wanderer stays, with its warning, until it is closed. Apps installed before the sign-up rule get a "close sign-up now" button. **What I did, and it worked.** -- **The gate is on 32 of the 34 apps where the first visitor would become the admin.** On a new install a stranger gets only the gate page. You, signed in to the dashboard, go straight to the setup. I tested every one on the scratch box. -- **9 apps open the gate by themselves** when their setup is done. The rest open with the "Done" button. -- **The "Done" button now asks the app first.** If the app says "not done", the button refuses. I tested it on zipline before its setup. -- **Strangers can no longer sign up after the setup.** 11 apps let anyone make an account; the box now blocks that. 11 more refuse strangers by themselves. The 15-minute window works and closes again by itself. -- **claper's password step is safe** for passwords with unusual characters. I tested one with a quote mark in it. -- **Apps already installed were not closed.** I checked this first, and again on both demo boxes after the update. +- **Sign-up now has two locks.** 9 apps have their own "no sign-up" setting. The box switches it on after your first account exists. The address block stays as the second lock. +- **The block resists address tricks now.** I tried 113 tricks on 11 apps (capital letters, extra slashes and similar). None got in. Two tricks worked before I fixed them. +- **"Close sign-up now" works.** I pressed it on the HP box's adventurelog and opengist, and on the N100's opengist. Strangers can no longer sign up there. adventurelog restarted once for about 30 seconds. +- **wanderer is closed too.** It cannot have the gate. You make your account, then press "close sign-up now". +- **ghost, home-assistant and gramps-web now open their gate by themselves** after the setup, like 9 other apps. +- **A new catalog check** stops a wrong "setup done" check from reaching the catalog. **What is not done.** -- **wanderer is not closed.** Its web part calls its own database through the public address, and a gate would block that call. So a stranger can still create its admin and sign up. It needs a small design change. -- **seerr, outline and rallly** are closed to strangers. I could not test the opening, because it needs a media server or e-mail. -- **3 apps have a "setup done" signal the box cannot read yet** (ghost, home-assistant, gramps-web). They use the button for now. +- **opengist and wishlist** keep their own "no sign-up" setting inside their database. The box cannot reach it yet, so they have the address block only. It held against every trick. +- **The "close sign-up now" card does not say the app restarts.** Small text fix, written down. -**Rows.** 3 opened, 3 closed, plus 1 for you. The list went from 350 to 353 rows. +**Rows.** 3 closed, 2 opened. The list went from 353 to 355 rows. **What needs you.** -1. **wanderer:** (A) keep it in the catalog until I fix it; its page now says plainly that anyone who finds the address can make an account (I recommend A; the fix is small). (B) Hide it from new installs until fixed. If you do nothing: A. -2. **Apps installed before today on the demo boxes** (adventurelog, opengist) still allow open sign-up. (A) Add a one-time "close sign-up now" button for installed apps. (B) Leave them; only the demo boxes have such installs. If you do nothing: B. -3. **D4, the image copies, the Peti leftovers:** unchanged. If you do nothing, nothing changes. +1. **D4, the image copies, the Peti leftovers:** unchanged. If you do nothing, nothing changes. diff --git a/documentation/architecture/01-topology-and-trust.md b/documentation/architecture/01-topology-and-trust.md index 752c225f..3c7ebcab 100644 --- a/documentation/architecture/01-topology-and-trust.md +++ b/documentation/architecture/01-topology-and-trust.md @@ -139,6 +139,10 @@ exists, open sign-up is closed; only the admin adds people, from the app's own u says so on its page. Mechanism (controller ≥ 0.281.0): the box keeps a small traefik router on the app's own sign-up address once the gate opens, answered "sign-up is closed" by the controller; the household opens it for 15 minutes from the app page to let a family member in. The controller is in THAT address's path only. +Since controller 0.282.0 there are two locks where the app has its own switch: the box also sets the app's own +"no sign-up" setting (`after_setup`), and the block matches any letter case and extra slashes. An app installed before +decision 47 gets both only when the household presses "Close sign-up now" (decision 49); wanderer, which is not gated, +the same way. --- diff --git a/documentation/architecture/09-update-architecture.md b/documentation/architecture/09-update-architecture.md index dc33405d..c17e657e 100644 --- a/documentation/architecture/09-update-architecture.md +++ b/documentation/architecture/09-update-architecture.md @@ -528,12 +528,24 @@ R-636's louder repeated alarm. refuse by themselves; the window let a family member in and closed again. wanderer cannot be gated yet (R-714). Evidence `audits/gate-rollout-2026-09-29/`. 48. **wanderer stays in the catalog, with its page's warning, until its gate ships** — *operator ruling 2026-09-29 - evening (R-714, option A).* Outcome: *(filled in by the session that ships it)*. + evening (R-714, option A).* **Outcome (2026-09-29 evening):** no gate — measured: wanderer has ONE database address + for its web server and the browser (no internal URL), so a gate would refuse its own server's calls; and it needs + none for the first admin (PocketBase's superuser installer needs the one-time link from the server log). Its door + is sign-up, in the web AND straight through PocketBase's API. Closed by the household's "Close sign-up now" (decision + 49's press, offered because wanderer is never gated): a case-insensitive block on `/register` and PocketBase's + `POST /api/collections/(users|_pb_users_auth_)/records` (the collection id and a letter-case change both got in + before the fix), plus `PUBLIC_DISABLE_SIGNUP`. Its first step now says: make your account, then close sign-up. 49. **"Close sign-up now" for apps installed before decision 47** — *operator ruling 2026-09-29 evening (R-716, option A).* The app page of an installed app whose template has a sign-up lock and whose install has none offers one press that applies exactly what a fresh install gets after its setup. The box never applies it by itself: a - catalog change never touches an installed app (Part 0 of 2026-09-29 stays the rule). Outcome: *(filled in by the - session that ships it)*. + catalog change never touches an installed app (Part 0 of 2026-09-29 stays the rule). **Outcome (2026-09-29 + evening, controller v0.282.0):** `POST /apps//close-signup` writes a lock record (`opened_by: close-signup`, + never a gate), the block, then the app's own switch (`after_setup`), which restarts the app once. Pressed on the + demo boxes (demo-hp adventurelog and opengist, demo-felhom opengist): before, each served its sign-up; after, every + sign-up answered "closed"; adventurelog's own switch went on (its backend restarted ~30 s, same images). + **Also 2026-09-29 evening (decision 47, second lock):** 9 of the 11 apps' OWN sign-up switch is set by `after_setup` + when the gate opens (6 of them also refuse the household's own first account, so never at install), and every block + is case-insensitive (termix's router ignores case). Final trick run: 113 tries on 11 apps, 0 got in. Same day, operator: CC changes the admin passwords of demo-hp's installed bookstack and calibre-web and stores them in the operator's credentials file (not in any repo). diff --git a/documentation/audits/signup-lock-2026-09-29/A/A1-deploy-locked.txt b/documentation/audits/signup-lock-2026-09-29/A/A1-deploy-locked.txt new file mode 100644 index 0000000000000000000000000000000000000000..1e449f700a0e1faf407a0a10152b25733645e8ca GIT binary patch literal 3706 zcmeH~&2EA~5XXD&Q_PJ^F?rDvp6k4eFZ}^;8_<=3-G>^H(v3?Cp1CBI_BuA9yL$uGyNADqyt6S|jG464M zoP_AbJS^Y`c2SIMxnjukObtq_dKuVRk;H4D=%;kM{8p;Z6&DmgP8n{M7f@J4d_2A? z18=LEqZ-beZiyVdA05J!aaKC46xx%Erqt5hBizqI^Qjrmez@BEh^=6P91d^ zu<{#HZ)}7`yRf!GY#MCJ*&IHoL7KZw~2`2H12=+8qNMZ Z+=!>)d-*FNO}aS^-_|cm&w3yqzJKEvBZdF~ literal 0 HcmV?d00001 diff --git a/documentation/audits/signup-lock-2026-09-29/A/A2-first-admin-with-lock-on.txt b/documentation/audits/signup-lock-2026-09-29/A/A2-first-admin-with-lock-on.txt new file mode 100644 index 00000000..5502b7b3 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/A2-first-admin-with-lock-on.txt @@ -0,0 +1,10 @@ +16:17:22 adventurelog HOUSEHOLD SETUP through the gate -> 200 {"type":"failure","status":403,"data":"[{\"message\":-1}]"} +16:17:23 calcom HOUSEHOLD SETUP through the gate -> 200 {"message":"First admin user created successfully."} +16:17:28 gitea HOUSEHOLD SETUP through the gate -> 200 after POST: 200 / +16:17:28 gramps-web HOUSEHOLD SETUP through the gate -> 201 +16:17:28 homebox HOUSEHOLD SETUP through the gate -> 403 {"error":"user registration disabled"} +16:17:28 papra HOUSEHOLD SETUP through the gate -> 400 {"message":"Email and password sign up is not enabled","code":"EMAIL_PASSWORD_SIGN_UP_DISABLED"} +16:17:28 sparkyfitness HOUSEHOLD SETUP through the gate -> 400 {"message":"Signups are currently disabled by the administrator."} +16:17:28 termix HOUSEHOLD SETUP through the gate -> 403 {"error":"Registration is currently disabled"} +16:17:28 vikunja HOUSEHOLD SETUP through the gate -> 404 {"message":"Not Found"} + diff --git a/documentation/audits/signup-lock-2026-09-29/A/A3-native-lock-direct.txt b/documentation/audits/signup-lock-2026-09-29/A/A3-native-lock-direct.txt new file mode 100644 index 00000000..0bd905d8 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/A3-native-lock-direct.txt @@ -0,0 +1,8 @@ +calcom POST /api/auth/signup -> 403 +gitea GET /user/sign_up -> 200 +gramps-web POST /api/users/strngr1/register/ -> 405 +Registration is disabled. Please contact your site administrator + +{"message":"Signup is disabled"} +{"error": {"code": 405, "message": "Registration is disabled"}} +64:DISABLE_REGISTRATION = true diff --git a/documentation/audits/signup-lock-2026-09-29/A/A4-remove-2.txt b/documentation/audits/signup-lock-2026-09-29/A/A4-remove-2.txt new file mode 100644 index 00000000..f44def55 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/A4-remove-2.txt @@ -0,0 +1,21 @@ +16:20:48 [X] stop -> 200 {'ok': True, 'message': 'Stack gitea stop completed'} +16:21:19 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'gitea', 'volumes_removed': [], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': 'Az alkalmazás nem tárolt saját +16:21:27 [X] after remove: deployed=False leftovers='/opt/docker/stacks/gitea' +16:21:28 [X] stop -> 200 {'ok': True, 'message': 'Stack gramps-web stop completed'} +16:21:59 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'gramps-web', 'volumes_removed': [], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': 'Az alkalmazás nem tárolt +16:22:07 [X] after remove: deployed=False leftovers='/opt/docker/stacks/gramps-web' +16:22:08 [X] stop -> 200 {'ok': True, 'message': 'Stack homebox stop completed'} +16:22:39 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'homebox', 'volumes_removed': [], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': 'Az alkalmazás nem tárolt saj +16:22:47 [X] after remove: deployed=False leftovers='/opt/docker/stacks/homebox' +16:22:48 [X] stop -> 200 {'ok': True, 'message': 'Stack papra stop completed'} +16:23:19 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'papra', 'volumes_removed': [], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': 'Az alkalmazás nem tárolt saját +16:23:27 [X] after remove: deployed=False leftovers='/opt/docker/stacks/papra' +16:23:28 [X] stop -> 200 {'ok': True, 'message': 'Stack sparkyfitness stop completed'} +16:23:59 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'sparkyfitness', 'volumes_removed': [], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': 'Az alkalmazás nem táro +16:24:07 [X] after remove: deployed=False leftovers='/opt/docker/stacks/sparkyfitness' +16:24:08 [X] stop -> 200 {'ok': True, 'message': 'Stack termix stop completed'} +16:24:39 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'termix', 'volumes_removed': [], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': 'Az alkalmazás nem tárolt sajá +16:24:47 [X] after remove: deployed=False leftovers='/opt/docker/stacks/termix' +16:24:47 [X] stop -> 200 {'ok': True, 'message': 'Stack vikunja stop completed'} +16:25:19 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'vikunja', 'volumes_removed': [], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': 'Az alkalmazás nem tárolt saj +16:25:28 [X] after remove: deployed=False leftovers='/opt/docker/stacks/vikunja' diff --git a/documentation/audits/signup-lock-2026-09-29/A/A4-remove.txt b/documentation/audits/signup-lock-2026-09-29/A/A4-remove.txt new file mode 100644 index 00000000..d424a8df --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/A4-remove.txt @@ -0,0 +1,27 @@ +16:18:29 [X] stop -> 200 {'ok': True, 'message': 'Stack adventurelog stop completed'} +16:19:01 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'adventurelog', 'volumes_removed': ['adventurelog_adventurelog_media', 'adventurelog_adventurelog_postgres_data'], 'hdd_paths_r +16:19:09 [X] after remove: deployed=False leftovers='/opt/docker/stacks/adventurelog' +16:19:20 [X] stop -> 200 {'ok': True, 'message': 'Stack calcom stop completed'} +16:19:52 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'calcom', 'volumes_removed': ['calcom_calcom_postgres_data'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': ' +16:20:01 [X] after remove: deployed=False leftovers='/opt/docker/stacks/calcom' +16:20:01 [X] stop -> 200 {'ok': True, 'message': 'Stack gitea stop completed'} +16:20:19 [X] remove (with drive data) -> 502 {'_raw': 'Bad Gateway'} +16:20:27 [X] after remove: deployed=True leftovers='/opt/docker/stacks/gitea' +16:20:38 [X] stop -> 200 {'ok': True, 'message': 'Stack gramps-web stop completed'} +16:21:09 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'gramps-web', 'volumes_removed': ['gramps-web_gramps_cache', 'gramps-web_gramps_db', 'gramps-web_gramps_index', 'gramps-web_gra +16:21:18 [X] after remove: deployed=False leftovers='/opt/docker/stacks/gramps-web' +16:21:18 [X] stop -> 200 {'ok': True, 'message': 'Stack homebox stop completed'} +16:21:50 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'homebox', 'volumes_removed': ['homebox_homebox_data'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': 'Az alk +16:21:58 [X] after remove: deployed=False leftovers='/opt/docker/stacks/homebox' +16:21:59 [X] stop -> 200 {'ok': True, 'message': 'Stack papra stop completed'} +16:22:31 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'papra', 'volumes_removed': ['papra_papra_data'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': 'Az alkalmazá +16:22:39 [X] after remove: deployed=False leftovers='/opt/docker/stacks/papra' +16:22:40 [X] stop -> 200 {'ok': True, 'message': 'Stack sparkyfitness stop completed'} +16:23:12 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'sparkyfitness', 'volumes_removed': ['sparkyfitness_sparkyfitness_backup', 'sparkyfitness_sparkyfitness_db_data', 'sparkyfitnes +16:23:21 [X] after remove: deployed=False leftovers='/opt/docker/stacks/sparkyfitness' +16:23:21 [X] stop -> 200 {'ok': True, 'message': 'Stack termix stop completed'} +16:23:53 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'termix', 'volumes_removed': ['termix_termix_data'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': 'Az alkalm +16:24:01 [X] after remove: deployed=False leftovers='/opt/docker/stacks/termix' +16:24:02 [X] stop -> 200 {'ok': True, 'message': 'Stack vikunja stop completed'} +16:24:33 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'vikunja', 'volumes_removed': ['vikunja_vikunja_data', 'vikunja_vikunja_db'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [ +16:24:42 [X] after remove: deployed=False leftovers='/opt/docker/stacks/vikunja' diff --git a/documentation/audits/signup-lock-2026-09-29/A/A5-native-lock-after-gate.txt b/documentation/audits/signup-lock-2026-09-29/A/A5-native-lock-after-gate.txt new file mode 100644 index 00000000..60ae1c3c --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/A5-native-lock-after-gate.txt @@ -0,0 +1,15 @@ +gitea: after_setup { at: "2026-09-29T14:29:30Z" ok: true } native_lock: applied app.yaml SIGNUP_CLOSED: "true +calcom: after_setup { at: "2026-09-29T14:29:48Z" ok: true } native_lock: applied app.yaml SIGNUP_CLOSED: "true +gramps-web: after_setup { at: "2026-09-29T14:29:30Z" ok: true } native_lock: applied app.yaml SIGNUP_CLOSED: "true +adventurelog: after_setup { at: "2026-09-29T14:29:45Z" ok: true } native_lock: applied app.yaml SIGNUP_CLOSED: "true +homebox: after_setup { at: "2026-09-29T14:29:48Z" ok: true } native_lock: applied app.yaml SIGNUP_OPEN: "false +running env: gitea true | calcom true | gramps-web true | adventurelog true | homebox false +stranger sign-up straight at the app (no traefik, no block): + gitea GET /user/sign_up (form or 'disabled') -> + 405 {"error": {"code": 405, "message": "Registration is disabled"}} + homebox POST /api/v1/users/register -> 403 {"error":"user registration disabled"} + adventurelog backend is-registration-disabled -> 200 {"is_disabled":true,"message":"Registration is disabled. Please contact the administrator if you nee +gitea GET /user/sign_up straight at the app -> Registration is disabled +64:DISABLE_REGISTRATION = true diff --git a/documentation/audits/signup-lock-2026-09-29/A/A6-window-native-homebox.txt b/documentation/audits/signup-lock-2026-09-29/A/A6-window-native-homebox.txt new file mode 100644 index 00000000..c698d7d4 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/A6-window-native-homebox.txt @@ -0,0 +1,4 @@ +16:31:55 homebox: household opens sign-up for 15 min -> 200 {'data': {'open_until': '2026-09-29T14:46:55Z'}, 'error': '', 'ok': Tr +16:32:07 homebox after the window press: the app's own switch in the running container = true | files=[] record={'state': 'open', 'since': '2026-09-29T14:26:31Z', 'hosts': ['r-homebox.enkisfelhom.hu'], 'opened_at': '2026-09-29T14:29:47Z', 'opened_by': 'household', 'signup_open_until': '2026-09-29T14:46:55Z', 'native_lock': 'lifted'} +16:32:07 a family member signs up inside the window -> 204 +16:32:07 app page during the window: restart line shown: False | open-until shown: True diff --git a/documentation/audits/signup-lock-2026-09-29/A/A7-native-lock-X2.txt b/documentation/audits/signup-lock-2026-09-29/A/A7-native-lock-X2.txt new file mode 100644 index 00000000..864482e0 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/A7-native-lock-X2.txt @@ -0,0 +1,7 @@ +papra: native_lock: applied SIGNUP_OPEN: "false | after_setup ok=ok: true +sparkyfitness: native_lock: applied SIGNUP_CLOSED: "true | after_setup ok=ok: true +termix: native_lock: applied SIGNUP_OPEN: "false | after_setup ok=ok: true +vikunja: native_lock: applied SIGNUP_OPEN: "false | after_setup ok=ok: true +opengist: | after_setup ok= +wishlist: | after_setup ok= +running env: papra false | sparkyfitness true | termix false | vikunja VIKUNJA_SERVICE_ENABLEREGISTRATION=false diff --git a/documentation/audits/signup-lock-2026-09-29/A/A8-window-closed-homebox.txt b/documentation/audits/signup-lock-2026-09-29/A/A8-window-closed-homebox.txt new file mode 100644 index 00000000..8949568b --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/A8-window-closed-homebox.txt @@ -0,0 +1,3 @@ +16:47:53 homebox after the window: its own switch in the running container = false | files=[signup-block-homebox.yml] record={'state': 'open', 'since': '2026-09-29T14:26:31Z', 'hosts': ['r-homebox.enkisfelhom.hu'], 'opened_at': '2026-09-29T14:29:47Z', 'opened_by': 'household', 'signup_open_until': '2026-09-29T14:46:55Z', 'native_lock': 'applied'} +16:47:53 stranger sign-up through traefik -> 403 {"error":"sign-up is closed on this app; its admin adds new +16:47:56 stranger sign-up straight at the app -> 403 diff --git a/documentation/audits/signup-lock-2026-09-29/A/X1-deploy.txt b/documentation/audits/signup-lock-2026-09-29/A/X1-deploy.txt new file mode 100644 index 00000000..afea3682 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/X1-deploy.txt @@ -0,0 +1,14 @@ +16:26:30 ghost deploy -> 202 +16:26:30 home-assistant deploy -> 202 +16:26:30 gramps-web deploy -> 202 +16:26:30 gitea deploy -> 202 +16:26:30 calcom deploy -> 202 +16:26:31 adventurelog deploy -> 202 +16:26:31 homebox deploy -> 202 +16:26:55 ghost state running | files=[setup-gate-ghost.yml] record={'state': 'closed', 'since': '2026-09-29T14:26:29Z', 'hosts': ['r-ghost.enkisfelhom.hu']} +16:26:59 home-assistant state running | files=[setup-gate-home-assistant.yml] record={'state': 'closed', 'since': '2026-09-29T14:26:30Z', 'hosts': ['r-home-assistant.enkisfelhom.hu']} +16:27:02 gramps-web state running | files=[setup-gate-gramps-web.yml] record={'state': 'closed', 'since': '2026-09-29T14:26:30Z', 'hosts': ['r-gramps-web.enkisfelhom.hu']} +16:27:06 gitea state running | files=[setup-gate-gitea.yml] record={'state': 'closed', 'since': '2026-09-29T14:26:30Z', 'hosts': ['r-gitea.enkisfelhom.hu']} +16:27:44 calcom state running | files=[setup-gate-calcom.yml] record={'state': 'closed', 'since': '2026-09-29T14:26:30Z', 'hosts': ['r-calcom.enkisfelhom.hu']} +16:28:18 adventurelog state unhealthy | files=[setup-gate-adventurelog.yml] record={'state': 'closed', 'since': '2026-09-29T14:26:31Z', 'hosts': ['r-adventurelog.enkisfelhom.hu']} +16:28:21 homebox state running | files=[setup-gate-homebox.yml] record={'state': 'closed', 'since': '2026-09-29T14:26:31Z', 'hosts': ['r-homebox.enkisfelhom.hu']} diff --git a/documentation/audits/signup-lock-2026-09-29/A/X1-household.txt b/documentation/audits/signup-lock-2026-09-29/A/X1-household.txt new file mode 100644 index 00000000..5364c455 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/X1-household.txt @@ -0,0 +1,7 @@ +16:29:06 ghost HOUSEHOLD -> 200 in 0.67s | __gate/start -> 302 r-ghost.enkisfelhom.hu/__felhom_gate/cb -> 200 r-ghost.enkisfelhom.hu/ | gate page False | app says 'Ghost' +16:29:06 home-assistant HOUSEHOLD -> 200 in 0.25s | 02 r-home-assistant.enkisfelhom.hu/ -> 200 r-home-assistant.enkisfelhom.hu/onboarding.html | gate page False | app says 'Home Assistant' +16:29:06 gramps-web HOUSEHOLD -> 200 in 0.20s | rt -> 302 r-gramps-web.enkisfelhom.hu/__felhom_gate/cb -> 200 r-gramps-web.enkisfelhom.hu/ | gate page False | app says 'Gramps Web' +16:29:06 gitea HOUSEHOLD -> 200 in 0.20s | __gate/start -> 302 r-gitea.enkisfelhom.hu/__felhom_gate/cb -> 200 r-gitea.enkisfelhom.hu/ | gate page False | app says 'Installation - Gitea: Git with a cup of ' +16:29:08 calcom HOUSEHOLD -> 200 in 1.32s | /login -> 307 r-calcom.enkisfelhom.hu/auth/setup -> 200 r-calcom.enkisfelhom.hu/auth/setup | gate page False | app says 'Setup | Cal.com' +16:29:08 adventurelog HOUSEHOLD -> 200 in 0.28s | > 302 r-adventurelog.enkisfelhom.hu/__felhom_gate/cb -> 200 r-adventurelog.enkisfelhom.hu/ | gate page False | app says 'AdventureLog' +16:29:08 homebox HOUSEHOLD -> 200 in 0.19s | te/start -> 302 r-homebox.enkisfelhom.hu/__felhom_gate/cb -> 200 r-homebox.enkisfelhom.hu/ | gate page False | app says ' 200 {'data': {'opened': True}, 'error': '', 'ok': True} +16:29:36 calcom PRESS 'Done' -> 200 {'data': {'opened': True}, 'error': '', 'ok': True} +16:29:42 adventurelog PRESS 'Done' -> 200 {'data': {'opened': True}, 'error': '', 'ok': True} +16:29:47 homebox PRESS 'Done' -> 200 {'data': {'opened': True}, 'error': '', 'ok': True} diff --git a/documentation/audits/signup-lock-2026-09-29/A/X1-setup.txt b/documentation/audits/signup-lock-2026-09-29/A/X1-setup.txt new file mode 100644 index 00000000..3b064cf9 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/X1-setup.txt @@ -0,0 +1,7 @@ +16:29:09 ghost HOUSEHOLD SETUP through the gate -> 201 {"users":[{"id":"6abbca9dfac6f200013fe617","name":"Family","slug":"family","email":"family@spike.hu","profile_image":nul +16:29:09 home-assistant HOUSEHOLD SETUP through the gate -> 200 {"auth_code":""} +16:29:10 gramps-web HOUSEHOLD SETUP through the gate -> 201 +16:29:15 gitea HOUSEHOLD SETUP through the gate -> 200 after POST: 200 / +16:29:15 calcom HOUSEHOLD SETUP through the gate -> 200 {"message":"First admin user created successfully."} +16:29:16 adventurelog HOUSEHOLD SETUP through the gate -> 200 {"type":"redirect","status":302,"location":"/"} +16:29:16 homebox HOUSEHOLD SETUP through the gate -> 204 diff --git a/documentation/audits/signup-lock-2026-09-29/A/X2-deploy.txt b/documentation/audits/signup-lock-2026-09-29/A/X2-deploy.txt new file mode 100644 index 00000000..9755d5b5 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/X2-deploy.txt @@ -0,0 +1,12 @@ +16:32:18 opengist deploy -> 202 +16:32:18 papra deploy -> 202 +16:32:18 sparkyfitness deploy -> 202 +16:32:18 termix deploy -> 202 +16:32:19 vikunja deploy -> 202 +16:32:19 wishlist deploy -> 202 +16:32:38 opengist state running | files=[setup-gate-opengist.yml] record={'state': 'closed', 'since': '2026-09-29T14:32:18Z', 'hosts': ['r-opengist.enkisfelhom.hu']} +16:32:46 papra state running | files=[setup-gate-papra.yml] record={'state': 'closed', 'since': '2026-09-29T14:32:18Z', 'hosts': ['r-papra.enkisfelhom.hu']} +16:33:04 sparkyfitness state running | files=[setup-gate-sparkyfitness.yml] record={'state': 'closed', 'since': '2026-09-29T14:32:18Z', 'hosts': ['r-sparkyfitness.enkisfelhom.hu']} +16:33:07 termix state running | files=[setup-gate-termix.yml] record={'state': 'closed', 'since': '2026-09-29T14:32:18Z', 'hosts': ['r-termix.enkisfelhom.hu']} +16:33:10 vikunja state running | files=[setup-gate-vikunja.yml] record={'state': 'closed', 'since': '2026-09-29T14:32:19Z', 'hosts': ['r-vikunja.enkisfelhom.hu']} +16:33:13 wishlist state running | files=[setup-gate-wishlist.yml] record={'state': 'closed', 'since': '2026-09-29T14:32:19Z', 'hosts': ['r-wishlist.enkisfelhom.hu']} diff --git a/documentation/audits/signup-lock-2026-09-29/A/X2-household.txt b/documentation/audits/signup-lock-2026-09-29/A/X2-household.txt new file mode 100644 index 00000000..a7fd0909 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/X2-household.txt @@ -0,0 +1,6 @@ +16:33:14 opengist HOUSEHOLD -> 200 in 0.25s | /__felhom_gate/cb -> 302 r-opengist.enkisfelhom.hu/ -> 200 r-opengist.enkisfelhom.hu/-/all | gate page False | app says 'All gists - Opengist' +16:33:14 papra HOUSEHOLD -> 200 in 0.20s | __gate/start -> 302 r-papra.enkisfelhom.hu/__felhom_gate/cb -> 200 r-papra.enkisfelhom.hu/ | gate page False | app says 'Papra - Document archiving and sharing p' +16:33:14 sparkyfitness HOUSEHOLD -> 200 in 0.19s | 302 r-sparkyfitness.enkisfelhom.hu/__felhom_gate/cb -> 200 r-sparkyfitness.enkisfelhom.hu/ | gate page False | app says 'SparkyFitness' +16:33:14 termix HOUSEHOLD -> 200 in 0.19s | gate/start -> 302 r-termix.enkisfelhom.hu/__felhom_gate/cb -> 200 r-termix.enkisfelhom.hu/ | gate page False | app says 'Termix' +16:33:14 vikunja HOUSEHOLD -> 200 in 0.20s | te/start -> 302 r-vikunja.enkisfelhom.hu/__felhom_gate/cb -> 200 r-vikunja.enkisfelhom.hu/ | gate page False | app says 'Vikunja' +16:33:15 wishlist HOUSEHOLD -> 200 in 0.57s | m.hu/ -> 302 r-wishlist.enkisfelhom.hu/login -> 200 r-wishlist.enkisfelhom.hu/setup-wizard | gate page False | app says ' ","user":{"name":"Family","email":"family@spike.hu","emailVerified":false,"ima +16:33:16 sparkyfitness HOUSEHOLD SETUP through the gate -> 200 {"token":"","user":{"name":"Family","email":"family@spike.hu","emailVerified":false,"ima +16:33:16 termix HOUSEHOLD SETUP through the gate -> 200 {"message":"User created","is_admin":true,"toast":{"type":"success","message":"User created: family"}} +16:33:17 vikunja HOUSEHOLD SETUP through the gate -> 200 {"id":1,"name":"","username":"family","created":"2026-09-29T14:33:17Z","updated":"2026-09-29T14:33:17.010229582Z"} + +16:33:17 wishlist HOUSEHOLD SETUP through the gate -> 200 {"type":"success","status":200,"data":"[{\"success\":1},true]"} diff --git a/documentation/audits/signup-lock-2026-09-29/A/aftersetuppatch.py b/documentation/audits/signup-lock-2026-09-29/A/aftersetuppatch.py new file mode 100644 index 00000000..1191ae5f --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/aftersetuppatch.py @@ -0,0 +1,19 @@ +# after_setup env per app (decision 47, controller >= 0.282.0) — appended after the signup_block line. +import os, sys +R = sys.argv[1] +VAL = {"adventurelog": ("SIGNUP_CLOSED", "true"), "calcom": ("SIGNUP_CLOSED", "true"), "gitea": ("SIGNUP_CLOSED", "true"), + "gramps-web": ("SIGNUP_CLOSED", "true"), "homebox": ("SIGNUP_OPEN", "false"), "papra": ("SIGNUP_OPEN", "false"), + "sparkyfitness": ("SIGNUP_CLOSED", "true"), "termix": ("SIGNUP_OPEN", "false"), "vikunja": ("SIGNUP_OPEN", "false")} +WHY = {"calcom": "safe from install too (the first admin is /auth/setup)", "gitea": "safe from install too (the installer makes the admin)", + "gramps-web": "safe from install too (create_owner)"} +for app, (k, v) in VAL.items(): + p = os.path.join(R, "templates", app, ".felhom.yml") + s = open(p).read() + assert "after_setup:" not in s and "\nsignup_block:" in s, app + i = s.index("\nsignup_block:"); j = s.index("\n", i + 1) + 1 + note = WHY.get(app, "it also refuses the household's own first account, so it goes on AFTER the setup") + s = s[:j] + (f"# The app's OWN sign-up switch, set once the gate opens (measured on 9202 2026-09-29: with it on a stranger's\n" + f"# sign-up is refused even straight at the app; {note}). The compose default stays open, so an installed app is\n" + f"# unchanged by the catalog.\nafter_setup:\n env:\n {k}: \"{v}\"\n") + s[j:] + open(p, "w").write(s) +print("after_setup on", list(VAL)) diff --git a/documentation/audits/signup-lock-2026-09-29/A/floor-0282.txt b/documentation/audits/signup-lock-2026-09-29/A/floor-0282.txt new file mode 100644 index 00000000..7035eb21 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/floor-0282.txt @@ -0,0 +1,4 @@ +## 2026-09-29T14:58:59Z floor 0.281.0 -> 0.282.0 (min_agent 0.131.0 from the v0.282.0 header) +HTTP/1.1 303 See Other +Location: /configuration?flash=floor_set +name="min_controller_version" value="0.282.0" diff --git a/documentation/audits/signup-lock-2026-09-29/A/gatepatch.py b/documentation/audits/signup-lock-2026-09-29/A/gatepatch.py new file mode 100644 index 00000000..b0732b69 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/gatepatch.py @@ -0,0 +1,31 @@ +# Adds setup_gate (+ optional probe / signup_block / add_people) to catalog templates. argv[1] = repo root, argv[2] = JSON spec file. +# spec: {app: {"probe": {"url","field","done","why"} | null, "signup_block": "" | null, +# "add_people": {"hu": "...", "en": "..."} | null, "why": ""}} +import json, os, re, sys +R, spec = sys.argv[1], json.load(open(sys.argv[2])) +for app, sp in spec.items(): + p = os.path.join(R, "templates", app, ".felhom.yml") + s = open(p).read() + if "\nsetup_gate:" in s: + # replace the whole managed block + s = re.sub(r"\n# --- The setup gate \(controller.*?(?=\n# --- App info|\napp_info:)", "", s, flags=re.S) + block = ["", "# --- The setup gate (controller >= 0.281.0, `09` §3 decisions 46-47) ---", + "# " + sp.get("why", "The first visitor would create the admin; a fresh install is closed to everyone but the household until the first setup is done."), + "setup_gate: true"] + pr = sp.get("probe") + if pr: + block += ["# " + pr["why"], "setup_done_probe:", f" url: {pr['url']}", f" field: {pr['field']}", f" done: \"{pr['done']}\""] + ([f" done_status: {pr['done_status']}"] if pr.get("done_status") else []) + if sp.get("signup_block"): + block += ["# Decision 47: the app's own sign-up address is closed once the gate opens (" + sp.get("signup_why", "measured on 9202 2026-09-29") + ").", + "signup_block: \"" + sp["signup_block"].replace('"', '\\"') + "\""] + i = s.index("\n# --- App info") if "\n# --- App info" in s else s.index("\napp_info:") + s = s[:i] + "\n".join(block) + "\n" + s[i:] + ap = sp.get("add_people") + if ap: + s = re.sub(r"(?m)^[ ]*add_people: [^\n]*\n", "", s) # replace, never duplicate + # Hungarian: into app_info (after docs_url or default_creds or tagline line); English: into i18n.en.app_info + s = re.sub(r"(\napp_info:\n(?: [^\n]*\n)*? tagline: [^\n]*\n)", lambda m: m.group(1) + " add_people: " + json.dumps(ap["hu"], ensure_ascii=False) + "\n", s, count=1) + s = re.sub(r"(\n en:\n(?: [^\n]*\n)*? app_info:\n tagline: [^\n]*\n)", lambda m: m.group(1) + " add_people: " + json.dumps(ap["en"], ensure_ascii=False) + "\n", s, count=1) + assert s.count("add_people:") == 2, app + open(p, "w").write(s) +print("patched", len(spec), "apps in", R) diff --git a/documentation/audits/signup-lock-2026-09-29/A/probecomment.py b/documentation/audits/signup-lock-2026-09-29/A/probecomment.py new file mode 100644 index 00000000..18ee2178 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/probecomment.py @@ -0,0 +1,12 @@ +# Put the measured before/after directly above setup_done_probe: for the three morning probes (R-715 gate). +import os, sys +R = sys.argv[1] +NOTE = {"immich": "# measured on 9202 2026-09-29: isInitialized false -> true after the admin sign-up (audits/login-gate-2026-09-29/C).", + "n8n": "# measured on 9202 2026-09-29: showSetupOnFirstLoad true -> false after the owner setup (audits/login-gate-2026-09-29/C).", + "audiobookshelf": "# measured on 9202 2026-09-29: isInit false -> true after POST /init; the gate opened ~20 s later (audits/login-gate-2026-09-29/C)."} +for app, note in NOTE.items(): + p = os.path.join(R, "templates", app, ".felhom.yml"); s = open(p).read() + assert s.count("\nsetup_done_probe:") == 1 and note not in s, app + s = s.replace("\nsetup_done_probe:", "\n" + note + "\nsetup_done_probe:") + open(p, "w").write(s) +print("probe comments placed") diff --git a/documentation/audits/signup-lock-2026-09-29/A/signuppatch.py b/documentation/audits/signup-lock-2026-09-29/A/signuppatch.py new file mode 100644 index 00000000..f0d1392c --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/signuppatch.py @@ -0,0 +1,28 @@ +# Decision 47 part A: each app's OWN sign-up switch, read from SIGNUP_CLOSED / SIGNUP_OPEN (default: open, so an +# installed app is unchanged by the catalog). argv: repo [--closed-default] (the spike's lock-from-install variant). +import os, re, sys +R = sys.argv[1]; closed = "--closed-default" in sys.argv +NATIVE = { # app: (compose service, native env, which variable, value meaning CLOSED) + "adventurelog": ("adventurelog", "DISABLE_REGISTRATION", "SIGNUP_CLOSED"), + "calcom": ("calcom", "NEXT_PUBLIC_DISABLE_SIGNUP", "SIGNUP_CLOSED"), + "gitea": ("gitea", "GITEA__service__DISABLE_REGISTRATION", "SIGNUP_CLOSED"), + "gramps-web": ("gramps-web", "GRAMPSWEB_REGISTRATION_DISABLED", "SIGNUP_CLOSED"), + "homebox": ("homebox", "HBOX_OPTIONS_ALLOW_REGISTRATION", "SIGNUP_OPEN"), + "papra": ("papra", "AUTH_IS_REGISTRATION_ENABLED", "SIGNUP_OPEN"), + "sparkyfitness": ("sparkyfitness-server", "SPARKY_FITNESS_DISABLE_SIGNUP", "SIGNUP_CLOSED"), + "termix": ("termix", "ALLOW_REGISTRATION", "SIGNUP_OPEN"), + "vikunja": ("vikunja", "VIKUNJA_SERVICE_ENABLEREGISTRATION", "SIGNUP_OPEN"), +} +only = [a for a in sys.argv[2:] if not a.startswith("--")] or list(NATIVE) +for app in only: + svc, env, var = NATIVE[app] + p = os.path.join(R, "templates", app, "docker-compose.yml") + s = open(p).read() + s = re.sub(r"\n\s+- " + re.escape(env) + r"=[^\n]*", "", s) # an existing fixed value (sparkyfitness) goes + default = ("true" if var == "SIGNUP_CLOSED" else "false") if closed else ("false" if var == "SIGNUP_CLOSED" else "true") + line = f" # decision 47: the app's own sign-up switch — open until the box closes it after the first admin (after_setup)\n - {env}=${{{var}:-{default}}}\n" + m = re.search(r"\n " + re.escape(svc) + r":\n(?: [^\n]*\n|\n)*? environment:\n", s) + assert m, (app, svc) + s = s[:m.end()] + line + s[m.end():] + open(p, "w").write(s) +print("native switch wired:", only, "closed-default" if closed else "open-default") diff --git a/documentation/audits/signup-lock-2026-09-29/A/spec.json b/documentation/audits/signup-lock-2026-09-29/A/spec.json new file mode 100644 index 00000000..d57e7450 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/spec.json @@ -0,0 +1,170 @@ +{ + "actualbudget": { + "probe": { + "url": "http://actualbudget:5006/account/needs-bootstrap", + "field": "data.bootstrapped", + "done": "true", + "why": "measured on 9202 2026-09-29: data.bootstrapped false -> true once the server password is set." + } + }, + "adventurelog": { + "signup_block": "PathRegexp(`(?i)^/+(signup/*$|auth/+browser/+v1/+auth/+signup|_allauth/+browser/+v1/+auth/+signup|accounts/+signup)`)", + "signup_why": "measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)", + "add_people": { + "hu": "Nyisd meg a regisztrációt 15 percre, és a családtagod a saját címével regisztrál.", + "en": "Open sign-up for 15 minutes, and your family member signs up with their own address." + } + }, + "calcom": { + "signup_block": "PathRegexp(`(?i)^/+(signup|auth/+signup|api/+auth/+signup)(/|$)`)", + "signup_why": "measured on 9202 2026-09-29: a stranger's POST /api/auth/signup created an account (201) after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)", + "add_people": { + "hu": "Beállítások → Adminisztráció → Felhasználók → Új felhasználó.", + "en": "Settings → Admin → Users → Add user." + } + }, + "docmost": {}, + "emby": {}, + "ghost": { + "probe": { + "url": "http://ghost:2368/ghost/api/admin/authentication/setup/", + "field": "setup.0.status", + "done": "true", + "why": "measured on 9202 2026-09-29: before the setup {\"setup\":[{\"status\":false}]}, after {\"setup\":[{\"status\":true}]}." + } + }, + "gitea": { + "signup_block": "PathRegexp(`(?i)^/+user/+sign_up`)", + "signup_why": "measured on 9202 2026-09-29: /user/sign_up served the registration form after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)", + "add_people": { + "hu": "Webhely adminisztráció → Felhasználói fiókok → Új felhasználói fiók létrehozása.", + "en": "Site Administration → User Accounts → Create User Account." + } + }, + "gramps-web": { + "signup_block": "PathRegexp(`(?i)^/+api/+users/+[^/]+/+register`)", + "signup_why": "measured on 9202 2026-09-29: the self-registration address answered (500 here); closed to be safe; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)", + "add_people": { + "hu": "Adminként: Beállítások → Felhasználók kezelése → Új felhasználó.", + "en": "As the admin: Settings → Manage users → New user." + }, + "probe": { + "url": "http://gramps-web:5000/api/token/create_owner/", + "field": "error.code", + "done": "405", + "why": "measured on 9202 2026-09-29: before the setup HTTP 200 with an owner token, after HTTP 405 \"Users already exist\".", + "done_status": 405 + } + }, + "home-assistant": { + "probe": { + "url": "http://home-assistant:8123/api/onboarding", + "field": "0.done", + "done": "true", + "why": "measured on 9202 2026-09-29: before [{\"step\":\"user\",\"done\":false},…], after the user step [{\"step\":\"user\",\"done\":true},…]." + } + }, + "homebox": { + "signup_block": "PathRegexp(`(?i)^/+api/+v1/+users/+register`)", + "signup_why": "measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)", + "add_people": { + "hu": "Nyisd meg a regisztrációt 15 percre, és küldd el a családtagodnak a Homebox csoport meghívó linkjét (Beállítások → Csoport → Meghívó).", + "en": "Open sign-up for 15 minutes and send your family member Homebox's group invite link (Settings → Group → Invite)." + } + }, + "jellyfin": { + "probe": { + "url": "http://jellyfin:8096/System/Info/Public", + "field": "StartupWizardCompleted", + "done": "true", + "why": "measured on 9202 2026-09-29: StartupWizardCompleted false -> true after the startup wizard." + } + }, + "komga": { + "probe": { + "url": "http://komga:25600/api/v1/claim", + "field": "isClaimed", + "done": "true", + "why": "measured on 9202 2026-09-29: isClaimed false -> true once the admin claimed it." + } + }, + "navidrome": {}, + "opengist": { + "signup_block": "PathRegexp(`(?i)^/+-/+register`)", + "signup_why": "measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)", + "add_people": { + "hu": "Nyisd meg a regisztrációt 15 percre, és a családtagod regisztrál.", + "en": "Open sign-up for 15 minutes, and your family member signs up." + } + }, + "outline": {}, + "papra": { + "signup_block": "PathRegexp(`(?i)^/+api/+auth/+sign-up`)", + "signup_why": "measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)", + "add_people": { + "hu": "Nyisd meg a regisztrációt 15 percre; a családtagod regisztrál, aztán meghívod a szervezetedbe (Szervezet → Tagok → Meghívás).", + "en": "Open sign-up for 15 minutes; your family member signs up, then you invite them into your organization (Organization → Members → Invite)." + } + }, + "plant-it": {}, + "radarr": {}, + "rallly": {}, + "recipe-importer": {}, + "romm": { + "probe": { + "url": "http://romm:8080/api/heartbeat", + "field": "SYSTEM.SHOW_SETUP_WIZARD", + "done": "false", + "why": "measured on 9202 2026-09-29: SYSTEM.SHOW_SETUP_WIZARD true -> false once the first user exists." + } + }, + "seerr": {}, + "sonarr": {}, + "sparkyfitness": { + "signup_block": "PathRegexp(`(?i)^/+api/+auth/+sign-up`)", + "signup_why": "measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)", + "add_people": { + "hu": "Nyisd meg a regisztrációt 15 percre, és a családtagod regisztrál.", + "en": "Open sign-up for 15 minutes, and your family member signs up." + } + }, + "tandoor": {}, + "termix": { + "probe": { + "url": "http://termix:8080/users/setup-required", + "field": "setup_required", + "done": "false", + "why": "measured on 9202 2026-09-29: setup_required true -> false once the first user exists." + }, + "signup_block": "PathRegexp(`(?i)^/+users/+create`)", + "signup_why": "measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)", + "add_people": { + "hu": "Nyisd meg a regisztrációt 15 percre, és a családtagod regisztrál.", + "en": "Open sign-up for 15 minutes, and your family member signs up." + } + }, + "vikunja": { + "signup_block": "PathRegexp(`(?i)^/+api/+v1/+register`)", + "signup_why": "measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)", + "add_people": { + "hu": "Nyisd meg a regisztrációt 15 percre, és a családtagod regisztrál; utána megoszthatod vele a projektjeidet.", + "en": "Open sign-up for 15 minutes, and your family member signs up; then share your projects with them." + } + }, + "wishlist": { + "signup_block": "PathRegexp(`(?i)^/+signup/*$`)", + "signup_why": "measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)", + "add_people": { + "hu": "Nyisd meg a regisztrációt 15 percre, és a családtagod regisztrál (a Wishlist meghívó linkje is ebben az időben működik).", + "en": "Open sign-up for 15 minutes, and your family member signs up (Wishlist's invite link works in that time too)." + } + }, + "zipline": { + "probe": { + "url": "http://zipline:3000/api/server/public", + "field": "firstSetup", + "done": "false", + "why": "measured on 9202 2026-09-29: firstSetup true -> false once the first user exists (/api/setup itself answers 403 after the setup, so it cannot be the check)." + } + } +} \ No newline at end of file diff --git a/documentation/audits/signup-lock-2026-09-29/A/wandererpatch.py b/documentation/audits/signup-lock-2026-09-29/A/wandererpatch.py new file mode 100644 index 00000000..b88b2b8c --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/A/wandererpatch.py @@ -0,0 +1,35 @@ +# Decision 48 / R-714: wanderer cannot be gated (its web server calls its own database through the public name), and +# needs no gate for the first admin (PocketBase's superuser installer needs the one-time link from the server log). +# Its door is sign-up — through the web AND straight through PocketBase's API. Closed by the household's press. +import os, re, sys +R = sys.argv[1] +c = os.path.join(R, "templates", "wanderer", "docker-compose.yml"); s = open(c).read() +assert " - PUBLIC_DISABLE_SIGNUP=false\n" in s +s = s.replace(" - PUBLIC_DISABLE_SIGNUP=false\n", " # decision 47/48: open until the household closes sign-up (after_setup / close sign-up now)\n - PUBLIC_DISABLE_SIGNUP=${SIGNUP_CLOSED:-false}\n", 1) +open(c, "w").write(s) +f = os.path.join(R, "templates", "wanderer", ".felhom.yml"); s = open(f).read() +i = s.index("\n# --- App info") if "\n# --- App info" in s else s.index("\napp_info:") +block = """ +# --- Sign-up (controller >= 0.282.0, `09` §3 decisions 47-48) --- +# NOT gated: its web server calls its own database through the public name, which a gate would refuse (measured on +# 9202 2026-09-29, R-714). No first-admin screen to take: PocketBase's superuser installer needs the one-time link +# from the server log. The door is SIGN-UP — in the web and straight through PocketBase's API (measured 2026-09-29: +# a stranger's POST /api/collections/users/records answered 200 — and PocketBase takes the collection by its id +# `_pb_users_auth_` and in ANY letter case, hence the case-insensitive pattern). After the household makes its account, the app +# page offers "Close sign-up now": both addresses answer "sign-up is closed", and wanderer's own switch goes on. +signup_block: "Path(`/register`) || (PathRegexp(`(?i)^/+api/+collections/+(users|_pb_users_auth_)/+records`) && Method(`POST`))" +after_setup: + env: + SIGNUP_CLOSED: "true" +""" +s = s[:i] + block + s[i:] +s = re.sub(r"(\napp_info:\n(?: [^\n]*\n)*? tagline: [^\n]*\n)", lambda m: m.group(1) + ' add_people: "Nyisd meg a regisztrációt 15 percre, és a családtagod regisztrál."\n', s, count=1) +s = re.sub(r"(\n en:\n(?: [^\n]*\n)*? app_info:\n tagline: [^\n]*\n)", lambda m: m.group(1) + " add_people: 'Open sign-up for 15 minutes, and your family member signs up.'\n", s, count=1) +old_hu = " - 'Hozd létre a fiókodat azonnal a telepítés után. Figyelem: ebben az alkalmazásban bárki, aki megtalálja a címet, fiókot hozhat létre.'\n" +old_en = " - 'Create your account right after the install. Note: in this app, anyone who finds the address can make an account.'\n" +assert old_hu in s and old_en in s +s = s.replace(old_hu, " - 'Hozd létre a fiókodat azonnal a telepítés után, aztán az alkalmazás oldalán zárd le a regisztrációt. Addig bárki, aki megtalálja a címet, fiókot hozhat létre.'\n") +s = s.replace(old_en, " - 'Create your account right after the install, then close sign-up on the app page. Until then, anyone who finds the address can make an account.'\n") +assert s.count("add_people:") == 2 +open(f, "w").write(s) +print("wanderer patched") diff --git a/documentation/audits/signup-lock-2026-09-29/B/B1-case-check.txt b/documentation/audits/signup-lock-2026-09-29/B/B1-case-check.txt new file mode 100644 index 00000000..44132c09 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/B1-case-check.txt @@ -0,0 +1,7 @@ +homebox /API/V1/USERS/REGISTER -> 200 ' 200 ' 200 ' 403 BLOCK +gramps-web API UPPER /API/USERS/TRKE69882/REGISTER -> 200 APP-FALLBACK (no account) +gramps-web API Mixed /Api/Users/trk48fb2d/Register -> 200 APP-FALLBACK (no account) +gramps-web API percent-encoded /api/users/trk212ecd/r%65gister -> 308 APP-REFUSED +gramps-web API double slash //api/users/trkc4434c/register -> 308 APP-REFUSED +gramps-web API query string /api/users/trk262acd/register?x=1 -> 308 APP-REFUSED +homebox API as written /api/v1/users/register -> 403 BLOCK +homebox API trailing slash /api/v1/users/register/ -> 403 BLOCK +homebox API UPPER /API/V1/USERS/REGISTER -> 200 APP-FALLBACK (no account) +homebox API Mixed /Api/V1/Users/Register -> 200 APP-FALLBACK (no account) +homebox API percent-encoded /api/v1/users/r%65gister -> 403 BLOCK +homebox API double slash //api/v1/users/register -> 403 BLOCK +homebox API query string /api/v1/users/register?x=1 -> 403 BLOCK diff --git a/documentation/audits/signup-lock-2026-09-29/B/B1-tricks-X1.txt b/documentation/audits/signup-lock-2026-09-29/B/B1-tricks-X1.txt new file mode 100644 index 00000000..3379f641 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/B1-tricks-X1.txt @@ -0,0 +1,54 @@ +gitea web form as written /user/sign_up -> 403 BLOCK +gitea web form trailing slash /user/sign_up/ -> 403 BLOCK +gitea web form UPPER /USER/SIGN_UP -> 404 APP-REFUSED +gitea web form Mixed /User/Sign_up -> 404 APP-REFUSED +gitea web form percent-encoded /user/s%69gn_up -> 403 BLOCK +gitea web form double slash //user/sign_up -> 403 BLOCK +gitea web form query string /user/sign_up?x=1 -> 403 BLOCK +calcom API as written /api/auth/signup -> 403 BLOCK +calcom API trailing slash /api/auth/signup/ -> 403 BLOCK +calcom API UPPER /API/AUTH/SIGNUP -> 404 APP-REFUSED +calcom API Mixed /Api/Auth/Signup -> 404 APP-REFUSED +calcom API percent-encoded /api/auth/s%69gnup -> 403 BLOCK +calcom API double slash //api/auth/signup -> 403 BLOCK +calcom API query string /api/auth/signup?x=1 -> 403 BLOCK +calcom web page as written /signup -> 403 BLOCK +calcom web page trailing slash /signup/ -> 403 BLOCK +calcom web page UPPER /SIGNUP -> 404 APP-REFUSED +calcom web page Mixed /Signup -> 404 APP-REFUSED +calcom web page percent-encoded /s%69gnup -> 403 BLOCK +calcom web page double slash //signup -> 403 BLOCK +calcom web page query string /signup?x=1 -> 403 BLOCK +gramps-web API as written /api/users/trk2ea6cd/register/ -> 403 BLOCK +gramps-web API UPPER /API/USERS/TRK5BAA7E/REGISTER -> 200 GOT-IN +gramps-web API Mixed /Api/Users/trk5a331f/Register -> 200 GOT-IN +gramps-web API percent-encoded /api/users/trk5eeb65/r%65gister -> 308 APP-REFUSED +gramps-web API double slash //api/users/trk617174/register -> 308 APP-REFUSED +gramps-web API query string /api/users/trk67074f/register?x=1 -> 308 APP-REFUSED +homebox API as written /api/v1/users/register -> 403 BLOCK +homebox API trailing slash /api/v1/users/register/ -> 403 BLOCK +homebox API UPPER /API/V1/USERS/REGISTER -> 200 GOT-IN +homebox API Mixed /Api/V1/Users/Register -> 200 GOT-IN +homebox API percent-encoded /api/v1/users/r%65gister -> 403 BLOCK +homebox API double slash //api/v1/users/register -> 403 BLOCK +homebox API query string /api/v1/users/register?x=1 -> 403 BLOCK +adventurelog web form action as written /signup -> 403 BLOCK +adventurelog web form action trailing slash /signup/ -> 308 APP-REFUSED +adventurelog web form action UPPER /SIGNUP -> 404 APP-REFUSED +adventurelog web form action Mixed /Signup -> 404 APP-REFUSED +adventurelog web form action percent-encoded /s%69gnup -> 403 BLOCK +adventurelog web form action double slash //signup -> 403 BLOCK +adventurelog web form action query string /signup?x=1 -> 403 BLOCK +adventurelog allauth API as written /auth/browser/v1/auth/signup -> 403 BLOCK +adventurelog allauth API trailing slash /auth/browser/v1/auth/signup/ -> 403 BLOCK +adventurelog allauth API UPPER /AUTH/BROWSER/V1/AUTH/SIGNUP -> 404 APP-REFUSED +adventurelog allauth API Mixed /Auth/Browser/V1/Auth/Signup -> 404 APP-REFUSED +adventurelog allauth API percent-encoded /auth/browser/v1/auth/s%69gnup -> 403 BLOCK +adventurelog allauth API double slash //auth/browser/v1/auth/signup -> 403 BLOCK +adventurelog allauth API query string /auth/browser/v1/auth/signup?x=1 -> 403 BLOCK +adventurelog allauth classic as written /accounts/signup/ -> 403 BLOCK +adventurelog allauth classic UPPER /ACCOUNTS/SIGNUP -> 404 APP-REFUSED +adventurelog allauth classic Mixed /Accounts/Signup -> 404 APP-REFUSED +adventurelog allauth classic percent-encoded /accounts/s%69gnup -> 403 BLOCK +adventurelog allauth classic double slash //accounts/signup -> 403 BLOCK +adventurelog allauth classic query string /accounts/signup?x=1 -> 403 BLOCK diff --git a/documentation/audits/signup-lock-2026-09-29/B/B2-tricks-X2.txt b/documentation/audits/signup-lock-2026-09-29/B/B2-tricks-X2.txt new file mode 100644 index 00000000..bde64422 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/B2-tricks-X2.txt @@ -0,0 +1,49 @@ +papra API as written /api/auth/sign-up/email -> 403 BLOCK +papra API trailing slash /api/auth/sign-up/email/ -> 403 BLOCK +papra API UPPER /API/AUTH/SIGN-UP/EMAIL -> 200 APP-REFUSED +papra API Mixed /Api/Auth/Sign-up/Email -> 200 APP-REFUSED +papra API percent-encoded /api/auth/sign-up/e%6Dail -> 403 BLOCK +papra API double slash //api/auth/sign-up/email -> 403 BLOCK +papra API query string /api/auth/sign-up/email?x=1 -> 403 BLOCK +sparkyfitness API as written /api/auth/sign-up/email -> 403 BLOCK +sparkyfitness API trailing slash /api/auth/sign-up/email/ -> 403 BLOCK +sparkyfitness API UPPER /API/AUTH/SIGN-UP/EMAIL -> 405 APP-REFUSED +sparkyfitness API Mixed /Api/Auth/Sign-up/Email -> 405 APP-REFUSED +sparkyfitness API percent-encoded /api/auth/sign-up/e%6Dail -> 403 BLOCK +sparkyfitness API double slash //api/auth/sign-up/email -> 403 BLOCK +sparkyfitness API query string /api/auth/sign-up/email?x=1 -> 403 BLOCK +termix API as written /users/create -> 403 BLOCK +termix API trailing slash /users/create/ -> 403 BLOCK +termix API UPPER /USERS/CREATE -> 405 APP-REFUSED +termix API Mixed /Users/Create -> 405 APP-REFUSED +termix API percent-encoded /users/c%72eate -> 403 BLOCK +termix API double slash //users/create -> 403 BLOCK +termix API query string /users/create?x=1 -> 403 BLOCK +vikunja API as written /api/v1/register -> 403 BLOCK +vikunja API trailing slash /api/v1/register/ -> 403 BLOCK +vikunja API UPPER /API/V1/REGISTER -> 200 APP-REFUSED +vikunja API Mixed /Api/V1/Register -> 200 APP-REFUSED +vikunja API percent-encoded /api/v1/r%65gister -> 403 BLOCK +vikunja API double slash //api/v1/register -> 403 BLOCK +vikunja API query string /api/v1/register?x=1 -> 403 BLOCK +vikunja web page as written /register -> 200 APP-REFUSED +vikunja web page trailing slash /register/ -> 200 APP-REFUSED +vikunja web page UPPER /REGISTER -> 200 APP-REFUSED +vikunja web page Mixed /Register -> 200 APP-REFUSED +vikunja web page percent-encoded /r%65gister -> 200 APP-REFUSED +vikunja web page double slash //register -> 200 APP-REFUSED +vikunja web page query string /register?x=1 -> 200 APP-REFUSED +opengist web form as written /-/register -> 403 BLOCK (no form served) +opengist web form trailing slash /-/register/ -> 403 BLOCK (no form served) +opengist web form UPPER /-/REGISTER -> 404 APP-REFUSED (no form served) +opengist web form Mixed /-/Register -> 404 APP-REFUSED (no form served) +opengist web form percent-encoded /-/r%65gister -> 403 BLOCK (no form served) +opengist web form double slash //-/register -> 403 BLOCK (no form served) +opengist web form query string /-/register?x=1 -> 403 BLOCK (no form served) +wishlist web form action as written /signup -> 403 BLOCK +wishlist web form action trailing slash /signup/ -> 308 APP-REFUSED +wishlist web form action UPPER /SIGNUP -> 404 APP-REFUSED +wishlist web form action Mixed /Signup -> 404 APP-REFUSED +wishlist web form action percent-encoded /s%69gnup -> 403 BLOCK +wishlist web form action double slash //signup -> 403 BLOCK +wishlist web form action query string /signup?x=1 -> 403 BLOCK diff --git a/documentation/audits/signup-lock-2026-09-29/B/B3-login-after-block.txt b/documentation/audits/signup-lock-2026-09-29/B/B3-login-after-block.txt new file mode 100644 index 00000000..96566ae4 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/B3-login-after-block.txt @@ -0,0 +1,7 @@ +vikunja household sign-in POST /api/v1/login -> 200 OK +homebox household sign-in POST /api/v1/users/login -> 200 OK +papra household sign-in POST /api/auth/sign-in/email -> 200 OK +sparkyfitness household sign-in POST /api/auth/sign-in/email -> 200 OK +termix household sign-in POST /users/login -> 200 OK +gramps-web household sign-in POST /api/token/ -> 200 OK +calcom household sign-in POST /api/auth/callback/credentials -> 200 OK diff --git a/documentation/audits/signup-lock-2026-09-29/B/B4-tricks-all-final.txt b/documentation/audits/signup-lock-2026-09-29/B/B4-tricks-all-final.txt new file mode 100644 index 00000000..e7a65f3d --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/B4-tricks-all-final.txt @@ -0,0 +1,113 @@ +gitea web form as written /user/sign_up -> 403 BLOCK +gitea web form trailing slash /user/sign_up/ -> 403 BLOCK +gitea web form UPPER /USER/SIGN_UP -> 404 APP-REFUSED +gitea web form Mixed /User/Sign_up -> 404 APP-REFUSED +gitea web form last part UPPER /user/SIGN_UP -> 404 APP-REFUSED +gitea web form percent-encoded /user/s%69gn_up -> 403 BLOCK +gitea web form double slash //user/sign_up -> 403 BLOCK +gitea web form query string /user/sign_up?x=1 -> 403 BLOCK +calcom API as written /api/auth/signup -> 403 BLOCK +calcom API trailing slash /api/auth/signup/ -> 403 BLOCK +calcom API UPPER /API/AUTH/SIGNUP -> 404 APP-REFUSED +calcom API Mixed /Api/Auth/Signup -> 404 APP-REFUSED +calcom API last part UPPER /api/auth/SIGNUP -> 400 APP-REFUSED +calcom API percent-encoded /api/auth/s%69gnup -> 403 BLOCK +calcom API double slash //api/auth/signup -> 403 BLOCK +calcom API query string /api/auth/signup?x=1 -> 403 BLOCK +calcom web page as written /signup -> 403 BLOCK +calcom web page trailing slash /signup/ -> 403 BLOCK +calcom web page UPPER /SIGNUP -> 404 APP-REFUSED +calcom web page Mixed /Signup -> 404 APP-REFUSED +calcom web page percent-encoded /s%69gnup -> 403 BLOCK +calcom web page double slash //signup -> 403 BLOCK +calcom web page query string /signup?x=1 -> 403 BLOCK +gramps-web API as written /api/users/trk6402c9/register/ -> 403 BLOCK +gramps-web API UPPER /API/USERS/TRKB72BE3/REGISTER -> 200 APP-FALLBACK (no account) +gramps-web API Mixed /Api/Users/trkb8e63d/Register -> 200 APP-FALLBACK (no account) +gramps-web API last part UPPER /api/users/trk9968a4/REGISTER -> 404 APP-REFUSED +gramps-web API percent-encoded /api/users/trk5844e4/r%65gister -> 308 APP-REFUSED +gramps-web API double slash //api/users/trkef20d0/register -> 308 APP-REFUSED +gramps-web API query string /api/users/trkcc9e74/register?x=1 -> 308 APP-REFUSED +homebox API as written /api/v1/users/register -> 403 BLOCK +homebox API trailing slash /api/v1/users/register/ -> 403 BLOCK +homebox API UPPER /API/V1/USERS/REGISTER -> 200 APP-FALLBACK (no account) +homebox API Mixed /Api/V1/Users/Register -> 200 APP-FALLBACK (no account) +homebox API last part UPPER /api/v1/users/REGISTER -> 404 APP-REFUSED +homebox API percent-encoded /api/v1/users/r%65gister -> 403 BLOCK +homebox API double slash //api/v1/users/register -> 403 BLOCK +homebox API query string /api/v1/users/register?x=1 -> 403 BLOCK +adventurelog web form action as written /signup -> 403 BLOCK +adventurelog web form action trailing slash /signup/ -> 308 APP-REFUSED +adventurelog web form action UPPER /SIGNUP -> 404 APP-REFUSED +adventurelog web form action Mixed /Signup -> 404 APP-REFUSED +adventurelog web form action percent-encoded /s%69gnup -> 403 BLOCK +adventurelog web form action double slash //signup -> 403 BLOCK +adventurelog web form action query string /signup?x=1 -> 403 BLOCK +adventurelog allauth API as written /auth/browser/v1/auth/signup -> 403 BLOCK +adventurelog allauth API trailing slash /auth/browser/v1/auth/signup/ -> 403 BLOCK +adventurelog allauth API UPPER /AUTH/BROWSER/V1/AUTH/SIGNUP -> 404 APP-REFUSED +adventurelog allauth API Mixed /Auth/Browser/V1/Auth/Signup -> 404 APP-REFUSED +adventurelog allauth API last part UPPER /auth/browser/v1/auth/SIGNUP -> 404 APP-REFUSED +adventurelog allauth API percent-encoded /auth/browser/v1/auth/s%69gnup -> 403 BLOCK +adventurelog allauth API double slash //auth/browser/v1/auth/signup -> 403 BLOCK +adventurelog allauth API query string /auth/browser/v1/auth/signup?x=1 -> 403 BLOCK +adventurelog allauth classic as written /accounts/signup/ -> 403 BLOCK +adventurelog allauth classic UPPER /ACCOUNTS/SIGNUP -> 404 APP-REFUSED +adventurelog allauth classic Mixed /Accounts/Signup -> 404 APP-REFUSED +adventurelog allauth classic last part UPPER /accounts/SIGNUP -> 404 APP-REFUSED +adventurelog allauth classic percent-encoded /accounts/s%69gnup -> 403 BLOCK +adventurelog allauth classic double slash //accounts/signup -> 403 BLOCK +adventurelog allauth classic query string /accounts/signup?x=1 -> 403 BLOCK +papra API as written /api/auth/sign-up/email -> 403 BLOCK +papra API trailing slash /api/auth/sign-up/email/ -> 403 BLOCK +papra API UPPER /API/AUTH/SIGN-UP/EMAIL -> 200 APP-REFUSED +papra API Mixed /Api/Auth/Sign-up/Email -> 200 APP-REFUSED +papra API last part UPPER /api/auth/sign-up/EMAIL -> 403 BLOCK +papra API percent-encoded /api/auth/sign-up/e%6Dail -> 403 BLOCK +papra API double slash //api/auth/sign-up/email -> 403 BLOCK +papra API query string /api/auth/sign-up/email?x=1 -> 403 BLOCK +sparkyfitness API as written /api/auth/sign-up/email -> 403 BLOCK +sparkyfitness API trailing slash /api/auth/sign-up/email/ -> 403 BLOCK +sparkyfitness API UPPER /API/AUTH/SIGN-UP/EMAIL -> 405 APP-REFUSED +sparkyfitness API Mixed /Api/Auth/Sign-up/Email -> 405 APP-REFUSED +sparkyfitness API last part UPPER /api/auth/sign-up/EMAIL -> 403 BLOCK +sparkyfitness API percent-encoded /api/auth/sign-up/e%6Dail -> 403 BLOCK +sparkyfitness API double slash //api/auth/sign-up/email -> 403 BLOCK +sparkyfitness API query string /api/auth/sign-up/email?x=1 -> 403 BLOCK +termix API as written /users/create -> 403 BLOCK +termix API trailing slash /users/create/ -> 403 BLOCK +termix API UPPER /USERS/CREATE -> 405 APP-REFUSED +termix API Mixed /Users/Create -> 405 APP-REFUSED +termix API last part UPPER /users/CREATE -> 403 APP-REFUSED +termix API percent-encoded /users/c%72eate -> 403 BLOCK +termix API double slash //users/create -> 403 BLOCK +termix API query string /users/create?x=1 -> 403 BLOCK +vikunja API as written /api/v1/register -> 403 BLOCK +vikunja API trailing slash /api/v1/register/ -> 403 BLOCK +vikunja API UPPER /API/V1/REGISTER -> 200 APP-REFUSED +vikunja API Mixed /Api/V1/Register -> 200 APP-REFUSED +vikunja API last part UPPER /api/v1/REGISTER -> 404 APP-REFUSED +vikunja API percent-encoded /api/v1/r%65gister -> 403 BLOCK +vikunja API double slash //api/v1/register -> 403 BLOCK +vikunja API query string /api/v1/register?x=1 -> 403 BLOCK +vikunja web page as written /register -> 200 APP-REFUSED +vikunja web page trailing slash /register/ -> 200 APP-REFUSED +vikunja web page UPPER /REGISTER -> 200 APP-REFUSED +vikunja web page Mixed /Register -> 200 APP-REFUSED +vikunja web page percent-encoded /r%65gister -> 200 APP-REFUSED +vikunja web page double slash //register -> 200 APP-REFUSED +vikunja web page query string /register?x=1 -> 200 APP-REFUSED +opengist web form as written /-/register -> 403 BLOCK (no form served) +opengist web form trailing slash /-/register/ -> 403 BLOCK (no form served) +opengist web form UPPER /-/REGISTER -> 404 APP-REFUSED (no form served) +opengist web form Mixed /-/Register -> 404 APP-REFUSED (no form served) +opengist web form percent-encoded /-/r%65gister -> 403 BLOCK (no form served) +opengist web form double slash //-/register -> 403 BLOCK (no form served) +opengist web form query string /-/register?x=1 -> 403 BLOCK (no form served) +wishlist web form action as written /signup -> 403 BLOCK +wishlist web form action trailing slash /signup/ -> 308 APP-REFUSED +wishlist web form action UPPER /SIGNUP -> 404 APP-REFUSED +wishlist web form action Mixed /Signup -> 404 APP-REFUSED +wishlist web form action percent-encoded /s%69gnup -> 403 BLOCK +wishlist web form action double slash //signup -> 403 BLOCK +wishlist web form action query string /signup?x=1 -> 403 BLOCK diff --git a/documentation/audits/signup-lock-2026-09-29/B/B5-case-holes-caught-by-native.txt b/documentation/audits/signup-lock-2026-09-29/B/B5-case-holes-caught-by-native.txt new file mode 100644 index 00000000..dea04010 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/B5-case-holes-caught-by-native.txt @@ -0,0 +1,11 @@ +termix /users/CREATE -> 403 {"error":"Registration is currently disabled"} +termix /Users/Create -> 405 +405 Not Allowed + +

405 Not +termix /USERS/CREATE -> 405 +405 Not Allowed + +

405 Not +calcom /api/auth/SIGNUP -> 400 Error: This action with HTTP POST is not supported by NextAuth.js +calcom /API/AUTH/SIGNUP -> 404 403 BLOCK +gitea web form trailing slash /user/sign_up/ -> 403 BLOCK +gitea web form UPPER /USER/SIGN_UP -> 403 BLOCK +gitea web form Mixed /User/Sign_up -> 403 BLOCK +gitea web form last part UPPER /user/SIGN_UP -> 403 BLOCK +gitea web form percent-encoded /user/s%69gn_up -> 403 BLOCK +gitea web form double slash //user/sign_up -> 403 BLOCK +gitea web form query string /user/sign_up?x=1 -> 403 BLOCK +calcom API as written /api/auth/signup -> 403 BLOCK +calcom API trailing slash /api/auth/signup/ -> 403 BLOCK +calcom API UPPER /API/AUTH/SIGNUP -> 403 BLOCK +calcom API Mixed /Api/Auth/Signup -> 403 BLOCK +calcom API last part UPPER /api/auth/SIGNUP -> 403 BLOCK +calcom API percent-encoded /api/auth/s%69gnup -> 403 BLOCK +calcom API double slash //api/auth/signup -> 403 BLOCK +calcom API query string /api/auth/signup?x=1 -> 403 BLOCK +calcom web page as written /signup -> 403 BLOCK +calcom web page trailing slash /signup/ -> 403 BLOCK +calcom web page UPPER /SIGNUP -> 403 BLOCK +calcom web page Mixed /Signup -> 403 BLOCK +calcom web page percent-encoded /s%69gnup -> 403 BLOCK +calcom web page double slash //signup -> 403 BLOCK +calcom web page query string /signup?x=1 -> 403 BLOCK +gramps-web API as written /api/users/trk3143a9/register/ -> 403 BLOCK +gramps-web API UPPER /API/USERS/TRK3C1C0D/REGISTER -> 403 BLOCK +gramps-web API Mixed /Api/Users/trkb06481/Register -> 403 BLOCK +gramps-web API last part UPPER /api/users/trkedf384/REGISTER -> 403 BLOCK +gramps-web API percent-encoded /api/users/trk783e4e/r%65gister -> 403 BLOCK +gramps-web API double slash //api/users/trk3d1328/register -> 403 BLOCK +gramps-web API query string /api/users/trk671982/register?x=1 -> 403 BLOCK +homebox API as written /api/v1/users/register -> 403 BLOCK +homebox API trailing slash /api/v1/users/register/ -> 403 BLOCK +homebox API UPPER /API/V1/USERS/REGISTER -> 403 BLOCK +homebox API Mixed /Api/V1/Users/Register -> 403 BLOCK +homebox API last part UPPER /api/v1/users/REGISTER -> 403 BLOCK +homebox API percent-encoded /api/v1/users/r%65gister -> 403 BLOCK +homebox API double slash //api/v1/users/register -> 403 BLOCK +homebox API query string /api/v1/users/register?x=1 -> 403 BLOCK +adventurelog web form action as written /signup -> 403 BLOCK +adventurelog web form action trailing slash /signup/ -> 403 BLOCK +adventurelog web form action UPPER /SIGNUP -> 403 BLOCK +adventurelog web form action Mixed /Signup -> 403 BLOCK +adventurelog web form action percent-encoded /s%69gnup -> 403 BLOCK +adventurelog web form action double slash //signup -> 403 BLOCK +adventurelog web form action query string /signup?x=1 -> 403 BLOCK +adventurelog allauth API as written /auth/browser/v1/auth/signup -> 403 BLOCK +adventurelog allauth API trailing slash /auth/browser/v1/auth/signup/ -> 403 BLOCK +adventurelog allauth API UPPER /AUTH/BROWSER/V1/AUTH/SIGNUP -> 403 BLOCK +adventurelog allauth API Mixed /Auth/Browser/V1/Auth/Signup -> 403 BLOCK +adventurelog allauth API last part UPPER /auth/browser/v1/auth/SIGNUP -> 403 BLOCK +adventurelog allauth API percent-encoded /auth/browser/v1/auth/s%69gnup -> 403 BLOCK +adventurelog allauth API double slash //auth/browser/v1/auth/signup -> 403 BLOCK +adventurelog allauth API query string /auth/browser/v1/auth/signup?x=1 -> 403 BLOCK +adventurelog allauth classic as written /accounts/signup/ -> 403 BLOCK +adventurelog allauth classic UPPER /ACCOUNTS/SIGNUP -> 403 BLOCK +adventurelog allauth classic Mixed /Accounts/Signup -> 403 BLOCK +adventurelog allauth classic last part UPPER /accounts/SIGNUP -> 403 BLOCK +adventurelog allauth classic percent-encoded /accounts/s%69gnup -> 403 BLOCK +adventurelog allauth classic double slash //accounts/signup -> 403 BLOCK +adventurelog allauth classic query string /accounts/signup?x=1 -> 403 BLOCK +papra API as written /api/auth/sign-up/email -> 403 BLOCK +papra API trailing slash /api/auth/sign-up/email/ -> 403 BLOCK +papra API UPPER /API/AUTH/SIGN-UP/EMAIL -> 403 BLOCK +papra API Mixed /Api/Auth/Sign-up/Email -> 403 BLOCK +papra API last part UPPER /api/auth/sign-up/EMAIL -> 403 BLOCK +papra API percent-encoded /api/auth/sign-up/e%6Dail -> 403 BLOCK +papra API double slash //api/auth/sign-up/email -> 403 BLOCK +papra API query string /api/auth/sign-up/email?x=1 -> 403 BLOCK +sparkyfitness API as written /api/auth/sign-up/email -> 403 BLOCK +sparkyfitness API trailing slash /api/auth/sign-up/email/ -> 403 BLOCK +sparkyfitness API UPPER /API/AUTH/SIGN-UP/EMAIL -> 403 BLOCK +sparkyfitness API Mixed /Api/Auth/Sign-up/Email -> 403 BLOCK +sparkyfitness API last part UPPER /api/auth/sign-up/EMAIL -> 403 BLOCK +sparkyfitness API percent-encoded /api/auth/sign-up/e%6Dail -> 403 BLOCK +sparkyfitness API double slash //api/auth/sign-up/email -> 403 BLOCK +sparkyfitness API query string /api/auth/sign-up/email?x=1 -> 403 BLOCK +termix API as written /users/create -> 403 BLOCK +termix API trailing slash /users/create/ -> 403 BLOCK +termix API UPPER /USERS/CREATE -> 403 BLOCK +termix API Mixed /Users/Create -> 403 BLOCK +termix API last part UPPER /users/CREATE -> 403 BLOCK +termix API percent-encoded /users/c%72eate -> 403 BLOCK +termix API double slash //users/create -> 403 BLOCK +termix API query string /users/create?x=1 -> 403 BLOCK +vikunja API as written /api/v1/register -> 403 BLOCK +vikunja API trailing slash /api/v1/register/ -> 403 BLOCK +vikunja API UPPER /API/V1/REGISTER -> 403 BLOCK +vikunja API Mixed /Api/V1/Register -> 403 BLOCK +vikunja API last part UPPER /api/v1/REGISTER -> 403 BLOCK +vikunja API percent-encoded /api/v1/r%65gister -> 403 BLOCK +vikunja API double slash //api/v1/register -> 403 BLOCK +vikunja API query string /api/v1/register?x=1 -> 403 BLOCK +vikunja web page as written /register -> 200 APP-REFUSED +vikunja web page trailing slash /register/ -> 200 APP-REFUSED +vikunja web page UPPER /REGISTER -> 200 APP-REFUSED +vikunja web page Mixed /Register -> 200 APP-REFUSED +vikunja web page percent-encoded /r%65gister -> 200 APP-REFUSED +vikunja web page double slash //register -> 200 APP-REFUSED +vikunja web page query string /register?x=1 -> 200 APP-REFUSED +opengist web form as written /-/register -> 403 BLOCK (no form served) +opengist web form trailing slash /-/register/ -> 403 BLOCK (no form served) +opengist web form UPPER /-/REGISTER -> 403 BLOCK (no form served) +opengist web form Mixed /-/Register -> 403 BLOCK (no form served) +opengist web form percent-encoded /-/r%65gister -> 403 BLOCK (no form served) +opengist web form double slash //-/register -> 403 BLOCK (no form served) +opengist web form query string /-/register?x=1 -> 403 BLOCK (no form served) +wishlist web form action as written /signup -> 403 BLOCK +wishlist web form action trailing slash /signup/ -> 403 BLOCK +wishlist web form action UPPER /SIGNUP -> 403 BLOCK +wishlist web form action Mixed /Signup -> 403 BLOCK +wishlist web form action percent-encoded /s%69gnup -> 403 BLOCK +wishlist web form action double slash //signup -> 403 BLOCK +wishlist web form action query string /signup?x=1 -> 403 BLOCK diff --git a/documentation/audits/signup-lock-2026-09-29/B/B7-login-after-regex.txt b/documentation/audits/signup-lock-2026-09-29/B/B7-login-after-regex.txt new file mode 100644 index 00000000..f060d4e2 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/B7-login-after-regex.txt @@ -0,0 +1,4 @@ +vikunja household sign-in POST /api/v1/login -> 200 +homebox household sign-in POST /api/v1/users/login -> 200 +termix household sign-in POST /users/login -> 200 +papra household sign-in POST /api/auth/sign-in/email -> 200 diff --git a/documentation/audits/signup-lock-2026-09-29/B/B8-reset-and-share-not-blocked.txt b/documentation/audits/signup-lock-2026-09-29/B/B8-reset-and-share-not-blocked.txt new file mode 100644 index 00000000..8b5b0591 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/B8-reset-and-share-not-blocked.txt @@ -0,0 +1,8 @@ +vikunja /api/v1/user/password/token -> 200 not blocked (the app answered) +papra /api/auth/forget-password -> 404 not blocked (the app answered) +gitea /user/forgot_password -> 200 not blocked (the app answered) +calcom /auth/forgot-password -> 200 not blocked (the app answered) +opengist /-/login -> 200 not blocked (the app answered) +wishlist /login -> 200 not blocked (the app answered) +homebox /api/v1/status -> 200 not blocked (the app answered) +vikunja /api/v1/shares/nonexistent/auth -> 405 not blocked (the app answered) diff --git a/documentation/audits/signup-lock-2026-09-29/B/direct.sh b/documentation/audits/signup-lock-2026-09-29/B/direct.sh new file mode 100644 index 00000000..f9f24b63 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/direct.sh @@ -0,0 +1,6 @@ +# A stranger's sign-up sent STRAIGHT to the app on the docker network (past traefik and any address block): only the +# app's own switch can refuse it. calcom, gitea, gramps-web run with their switch ON from install here. +c() { printf '%s -> ' "$1"; shift; docker exec felhom-controller curl -s -m 8 -o /tmp/d.b -w '%{http_code}' "$@"; echo " $(head -c 120 /tmp/d.b | tr '\n' ' ')"; docker exec felhom-controller rm -f /tmp/d.b 2>/dev/null; } +c "calcom POST /api/auth/signup" -X POST -H 'Content-Type: application/json' -d '{"username":"strngr1","email":"s1@x.hu","password":"Xx12345678z!A"}' http://calcom:3000/api/auth/signup +c "gitea GET /user/sign_up" http://gitea:3000/user/sign_up +c "gramps-web POST /api/users/strngr1/register/" -X POST -H 'Content-Type: application/json' -d '{"email":"s1@x.hu","password":"Xx12345678z","full_name":"S"}' http://gramps-web:5000/api/users/strngr1/register/ diff --git a/documentation/audits/signup-lock-2026-09-29/B/direct2.sh b/documentation/audits/signup-lock-2026-09-29/B/direct2.sh new file mode 100644 index 00000000..9d8c9c0a --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/direct2.sh @@ -0,0 +1,2 @@ +docker exec felhom-controller sh -c 'curl -s http://gitea:3000/user/sign_up | grep -oiE "registration is disabled[^<]{0,40}|name=\"user_name\"" | sort -u; echo; curl -s -X POST -H "Content-Type: application/json" -d "{\"username\":\"strngr2\",\"email\":\"s2@x.hu\",\"password\":\"Xx12345678z!A\"}" http://calcom:3000/api/auth/signup | head -c 150; echo; curl -s -X POST -H "Content-Type: application/json" -d "{\"email\":\"s2@x.hu\",\"password\":\"Xx12345678z\",\"full_name\":\"S\"}" http://gramps-web:5000/api/users/strngr2/register/ | head -c 150; echo' +docker exec gitea sh -c 'grep -n "DISABLE_REGISTRATION" /data/gitea/conf/app.ini' diff --git a/documentation/audits/signup-lock-2026-09-29/B/gt2.sh b/documentation/audits/signup-lock-2026-09-29/B/gt2.sh new file mode 100644 index 00000000..ad5d7956 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/gt2.sh @@ -0,0 +1 @@ +echo "gitea GET /user/sign_up straight at the app -> $(docker exec felhom-controller curl -s http://gitea:3000/user/sign_up | grep -oiE "Registration is disabled|name=.user_name." | sort -u | tr "\n" " ")"; docker exec gitea grep -n DISABLE_REGISTRATION /data/gitea/conf/app.ini diff --git a/documentation/audits/signup-lock-2026-09-29/B/hb.sh b/documentation/audits/signup-lock-2026-09-29/B/hb.sh new file mode 100644 index 00000000..35de674a --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/hb.sh @@ -0,0 +1 @@ +docker exec homebox sh -c "ls /data 2>/dev/null"; docker exec felhom-controller sh -c "curl -s -X POST -H \"Content-Type: application/json\" -d \"{\\\"username\\\":\\\"cs1@x.hu\\\",\\\"password\\\":\\\"Xx12345678z9!\\\"}\" http://homebox:7745/api/v1/users/login | head -c 120"; echo diff --git a/documentation/audits/signup-lock-2026-09-29/B/native.sh b/documentation/audits/signup-lock-2026-09-29/B/native.sh new file mode 100644 index 00000000..b920cf5b --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/native.sh @@ -0,0 +1,17 @@ +# After the gate opened: the app's own switch (app.yaml record + the running container's env), and a stranger's +# sign-up sent STRAIGHT to the app on the docker network (past the address block). +for a in gitea calcom gramps-web adventurelog homebox; do + f=/opt/docker/stacks/$a/app.yaml + rec=$(awk '/^after_setup:/{f=1;next} f&&/^[a-z]/{f=0} f' $f | tr -s ' ' | tr '\n' ' ') + nl=$(grep -o 'native_lock: [a-z]*' $f) + env=$(grep -oE 'SIGNUP_(CLOSED|OPEN): "?[a-z]+' $f) + echo "$a: after_setup {$rec} $nl app.yaml $env" +done +echo "running env: gitea $(docker exec gitea printenv GITEA__service__DISABLE_REGISTRATION) | calcom $(docker exec calcom printenv NEXT_PUBLIC_DISABLE_SIGNUP) | gramps-web $(docker exec gramps-web printenv GRAMPSWEB_REGISTRATION_DISABLED) | adventurelog $(docker exec adventurelog printenv DISABLE_REGISTRATION) | homebox $(docker exec homebox printenv HBOX_OPTIONS_ALLOW_REGISTRATION)" +C() { printf ' %s -> ' "$1"; shift; docker exec felhom-controller sh -c "curl -s -m 8 -o /tmp/n.b -w '%{http_code}' $* ; echo \" \$(head -c 100 /tmp/n.b | tr '\n' ' ')\"; rm -f /tmp/n.b"; } +echo "stranger sign-up straight at the app (no traefik, no block):" +C "gitea GET /user/sign_up (form or 'disabled')" "http://gitea:3000/user/sign_up | grep -oiE 'Registration is disabled|name=.user_name.' | head -1 >/tmp/n.b; echo" +C "calcom POST /api/auth/signup" "-X POST -H 'Content-Type: application/json' -d '{\"username\":\"strx\",\"email\":\"sx@x.hu\",\"password\":\"Xx12345678z!A\"}' http://calcom:3000/api/auth/signup" +C "gramps-web POST register" "-X POST -H 'Content-Type: application/json' -d '{\"email\":\"sx@x.hu\",\"password\":\"Xx12345678z\",\"full_name\":\"S\"}' http://gramps-web:5000/api/users/strx/register/" +C "homebox POST /api/v1/users/register" "-X POST -H 'Content-Type: application/json' -d '{\"name\":\"S\",\"email\":\"sx@x.hu\",\"password\":\"Xx12345678z\"}' http://homebox:7745/api/v1/users/register" +C "adventurelog backend is-registration-disabled" "http://adventurelog:80/auth/is-registration-disabled/" diff --git a/documentation/audits/signup-lock-2026-09-29/B/native2.sh b/documentation/audits/signup-lock-2026-09-29/B/native2.sh new file mode 100644 index 00000000..25e391bc --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/native2.sh @@ -0,0 +1,2 @@ +for a in papra sparkyfitness termix vikunja opengist wishlist; do f=/opt/docker/stacks/$a/app.yaml; echo "$a: $(grep -o 'native_lock: [a-z]*' $f) $(grep -oE 'SIGNUP_(CLOSED|OPEN): "?[a-z]+' $f) | after_setup ok=$(awk '/^after_setup:/{f=1;next} f&&/^[a-z]/{f=0} f' $f | grep -o 'ok: [a-z]*')"; done +echo "running env: papra $(docker exec papra printenv AUTH_IS_REGISTRATION_ENABLED) | sparkyfitness $(docker exec sparkyfitness-server printenv SPARKY_FITNESS_DISABLE_SIGNUP) | termix $(docker exec termix printenv ALLOW_REGISTRATION) | vikunja $(docker exec vikunja /app/vikunja/vikunja version >/dev/null 2>&1; docker inspect vikunja --format '{{range .Config.Env}}{{println .}}{{end}}' | grep ENABLEREGISTRATION)" diff --git a/documentation/audits/signup-lock-2026-09-29/B/reset.sh b/documentation/audits/signup-lock-2026-09-29/B/reset.sh new file mode 100644 index 00000000..6a5ec98a --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/reset.sh @@ -0,0 +1,11 @@ +D=enkisfelhom.hu +r() { printf '%-60s -> ' "$1 $3"; c=$(curl -sk -o /tmp/r.b -w '%{http_code}' -H "Host: r-$1.$D" -H 'Content-Type: application/json' -X "$2" ${4:+-d "$4"} "https://127.0.0.1$3"); echo "$c $(grep -q 'sign-up is closed' /tmp/r.b && echo BLOCKED || echo 'not blocked (the app answered)')"; } +r vikunja POST /api/v1/user/password/token '{"email":"family@spike.hu"}' +r papra POST /api/auth/forget-password '{"email":"family@spike.hu","redirectTo":"/"}' +r gitea GET /user/forgot_password +r calcom GET /auth/forgot-password +r opengist GET /-/login +r wishlist GET /login +r homebox GET /api/v1/status +r vikunja GET /api/v1/shares/nonexistent/auth +rm -f /tmp/r.b diff --git a/documentation/audits/signup-lock-2026-09-29/B/ro.py b/documentation/audits/signup-lock-2026-09-29/B/ro.py new file mode 100644 index 00000000..7a1890fe --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/ro.py @@ -0,0 +1,121 @@ +# Rollout harness (2026-09-29 afternoon, 9202, controller 0.281.0, drill catalog). Evidence, not product. +# python3 ro.py deploy app... fresh installs through the product; the gate file + record right after +# python3 ro.py stranger app... a stranger: API-style request and a browser request +# python3 ro.py household app... the household (dashboard session) opens the app through the gate +# python3 ro.py probe app URL read a candidate status URL from inside the box (the controller's own network) +# python3 ro.py state app... gate record, gate file, sign-up block file +# python3 ro.py press app the household's "Done" press (the product's endpoint) +# python3 ro.py after app... a stranger after the gate opened +# python3 ro.py remove app... +# Household test passwords are generated per run and kept in ro_creds.json (scratch, 0600, deleted at teardown). +import json, os, secrets, sys, time +sys.path.insert(0, '/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/pg-calcom-claper-2026-09-28/tools') +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import walk as w +from browser import Browser, hopstr + +D = "enkisfelhom.hu" +PW = open(os.path.join(w.SC, ".ctlpw")).read().strip() +JAR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ro_jar.json") +CREDS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ro_creds.json") + + +def sub(app): + return "r-" + app + + +def host(app): + return f"{sub(app)}.{D}" + + +def household(): + b = Browser("household") + if os.path.exists(JAR): + b.jar = json.load(open(JAR)) + if "felhom." + D not in b.jar: + b.login_dashboard(D, PW) + return b + + +def save(b): + old = os.umask(0o077) + json.dump(b.jar, open(JAR, "w")) + os.umask(old) + + +def creds(app): + c = json.load(open(CREDS)) if os.path.exists(CREDS) else {} + if app not in c: + c[app] = {"user": "family", "email": "family@spike.hu", "pw": "Hh" + secrets.token_hex(10) + "7"} + old = os.umask(0o077) + json.dump(c, open(CREDS, "w")) + os.umask(old) + return c[app] + + +def gstate(app): + f = w.guest(f"for f in setup-gate-{app}.yml signup-block-{app}.yml; do test -f /opt/docker/stacks/traefik/dynamic/$f && echo -n \"$f \"; done; echo").strip() + rec = (w.stack(app).get("app_config") or {}).get("setup_gate") + return f"files=[{f}] record={rec}" + + +def main(): + cmd, args = sys.argv[1], sys.argv[2:] + w.login() + if cmd == "deploy": + for app in args: + v = w.deploy_values(app, sub(app)) + code, d = w.ctl("POST", f"/api/stacks/{app}/deploy", {"values": v, "kept_data": "fresh"}) + w.say(app, "deploy ->", code, "" if code == "202" else str(d)[:200]) + for app in args: + st = {} + for _ in range(120): + st = w.stack(app) + if st.get("deployed") and (st.get("app_config") or {}).get("pinned_images") and st.get("state") in ("running", "unhealthy", "degraded"): + break + time.sleep(5) + w.say(app, "state", st.get("state"), "|", gstate(app)) + elif cmd == "stranger": + for app in args: + s = Browser("stranger") + st, body, hops = s.req(f"https://{host(app)}/") + gp = "Jelentkezz be a Felhom" in body or "waiting for its first setup" in body + st2, body2, _ = s.req(f"https://{host(app)}/api/x", "POST", body={"a": 1}, accept="application/json") + w.say(app, f"STRANGER browser -> {st} {hopstr(hops)} gate-page={gp} | API POST -> {st2} {body2[:52]!r}") + elif cmd == "household": + b = household() + for app in args: + t0 = time.time() + st, body, hops = b.req(f"https://{host(app)}/") + gp = "Jelentkezz be a Felhom" in body or "waiting for its first setup" in body + title = body[body.find("") + 7: body.find("")][:40] if "" in body else body[:40].replace("\n", " ") + w.say(app, f"HOUSEHOLD -> {st} in {time.time() - t0:.2f}s | {hopstr(hops)[-90:]} | gate page {gp} | app says {title!r}") + save(b) + elif cmd == "probe": + app, url = args[0], args[1] + out = w.guest(f"docker exec felhom-controller curl -s -m 8 {url} | head -c 400") + w.say(app, "PROBE", url, "->", out.strip()[:400]) + elif cmd == "state": + for app in args: + w.say(app, gstate(app)) + elif cmd == "press": + app = args[0] + code, d = w.ctl("POST", f"/apps/{app}/setup-gate/open", {}) + w.say(app, "PRESS 'Done' ->", code, str(d)[:120]) + time.sleep(2) + w.say(app, gstate(app)) + elif cmd == "after": + for app in args: + s = Browser("stranger") + st, body, hops = s.req(f"https://{host(app)}/") + gp = "Jelentkezz be a Felhom" in body or "waiting for its first setup" in body + w.say(app, f"AFTER, stranger browser -> {st} {hopstr(hops)[-80:]} | gate page {gp} | len {len(body)}") + elif cmd == "remove": + for app in args: + w.remove(app) + else: + sys.exit("unknown command") + + +if __name__ == "__main__": + main() diff --git a/documentation/audits/signup-lock-2026-09-29/B/ro_setup.py b/documentation/audits/signup-lock-2026-09-29/B/ro_setup.py new file mode 100644 index 00000000..8b456b09 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/ro_setup.py @@ -0,0 +1,203 @@ +# The household's first setup of each app, THROUGH the gate (its browser holds the gate cookies from `ro.py household`). +# Each app's own first-run API, as its first-setup screen calls it. Passwords from ro_creds.json; never printed. +import json, sys, time +sys.path.insert(0, '.') +import ro +from ro import w, host + + +def J(b, app, method, path, body=None, headers=None): + st, text, _ = b.req(f"https://{host(app)}{path}", method, body=body, accept="application/json", headers=headers) + return st, text + + +def actualbudget(b, c): + return J(b, "actualbudget", "POST", "/account/bootstrap", {"password": c["pw"]}) + + +def komga(b, c): + return J(b, "komga", "POST", "/api/v1/claim", headers={"X-Komga-Email": c["email"], "X-Komga-Password": c["pw"]}) + + +def jellyfin(b, c): + out = [] + out.append(J(b, "jellyfin", "POST", "/Startup/Configuration", {"UICulture": "en-US", "MetadataCountryCode": "HU", "PreferredMetadataLanguage": "hu"})[0]) + out.append(J(b, "jellyfin", "GET", "/Startup/User")[0]) + out.append(J(b, "jellyfin", "POST", "/Startup/User", {"Name": c["user"], "Password": c["pw"]})[0]) + st, t = J(b, "jellyfin", "POST", "/Startup/Complete") + out.append(st) + return st, f"steps {out}" + + +def emby(b, c): + out = [J(b, "emby", "POST", "/emby/Startup/Configuration", {"UICulture": "en-US", "MetadataCountryCode": "HU", "PreferredMetadataLanguage": "hu"})[0], + J(b, "emby", "POST", "/emby/Startup/User", {"Name": c["user"], "Password": c["pw"]})[0]] + st, t = J(b, "emby", "POST", "/emby/Startup/Complete") + out.append(st) + return st, f"steps {out}" + + +def navidrome(b, c): + return J(b, "navidrome", "POST", "/auth/createAdmin", {"username": c["user"], "password": c["pw"]}) + + +def ghost(b, c): + return J(b, "ghost", "POST", "/ghost/api/admin/authentication/setup/", + {"setup": [{"name": "Family", "email": c["email"], "password": c["pw"] + "Zz9", "blogTitle": "Family"}]}, + headers={"Origin": f"https://{host('ghost')}"}) + + +def home_assistant(b, c): + return J(b, "home-assistant", "POST", "/api/onboarding/users", + {"client_id": f"https://{host('home-assistant')}/", "name": "Family", "username": c["user"], "password": c["pw"], "language": "en"}) + + +def romm(b, c): + J(b, "romm", "GET", "/api/heartbeat") # sets romm's csrftoken cookie, as its setup screen's first call does + tok = b.jar.get(host("romm"), {}).get("romm_csrftoken") or b.jar.get(host("romm"), {}).get("csrftoken", "") + return J(b, "romm", "POST", "/api/users", {"username": c["user"], "password": c["pw"], "email": c["email"], "role": "admin"}, + headers={"X-CSRFToken": tok}) + + +def zipline(b, c): + return J(b, "zipline", "POST", "/api/setup", {"username": c["user"], "password": c["pw"]}) + + +def docmost(b, c): + return J(b, "docmost", "POST", "/api/auth/setup", {"name": "Family", "email": c["email"], "password": c["pw"], "workspaceName": "Family"}) + + +def calcom(b, c): + return J(b, "calcom", "POST", "/api/auth/setup", {"username": c["user"], "email_address": c["email"], "full_name": "Family", "password": c["pw"] + "Aa1!"}) + + +def _form(b, app, path, fields, csrf_name): + import re, urllib.parse + st, html, _ = b.req(f"https://{host(app)}{path}") + m = re.search(r'name="' + csrf_name + r'" value="([^"]+)"', html) + fields[csrf_name] = m.group(1) if m else "" + body = urllib.parse.urlencode(fields) + st, text, hops = b.req(f"https://{host(app)}{path}", "POST", body=body, + headers={"Content-Type": "application/x-www-form-urlencoded", "Referer": f"https://{host(app)}{path}", "Origin": f"https://{host(app)}"}) + return st, f"after POST: {' -> '.join(str(h[0]) + ' ' + h[2] for h in hops)}" + + +def gitea(b, c): + import re + st, html, _ = b.req(f"https://{host('gitea')}/") + fields = dict(re.findall(r'<input[^>]*name="([^"]+)"[^>]*value="([^"]*)"', html)) + for sel in re.findall(r'<select[^>]*name="([^"]+)"', html): + fields.setdefault(sel, "") + fields.update({"db_type": fields.get("db_type") or "sqlite3", "admin_name": "family", "admin_email": c["email"], + "admin_passwd": c["pw"], "admin_confirm_passwd": c["pw"]}) + import urllib.parse + st, text, hops = b.req(f"https://{host('gitea')}/", "POST", body=urllib.parse.urlencode(fields), + headers={"Content-Type": "application/x-www-form-urlencoded"}) + return st, f"after POST: {' -> '.join(str(h[0]) + ' ' + h[2] for h in hops)}" + + +def tandoor(b, c): + return _form(b, "tandoor", "/setup/", {"name": c["user"], "password": c["pw"], "password_confirm": c["pw"]}, "csrfmiddlewaretoken") + + +def homebox(b, c): + return J(b, "homebox", "POST", "/api/v1/users/register", {"name": "Family", "email": c["email"], "password": c["pw"]}) + + +def def_papra_marker(): + pass + + +def papra(b, c): + return J(b, "papra", "POST", "/api/auth/sign-up/email", {"email": c["email"], "password": c["pw"], "name": "Family"}, + headers={"Origin": f"https://{host('papra')}"}) + + +def sparkyfitness(b, c): + return J(b, "sparkyfitness", "POST", "/api/auth/sign-up/email", {"email": c["email"], "password": c["pw"], "name": "Family"}, + headers={"Origin": f"https://{host('sparkyfitness')}"}) + + +def vikunja(b, c): + return J(b, "vikunja", "POST", "/api/v1/register", {"username": c["user"], "email": c["email"], "password": c["pw"]}) + + +def adventurelog(b, c): + import urllib.parse + body = urllib.parse.urlencode({"username": c["user"], "email": c["email"], "password1": c["pw"], "password2": c["pw"], "first_name": "Family", "last_name": "Home"}) + st, t, _ = b.req(f"https://{host('adventurelog')}/signup", "POST", body=body, accept="application/json", + headers={"Content-Type": "application/x-www-form-urlencoded", "Origin": f"https://{host('adventurelog')}", "x-sveltekit-action": "true"}) + return st, t + + +def adventurelog_allauth(b, c): + J(b, "adventurelog", "GET", "/auth/browser/v1/config") + tok = b.jar.get(host("adventurelog"), {}).get("csrftoken", "") + return J(b, "adventurelog", "POST", "/auth/browser/v1/auth/signup", {"username": c["user"], "email": c["email"], "password": c["pw"]}, + headers={"X-CSRFToken": tok, "Referer": f"https://{host('adventurelog')}/", "Origin": f"https://{host('adventurelog')}"}) + + +def termix(b, c): + return J(b, "termix", "POST", "/users/create", {"username": c["user"], "password": c["pw"]}) + + +def opengist(b, c): + return _form(b, "opengist", "/-/register", {"username": c["user"], "password": c["pw"]}, "_csrf") + + +def _arr(b, app, c): + import re + st, html, _ = b.req(f"https://{host(app)}/initialize.json", accept="application/json") + key = json.loads(html).get("apiKey", "") + h = {"X-Api-Key": key} + st, cfg = J(b, app, "GET", "/api/v3/config/host", headers=h) + cfg = json.loads(cfg) + cfg.update({"authenticationMethod": "forms", "authenticationRequired": "enabled", "username": c["user"], + "password": c["pw"], "passwordConfirmation": c["pw"]}) + st, t = J(b, app, "PUT", "/api/v3/config/host", cfg, headers=h) + return st, t[:40].replace(key, "<key>") + + +def radarr(b, c): + return _arr(b, "radarr", c) + + +def sonarr(b, c): + return _arr(b, "sonarr", c) + + +def gramps_web(b, c): + st, t = J(b, "gramps-web", "GET", "/api/token/create_owner/") + tok = json.loads(t).get("access_token", "") if st == 200 else "" + st, t = J(b, "gramps-web", "POST", "/api/users/" + c["user"] + "/create_owner/", {"password": c["pw"], "email": c["email"], "full_name": "Family"}, + headers={"Authorization": "Bearer " + tok}) + return st, t + + +def wishlist(b, c): + import urllib.parse + body = urllib.parse.urlencode({"name": "Family", "username": c["user"], "email": c["email"], "password": c["pw"], "confirmPassword": c["pw"]}) + st, t, _ = b.req(f"https://{host('wishlist')}/signup", "POST", body=body, accept="application/json", + headers={"Content-Type": "application/x-www-form-urlencoded", "Origin": f"https://{host('wishlist')}", "x-sveltekit-action": "true"}) + return st, t + + +FN = {"actualbudget": actualbudget, "komga": komga, "jellyfin": jellyfin, "emby": emby, "navidrome": navidrome, + "ghost": ghost, "home-assistant": home_assistant, "romm": romm, + "zipline": zipline, "docmost": docmost, "calcom": calcom, "gitea": gitea, "tandoor": tandoor, + "homebox": homebox, "papra": papra, "sparkyfitness": sparkyfitness, "vikunja": vikunja, "adventurelog": adventurelog, + "termix": termix, "opengist": opengist, "radarr": radarr, "sonarr": sonarr, "gramps-web": gramps_web, "wishlist": wishlist} + +if __name__ == "__main__": + w.login() + b = ro.household() + for app in sys.argv[1:]: + c = ro.creds(app) + try: + st, text = FN[app](b, c) + except Exception as e: + st, text = "ERR", str(e) + for v in (c["pw"],): + text = str(text).replace(v, "<pw>") + w.say(app, "HOUSEHOLD SETUP through the gate ->", st, text[:120]) + ro.save(b) diff --git a/documentation/audits/signup-lock-2026-09-29/B/tricks.py b/documentation/audits/signup-lock-2026-09-29/B/tricks.py new file mode 100644 index 00000000..b2b77d15 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/tricks.py @@ -0,0 +1,87 @@ +# Part B: a stranger's sign-up through the public route (traefik), each sign-up route of the app in 7 shapes: +# as written, trailing slash, UPPER, Mixed, one letter percent-encoded, a double slash, a query string. +# Each answer: BLOCK (the box's "sign-up is closed"), APP-REFUSED (the app's own refusal), or GOT-IN (an account was +# made / the form was served). A GOT-IN is a hole. +import json, re, secrets, sys, urllib.parse +sys.path.insert(0, '.') +import ro +from ro import w, host +from browser import Browser + +def pw(): return "Xx" + secrets.token_hex(8) + "9!" +def jn(n): return {"username": n, "email": n + "@x.hu", "password": pw()} +FORM = {"Content-Type": "application/x-www-form-urlencoded"} + +# app -> list of (label, method, path, body-maker, headers, got-in test) +ROUTES = { + "gitea": [("web form", "GET", "/user/sign_up", None, {}, lambda st, t: st == 200 and 'name="user_name"' in t)], + "calcom": [("API", "POST", "/api/auth/signup", lambda n: dict(jn(n)), {}, lambda st, t: st in (200, 201)), + ("web page", "GET", "/signup", None, {}, lambda st, t: st == 200 and "Signup is disabled" not in t and "sign-up is closed" not in t and "nem lehet regisztr" not in t and 'name="password"' in t)], + "gramps-web": [("API", "POST", "/api/users/{n}/register/", lambda n: {"email": n + "@x.hu", "password": pw(), "full_name": "S"}, {}, lambda st, t: st in (200, 201))], + "homebox": [("API", "POST", "/api/v1/users/register", lambda n: {"name": n, "email": n + "@x.hu", "password": pw()}, {}, lambda st, t: st in (200, 201, 204))], + "adventurelog": [("web form action", "POST", "/signup", lambda n: urllib.parse.urlencode({"username": n, "email": n + "@x.hu", "password1": "Xx12345678z9!", "password2": "Xx12345678z9!", "first_name": "S", "last_name": "S"}), + dict(FORM, **{"x-sveltekit-action": "true"}), lambda st, t: '"type":"redirect"' in t and '"location":"/"' in t), + ("allauth API", "POST", "/auth/browser/v1/auth/signup", lambda n: jn(n), {}, lambda st, t: st in (200, 201)), + ("allauth classic", "POST", "/accounts/signup/", lambda n: urllib.parse.urlencode({"username": n, "email": n + "@x.hu", "password1": "Xx12345678z9!", "password2": "Xx12345678z9!"}), FORM, lambda st, t: st in (200, 302) and "sign-up is closed" not in t and "nem lehet" not in t and "CSRF" not in t and "Forbidden" not in t)], + "papra": [("API", "POST", "/api/auth/sign-up/email", lambda n: {"email": n + "@x.hu", "password": pw(), "name": n}, {}, lambda st, t: st in (200, 201) and "token" in t)], + "sparkyfitness": [("API", "POST", "/api/auth/sign-up/email", lambda n: {"email": n + "@x.hu", "password": pw(), "name": n}, {}, lambda st, t: st in (200, 201) and "token" in t)], + "termix": [("API", "POST", "/users/create", lambda n: {"username": n, "password": pw()}, {}, lambda st, t: st in (200, 201) and "User created" in t)], + "vikunja": [("API", "POST", "/api/v1/register", lambda n: jn(n), {}, lambda st, t: st in (200, 201) and '"id"' in t), + ("web page", "GET", "/register", None, {}, lambda st, t: False)], + "opengist": [("web form", "POST", "/-/register", "FORM-CSRF", {}, None)], + "wishlist": [("web form action", "POST", "/signup", lambda n: urllib.parse.urlencode({"name": "S", "username": n, "email": n + "@x.hu", "password": "Xx12345678z9!", "confirmPassword": "Xx12345678z9!"}), + dict(FORM, **{"x-sveltekit-action": "true"}), lambda st, t: '"type":"success"' in t)], +} + +def variants(path): + base = path.rstrip("/") + segs = base.split("/") + last = segs[-1] or segs[-2] + enc = last[0] + "%" + format(ord(last[1]), "02X") + last[2:] if len(last) > 2 else last + mixed = "/".join(s[:1].upper() + s[1:] for s in segs) + lastup = "/".join(segs[:-1] + [segs[-1].upper()]) + out = [("as written", path), ("trailing slash", base + "/"), ("UPPER", base.upper()), ("Mixed", mixed), ("last part UPPER", lastup), + ("percent-encoded", "/".join(segs[:-1] + [enc])), ("double slash", "/" + base), ("query string", base + "?x=1")] + seen, res = set(), [] + for k, p in out: + if p not in seen: + seen.add(p); res.append((k, p)) + return res + +def classify(st, t, got, m="POST"): + if "sign-up is closed" in t or "nem lehet regisztr" in t or "Sign-up closed" in t: + return "BLOCK" + # A POST answered by the app's HTML shell (its fallback for an unknown address) made no account: the app's own + # router is case-sensitive, so the tricked address never reached its sign-up code. Measured 2026-09-29: the + # homebox login of such a name answers "unauthorized". + if m == "POST" and t.lstrip()[:15].lower().startswith(("<!doctype", "<html")) and got(st, t): + return "APP-FALLBACK (no account)" + if got(st, t): + return "GOT-IN" + return "APP-REFUSED" + +w.login() +for app in sys.argv[1:]: + for label, m, path, body, hd, got in ROUTES[app]: + for kind, p in variants(path): + n = "trk" + secrets.token_hex(3) + p2 = p.replace("{n}", n).replace("{N}", n.upper()) + s = Browser("stranger") + h = dict(hd); h["Origin"] = f"https://{host(app)}" + if body == "FORM-CSRF": # opengist: fetch the form (its CSRF) at the same shape, then post it + st0, html, _ = s.req(f"https://{host(app)}{p2}") + mm = re.search(r'name="_csrf" value="([^"]+)"', html) + if not mm: + verdict = "BLOCK" if ("nem lehet regisztr" in html or "sign-up is closed" in html) else "APP-REFUSED" + print(f"{app:14} {label:16} {kind:16} {p2:42} -> {st0} {verdict} (no form served)") + continue + b = urllib.parse.urlencode({"username": n, "password": "Xx12345678z9!", "_csrf": mm.group(1)}) + st, t, hops = s.req(f"https://{host(app)}{p2}", "POST", body=b, headers=dict(FORM, Origin=h["Origin"])) + gotin = st == 200 and hops[-1][2] in ("/", "/-/all") and len(hops) > 1 + verdict = "BLOCK" if ("nem lehet regisztr" in t or "sign-up is closed" in t) else ("GOT-IN" if gotin else "APP-REFUSED") + print(f"{app:14} {label:16} {kind:16} {p2:42} -> {st} {verdict}") + continue + b = body(n) if callable(body) else None + accept = "text/html" if m == "GET" else "application/json" + st, t, _ = s.req(f"https://{host(app)}{p2}", m, body=b, accept=accept, headers=h, follow=(m == "GET")) + print(f"{app:14} {label:16} {kind:16} {p2:42} -> {st} {classify(st, t, got, m)}") diff --git a/documentation/audits/signup-lock-2026-09-29/B/tx.sh b/documentation/audits/signup-lock-2026-09-29/B/tx.sh new file mode 100644 index 00000000..1d36d2c1 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/tx.sh @@ -0,0 +1 @@ +for p in /users/CREATE /Users/Create /USERS/CREATE; do echo "termix $p -> $(curl -sk -o /tmp/tx -w %{http_code} -H "Host: r-termix.enkisfelhom.hu" -H "Content-Type: application/json" -X POST -d "{\"username\":\"trkx$RANDOM\",\"password\":\"Xx12345678z9\"}" https://127.0.0.1$p) $(head -c 80 /tmp/tx)"; done; for p in /api/auth/SIGNUP /API/AUTH/SIGNUP; do echo "calcom $p -> $(curl -sk -o /tmp/tx -w %{http_code} -H "Host: r-calcom.enkisfelhom.hu" -H "Content-Type: application/json" -X POST -d "{\"username\":\"trky$RANDOM\",\"email\":\"y$RANDOM@x.hu\",\"password\":\"Xx12345678z9!A\"}" https://127.0.0.1$p) $(head -c 80 /tmp/tx)"; done; rm -f /tmp/tx diff --git a/documentation/audits/signup-lock-2026-09-29/B/wd.sh b/documentation/audits/signup-lock-2026-09-29/B/wd.sh new file mode 100644 index 00000000..546c29b7 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/wd.sh @@ -0,0 +1,6 @@ +D=enkisfelhom.hu; DBH=$(grep -o 'SUBDOMAIN_DB: [a-z0-9-]*' /opt/docker/stacks/wanderer/app.yaml | awk '{print $2}') +echo "db host: $DBH.$D" +echo "PocketBase admin UI /_/ (via traefik, stranger) -> $(curl -sk -o /tmp/w1 -w '%{http_code}' -H "Host: $DBH.$D" https://127.0.0.1/_/) $(grep -oiE 'superuser|installer|pbinstal[a-z]*' /tmp/w1 | sort -u | tr '\n' ' ')" +echo "PB superusers count (API, stranger) -> $(curl -sk -H "Host: $DBH.$D" https://127.0.0.1/api/collections/_superusers/records | head -c 120)" +echo "stranger creates a wanderer user straight through PocketBase (via traefik) -> $(curl -sk -o /tmp/w2 -w '%{http_code}' -H "Host: $DBH.$D" -H 'Content-Type: application/json' -X POST -d '{"username":"strngrpb","email":"spb@x.hu","password":"Xx12345678z9","passwordConfirm":"Xx12345678z9"}' https://127.0.0.1/api/collections/users/records) $(head -c 140 /tmp/w2)" +rm -f /tmp/w1 /tmp/w2 diff --git a/documentation/audits/signup-lock-2026-09-29/B/wd2.sh b/documentation/audits/signup-lock-2026-09-29/B/wd2.sh new file mode 100644 index 00000000..db43d185 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/wd2.sh @@ -0,0 +1 @@ +docker logs wanderer-db 2>&1 | grep -iE "superuser|pbinstal|installer" | sed -E "s/pbinstal[^ ]*/pbinstal<token-redacted>/g" | head -5; docker exec felhom-controller curl -s http://wanderer-db:8090/api/health | head -c 100; echo diff --git a/documentation/audits/signup-lock-2026-09-29/B/wl.sh b/documentation/audits/signup-lock-2026-09-29/B/wl.sh new file mode 100644 index 00000000..8518f644 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/wl.sh @@ -0,0 +1 @@ +docker exec wishlist sh -c "grep -n -A6 \"^model SystemConfig\" /usr/src/app/prisma/schema.prisma 2>/dev/null | head -12; ls /usr/src/app/prisma | head; grep -rn \"enableSignup\" /usr/src/app/build/server/chunks/*.js 2>/dev/null | head -3 | cut -c1-200" diff --git a/documentation/audits/signup-lock-2026-09-29/B/wtr.sh b/documentation/audits/signup-lock-2026-09-29/B/wtr.sh new file mode 100644 index 00000000..4b24c486 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/wtr.sh @@ -0,0 +1,6 @@ +DB=hike-db.enkisfelhom.hu; n=0 +for p in /api/collections/users/records /api/collections/users/records/ /API/COLLECTIONS/USERS/RECORDS /Api/Collections/Users/Records /api/collections/users/r%65cords //api/collections/users/records "/api/collections/users/records?x=1" /api/collections/_pb_users_auth_/records; do + n=$((n+1)); c=$(curl -sk -o /tmp/t.b -w '%{http_code}' -H "Host: $DB" -H 'Content-Type: application/json' -X POST -d "{\"username\":\"trk$n$$\",\"email\":\"trk$n$$@x.hu\",\"password\":\"Xx12345678z9\",\"passwordConfirm\":\"Xx12345678z9\"}" "https://127.0.0.1$p") + v=$(grep -q 'sign-up is closed' /tmp/t.b && echo BLOCK || (grep -q '"collectionName":"users"' /tmp/t.b && echo GOT-IN || echo APP-REFUSED)) + printf '%-45s -> %s %s\n' "$p" "$c" "$v" +done; rm -f /tmp/t.b diff --git a/documentation/audits/signup-lock-2026-09-29/B/wtr2.sh b/documentation/audits/signup-lock-2026-09-29/B/wtr2.sh new file mode 100644 index 00000000..c4356003 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/B/wtr2.sh @@ -0,0 +1,8 @@ +DB=hike-db.enkisfelhom.hu; n=0 +t() { n=$((n+1)); c=$(curl -sk -o /tmp/t.b -w '%{http_code}' -H "Host: $DB" -H 'Content-Type: application/json' -X POST -d "$2" "https://127.0.0.1$1"); v=$(grep -q 'sign-up is closed' /tmp/t.b && echo BLOCK || (grep -q '"collectionName":"users"\|"collectionId":"_pb_users_auth_"' /tmp/t.b && echo GOT-IN || echo APP-REFUSED)); printf '%-50s -> %s %s %s\n' "$1" "$c" "$v" "$(head -c 70 /tmp/t.b)"; } +U() { echo "{\"username\":\"trk$1$$\",\"email\":\"trk$1$$@x.hu\",\"password\":\"Xx12345678z9\",\"passwordConfirm\":\"Xx12345678z9\"}"; } +t /api/collections/USERS/records "$(U a)" +t /api/collections/Users/records "$(U b)" +t /api/collections/_PB_USERS_AUTH_/records "$(U c)" +t /api/batch "{\"requests\":[{\"method\":\"POST\",\"url\":\"/api/collections/users/records\",\"body\":$(U d)}]}" +rm -f /tmp/t.b diff --git a/documentation/audits/signup-lock-2026-09-29/C/C1-demo-boxes-close-signup.txt b/documentation/audits/signup-lock-2026-09-29/C/C1-demo-boxes-close-signup.txt new file mode 100644 index 00000000..b5a38550 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/C/C1-demo-boxes-close-signup.txt @@ -0,0 +1,33 @@ +# Part C (decision 49) on the demo boxes, 2026-09-29 ~15:16Z, controller 0.282.0, catalog 6446197. Operator ruling allows this press. +# The 'open' proof is the app answering its OWN validation to an invalid sign-up — no account was created (the apps' admin passwords are the operator's; a test account could not be deleted through their admin pages). +# The first run (15:15Z) found no domain and changed NOTHING (all 404) — kept below the real run for honesty. +== hp +domain enkisfelhom.hu; apps adventurelog opengist; 2026-09-29T15:16:32Z + [BEFORE] adventurelog: its own switch "is_disabled":false | an invalid sign-up (mismatched) -> 200 the app answered + [BEFORE] adventurelog: login page -> 200 + [BEFORE] opengist: sign-up form served=1 | an invalid sign-up (empty name) -> 500 the app answered + [BEFORE] opengist: login page -> 200 + PRESS adventurelog: the card was on the page=1 -> {"data":{"closed":true},"error":"","ok":true} + PRESS opengist: the card was on the page=1 -> {"data":{"closed":true},"error":"","ok":true} + [AFTER] adventurelog: its own switch | an invalid sign-up (mismatched) -> 403 BLOCKED + [AFTER] adventurelog: login page -> 500 + [AFTER] opengist: sign-up form served=0 | an invalid sign-up (empty name) -> 403 BLOCKED + [AFTER] opengist: login page -> 200 + adventurelog app.yaml: state: open opened_by: close-signup after_setup: at: "2026-09-29T15:16:35Z" ok: true + opengist app.yaml: state: open opened_by: close-signup after_setup: + files: controller.yml serverstransports.yml signup-block-adventurelog.yml signup-block-opengist.yml +== felhom-pve +domain demo-felhom.eu; apps opengist; 2026-09-29T15:16:56Z + [BEFORE] opengist: sign-up form served=1 | an invalid sign-up (empty name) -> 500 the app answered + [BEFORE] opengist: login page -> 200 + PRESS opengist: the card was on the page=1 -> {"data":{"closed":true},"error":"","ok":true} + [AFTER] opengist: sign-up form served=0 | an invalid sign-up (empty name) -> 403 BLOCKED + [AFTER] opengist: login page -> 200 + opengist app.yaml: state: open opened_by: close-signup after_setup: + files: controller.yml serverstransports.yml signup-block-opengist.yml + +# adventurelog after the press (demo-hp): backend recreated once (~30 s: the one 500 above), same pinned images, its own switch on: +adventurelog ghcr.io/seanmorley15/adventurelog-backend:v0.13.0 Up About a minute (healthy) +adventurelog-postgres postgis/postgis:16-3.5-alpine Up 13 hours (healthy) +adventurelog-frontend ghcr.io/seanmorley15/adventurelog-frontend:v0.13.0 Up 13 hours (healthy) +{"is_disabled":true,"message":"Registrat diff --git a/documentation/audits/signup-lock-2026-09-29/C/c.sh b/documentation/audits/signup-lock-2026-09-29/C/c.sh new file mode 100644 index 00000000..683f9b99 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/C/c.sh @@ -0,0 +1,35 @@ +# Part C (decision 49) on a demo box: before / the household's press / after. No account is created: the "open" +# proof is the app answering its OWN validation (an invalid sign-up). Controller password on STDIN, never printed. +set -u +IFS= read -r PW +D=$(grep -hoE 'Host\(`felhom\.[a-z0-9.-]+`\)' /opt/docker/stacks/traefik/dynamic/controller.yml | head -1 | sed 's/Host(`felhom\.//;s/`)//') +[ -n "$D" ] || { echo "no domain found — stopping, nothing changed"; exit 1; } +APPS="$*"; J=/tmp/pc49.$$; CH="felhom.$D" +echo "domain $D; apps $APPS; $(date -u +%FT%TZ)" +probe() { # $1 app $2 phase + case $1 in + opengist) H="gist.$D" + f=$(curl -sk -H "Host: $H" https://127.0.0.1/-/register | grep -c 'name="username"') + c=$(curl -sk -o /tmp/pc.b -w '%{http_code}' -H "Host: $H" -H 'Content-Type: application/x-www-form-urlencoded' -X POST -d 'username=&password=' https://127.0.0.1/-/register) + echo " [$2] opengist: sign-up form served=$f | an invalid sign-up (empty name) -> $c $(grep -q 'sign-up is closed\|nem lehet regisztr' /tmp/pc.b && echo BLOCKED || echo 'the app answered')" + echo " [$2] opengist: login page -> $(curl -sk -o /dev/null -w '%{http_code}' -H "Host: $H" https://127.0.0.1/-/login)";; + adventurelog) H="travel.$D" + d=$(docker exec felhom-controller curl -s -m 5 http://adventurelog:80/auth/is-registration-disabled/ | grep -o '"is_disabled":[a-z]*') + c=$(curl -sk -o /tmp/pc.b -w '%{http_code}' -H "Host: $H" -H "Origin: https://$H" -H 'x-sveltekit-action: true' -H 'Content-Type: application/x-www-form-urlencoded' -X POST -d 'username=&email=&password1=a&password2=b' https://127.0.0.1/signup) + echo " [$2] adventurelog: its own switch $d | an invalid sign-up (mismatched) -> $c $(grep -q 'sign-up is closed\|nem lehet regisztr' /tmp/pc.b && echo BLOCKED || echo 'the app answered')" + echo " [$2] adventurelog: login page -> $(curl -sk -o /dev/null -w '%{http_code}' -H "Host: $H" https://127.0.0.1/login)";; + esac; rm -f /tmp/pc.b; } +for a in $APPS; do probe $a BEFORE; done +# the household's press, through the dashboard (the product's own endpoint) +curl -sk -c $J -b $J -H "Host: $CH" -o /dev/null --data-urlencode "password=$PW" https://127.0.0.1/login +CSRF=$(curl -sk -c $J -b $J -H "Host: $CH" -L https://127.0.0.1/ | grep -o '<meta name="csrf-token" content="[^"]*"' | sed 's/.*content="//;s/"$//') +for a in $APPS; do + pg=$(curl -sk -b $J -H "Host: $CH" https://127.0.0.1/apps/$a | grep -c 'id="close-signup-card"') + r=$(curl -sk -b $J -H "Host: $CH" -H "X-CSRF-Token: $CSRF" -H 'Content-Type: application/json' -X POST https://127.0.0.1/apps/$a/close-signup) + echo " PRESS $a: the card was on the page=$pg -> $r" +done +sleep 20 +for a in $APPS; do probe $a AFTER; done +for a in $APPS; do echo " $a app.yaml: $(grep -A6 '^setup_gate:' /opt/docker/stacks/$a/app.yaml | grep -oE 'state: [a-z]+|opened_by: [a-z-]+' | tr '\n' ' ') after_setup: $(awk '/^after_setup:/{f=1;next} f&&/^[a-z]/{f=0} f' /opt/docker/stacks/$a/app.yaml | tr -s ' ' | tr '\n' ' ' | cut -c1-200)"; done +echo " files: $(ls /opt/docker/stacks/traefik/dynamic | tr '\n' ' ')" +rm -f $J; unset PW diff --git a/documentation/audits/signup-lock-2026-09-29/D/D0-wanderer-pb-url.txt b/documentation/audits/signup-lock-2026-09-29/D/D0-wanderer-pb-url.txt new file mode 100644 index 00000000..9e8130fb --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/D/D0-wanderer-pb-url.txt @@ -0,0 +1,10 @@ + provider["img"] = await imageUrlToBase64(`${public_env.PUBLIC_POCKETBASE_URL}/_/images/oauth2/${provider.name.replace(/\s*\(\d+\)$/, "")}.svg`, event.fetch); + const pb = new PocketBase(public_env.PUBLIC_POCKETBASE_URL); + auth = async ({ event, resolve }) => {\n\tconst pb = new PocketBase(public_env.PUBLIC_POCKETBASE_URL);\n\tconst url = new URL(event.request.url);\n\tif (event.request.headers.has(\"Authorization\") && +.js";var i=globalThis.__sveltekit_1qbj9l3.env,a;function o(){return a||=new n(i.PUBLIC_POCKETBASE_URL),a}var s=e();async function c(e){let t=await fetch(`/api/v1/user`,{method:`PUT`,body:JSON.stringify +h2.providers) {\n\t\t\tprovider[\"img\"] = await imageUrlToBase64(`${public_env.PUBLIC_POCKETBASE_URL}/_/images/oauth2/${provider.name.replace(/\\s*\\(\\d+\\)$/, \"\")}.svg`, event.fetch);\n\t\t\tprovi + if (public_env.PUBLIC_DISABLE_SIGNUP !== "true") { + else if (public_env.PUBLIC_DISABLE_SIGNUP === "true" && url.pathname === "/register") throw redirect(302, "/"); + if (public_env.PUBLIC_DISABLE_SIGNUP === "true") throw new ClientResponseError({ +=>m()(`no-account`),()=>m()(`make-one`)]),h(t,n)};a(f,e=>{O.PUBLIC_DISABLE_SIGNUP!==`true`&&e(p)}),v(e=>w(u,e),[()=>m()(`forgot-your-password`)]),h(t,n)};a($,e=> +er Error\n*/\nasync function PUT(event) {\n\tif (public_env.PUBLIC_DISABLE_SIGNUP === \"true\") throw new ClientResponseError({\n\t\tstatus: 401,\n\t\tresponse: diff --git a/documentation/audits/signup-lock-2026-09-29/D/D1-wanderer-deploy.txt b/documentation/audits/signup-lock-2026-09-29/D/D1-wanderer-deploy.txt new file mode 100644 index 00000000..80f5b14c --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/D/D1-wanderer-deploy.txt @@ -0,0 +1,3 @@ +16:48:19 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['SUBDOMAIN_DB'] +16:48:19 wanderer deploy -> 202 +16:50:28 wanderer state unhealthy | files=[] record=None diff --git a/documentation/audits/signup-lock-2026-09-29/D/D2-wanderer-open.txt b/documentation/audits/signup-lock-2026-09-29/D/D2-wanderer-open.txt new file mode 100644 index 00000000..b71ae9a9 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/D/D2-wanderer-open.txt @@ -0,0 +1,9 @@ +db host: hike-db.enkisfelhom.hu +PocketBase admin UI /_/ (via traefik, stranger) -> 200 +PB superusers count (API, stranger) -> {"data":{},"message":"Only superusers can perform this action.","status":403} +stranger creates a wanderer user straight through PocketBase (via traefik) -> 200 {"avatar":"","collectionId":"_pb_users_auth_","collectionName":"users","created":"2026-09-29 14:50:30.864Z","emailVisibility":false,"id":"6c +(!) Launch the URL below in the browser if it hasn't been open already to create your first superuser account: +http://0.0.0.0:8090/_/#/pbinstal<token-redacted> +(you can also create your first superuser by running: /pocketbase superuser upsert EMAIL PASS) +{"message":"API is healthy.","code":200,"data":{}} + diff --git a/documentation/audits/signup-lock-2026-09-29/D/D3-remove.txt b/documentation/audits/signup-lock-2026-09-29/D/D3-remove.txt new file mode 100644 index 00000000..1fd09d5a --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/D/D3-remove.txt @@ -0,0 +1,3 @@ +16:51:38 [X] stop -> 200 {'ok': True, 'message': 'Stack wanderer stop completed'} +16:52:10 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'wanderer', 'volumes_removed': ['wanderer_wanderer_data', 'wanderer_wanderer_meili_data', 'wanderer_wanderer_plugins', 'wandere +16:52:18 [X] after remove: deployed=False leftovers='/opt/docker/stacks/wanderer' diff --git a/documentation/audits/signup-lock-2026-09-29/D/D4-deploy.txt b/documentation/audits/signup-lock-2026-09-29/D/D4-deploy.txt new file mode 100644 index 00000000..3f07276d --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/D/D4-deploy.txt @@ -0,0 +1,3 @@ +16:52:27 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['SUBDOMAIN_DB'] +16:52:27 wanderer deploy -> 202 +16:54:27 wanderer state unhealthy | files=[] record=None diff --git a/documentation/audits/signup-lock-2026-09-29/D/D5-wanderer-close-signup.txt b/documentation/audits/signup-lock-2026-09-29/D/D5-wanderer-close-signup.txt new file mode 100644 index 00000000..9b5bdbf6 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/D/D5-wanderer-close-signup.txt @@ -0,0 +1,10 @@ +16:54:30 BEFORE: household makes its account through PocketBase -> 200 +16:54:33 BEFORE: a stranger makes one too -> 200 +16:54:33 app page offers 'close sign-up now': True +16:54:33 household presses 'Close sign-up now' -> 200 {'data': {'closed': True}, 'error': '', 'ok': True} +16:54:44 AFTER: files=[signup-block-wanderer.yml] record={'state': 'open', 'since': '2026-09-29T14:54:33Z', 'hosts': ['hike-db.enkisfelhom.hu', 'r-wanderer.enkisfelhom.hu'], 'opened_at': '2026-09-29T14:54:33Z', 'opened_by': 'close-signup', 'native_lock': 'applied'} | web container PUBLIC_DISABLE_SIGNUP = true +16:54:47 AFTER: a stranger through PocketBase -> 403 +16:54:50 AFTER: a stranger on /register (web host) -> 403 +16:54:53 AFTER: PocketBase health (the rest still answers) -> 200 +16:54:53 app page: close card gone: True | sign-up card shown: True +16:54:53 second press -> 409 {'data': None, 'error': 'Ennél az alkalmazásnál nincs mit le diff --git a/documentation/audits/signup-lock-2026-09-29/D/D6-wanderer-tricks.txt b/documentation/audits/signup-lock-2026-09-29/D/D6-wanderer-tricks.txt new file mode 100644 index 00000000..5696160f --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/D/D6-wanderer-tricks.txt @@ -0,0 +1,8 @@ +/api/collections/users/records -> 403 BLOCK +/api/collections/users/records/ -> 403 BLOCK +/API/COLLECTIONS/USERS/RECORDS -> 404 APP-REFUSED +/Api/Collections/Users/Records -> 404 APP-REFUSED +/api/collections/users/r%65cords -> 403 BLOCK +//api/collections/users/records -> 403 BLOCK +/api/collections/users/records?x=1 -> 403 BLOCK +/api/collections/_pb_users_auth_/records -> 200 GOT-IN diff --git a/documentation/audits/signup-lock-2026-09-29/D/D7-wanderer-tricks-2.txt b/documentation/audits/signup-lock-2026-09-29/D/D7-wanderer-tricks-2.txt new file mode 100644 index 00000000..82e4e606 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/D/D7-wanderer-tricks-2.txt @@ -0,0 +1,4 @@ +/api/collections/USERS/records -> 200 GOT-IN {"avatar":"","collectionId":"_pb_users_auth_","collectionName":"users" +/api/collections/Users/records -> 200 GOT-IN {"avatar":"","collectionId":"_pb_users_auth_","collectionName":"users" +/api/collections/_PB_USERS_AUTH_/records -> 404 APP-REFUSED {"data":{},"message":"Missing or invalid collection context.","status" +/api/batch -> 403 APP-REFUSED {"data":{},"message":"Batch requests are not allowed.","status":403} diff --git a/documentation/audits/signup-lock-2026-09-29/D/D8-wanderer-tricks-after-fix.txt b/documentation/audits/signup-lock-2026-09-29/D/D8-wanderer-tricks-after-fix.txt new file mode 100644 index 00000000..e29fca18 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/D/D8-wanderer-tricks-after-fix.txt @@ -0,0 +1,12 @@ +/api/collections/users/records -> 403 BLOCK +/api/collections/users/records/ -> 403 BLOCK +/API/COLLECTIONS/USERS/RECORDS -> 403 BLOCK +/Api/Collections/Users/Records -> 403 BLOCK +/api/collections/users/r%65cords -> 403 BLOCK +//api/collections/users/records -> 403 BLOCK +/api/collections/users/records?x=1 -> 403 BLOCK +/api/collections/_pb_users_auth_/records -> 403 BLOCK +/api/collections/USERS/records -> 403 BLOCK {"error":"sign-up is closed on this app; its admin adds new accounts"} +/api/collections/Users/records -> 403 BLOCK {"error":"sign-up is closed on this app; its admin adds new accounts"} +/api/collections/_PB_USERS_AUTH_/records -> 403 BLOCK {"error":"sign-up is closed on this app; its admin adds new accounts"} +/api/batch -> 403 APP-REFUSED {"data":{},"message":"Batch requests are not allowed.","status":403} diff --git a/documentation/audits/signup-lock-2026-09-29/E/E1-press-before-setup.txt b/documentation/audits/signup-lock-2026-09-29/E/E1-press-before-setup.txt new file mode 100644 index 00000000..2cd4b844 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/E/E1-press-before-setup.txt @@ -0,0 +1,3 @@ +16:28:30 ghost PRESS 'Done' -> 409 {'data': None, 'error': 'Az alkalmazás szerint még nincs kész az első beállítás. Hozd létre a fiókodat, aztán próbáld új +16:28:36 home-assistant PRESS 'Done' -> 409 {'data': None, 'error': 'Az alkalmazás szerint még nincs kész az első beállítás. Hozd létre a fiókodat, aztán próbáld új +16:28:41 gramps-web PRESS 'Done' -> 409 {'data': None, 'error': 'Az alkalmazás szerint még nincs kész az első beállítás. Hozd létre a fiókodat, aztán próbáld új diff --git a/documentation/audits/signup-lock-2026-09-29/E/E2-probe-before.txt b/documentation/audits/signup-lock-2026-09-29/E/E2-probe-before.txt new file mode 100644 index 00000000..4975af0d --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/E/E2-probe-before.txt @@ -0,0 +1,3 @@ +16:28:50 ghost PROBE http://ghost:2368/ghost/api/admin/authentication/setup/ -> {"setup":[{"status":false}]} +16:28:53 home-assistant PROBE http://home-assistant:8123/api/onboarding -> [{"step":"user","done":false},{"step":"core_config","done":false},{"step":"analytics","done":false},{"step":"integration","done":false}] +gramps-web create_owner BEFORE: HTTP 200 diff --git a/documentation/audits/signup-lock-2026-09-29/E/E3-open-by-probe.txt b/documentation/audits/signup-lock-2026-09-29/E/E3-open-by-probe.txt new file mode 100644 index 00000000..b0f64c30 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/E/E3-open-by-probe.txt @@ -0,0 +1,3 @@ +16:29:24 ghost gate OPENED by probe 0s after the setup +16:29:24 gramps-web gate OPENED by probe 0s after the setup +16:29:25 home-assistant gate OPENED by probe 0s after the setup diff --git a/documentation/audits/signup-lock-2026-09-29/redproofs/RP25.txt b/documentation/audits/signup-lock-2026-09-29/redproofs/RP25.txt new file mode 100644 index 00000000..ed8d9c9d --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/redproofs/RP25.txt @@ -0,0 +1,9 @@ +# RP25 — the gate opening sets the app's own switch +# mutation in internal/stacks/setup_gate.go: +# - '\tif st.Meta.AfterSetup != nil {\n\t\tm.goNativeLock(name, true, "the gate opened ("+by+")")' +# + '\tif false && st.Meta.AfterSetup != nil { // RED-PROOF RP25\n\t\tm.goNativeLock(name, true, "the gate opened ("+by+")")' +# go test -run ^TestAfterSetup_TheGateOpeningSetsTheAppsOwnSwitch$ ./internal/stacks +# verdict: RED (assertion) +=== RUN TestAfterSetup_TheGateOpeningSetsTheAppsOwnSwitch + after_setup_test.go:59: the app's own switch was not set: env="" starts=0 +--- FAIL: TestAfterSetup_TheGateOpeningSetsTheAppsOwnSwitch (0.01s) diff --git a/documentation/audits/signup-lock-2026-09-29/redproofs/RP26.txt b/documentation/audits/signup-lock-2026-09-29/redproofs/RP26.txt new file mode 100644 index 00000000..6cd9ea13 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/redproofs/RP26.txt @@ -0,0 +1,9 @@ +# RP26 — the loop closes the switch again after the window +# mutation in internal/stacks/signup_block.go: +# - '\t\t\tif due {\n\t\t\t\tm.goNativeLock(n, true,' +# + '\t\t\tif false && due { // RED-PROOF RP26\n\t\t\t\tm.goNativeLock(n, true,' +# go test -run ^TestAfterSetup_TheWindowLiftsItAndTheLoopClosesItAgain$ ./internal/stacks +# verdict: RED (assertion) +=== RUN TestAfterSetup_TheWindowLiftsItAndTheLoopClosesItAgain + after_setup_test.go:88: the switch stayed open after the window: env="" native="lifted" starts=2 +--- FAIL: TestAfterSetup_TheWindowLiftsItAndTheLoopClosesItAgain (0.01s) diff --git a/documentation/audits/signup-lock-2026-09-29/redproofs/RP27.txt b/documentation/audits/signup-lock-2026-09-29/redproofs/RP27.txt new file mode 100644 index 00000000..b151ff6e --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/redproofs/RP27.txt @@ -0,0 +1,9 @@ +# RP27 — an old compose is reported, not faked +# mutation in internal/stacks/after_setup.go: +# - '\t\tif miss := composeMissingVars(st.ComposePath, spec.Env); len(miss) > 0 {' +# + '\t\tif miss := composeMissingVars(st.ComposePath, spec.Env); false && len(miss) > 0 { // RED-PROOF RP27' +# go test -run ^TestAfterSetup_AnOldComposeIsReportedNotFaked$ ./internal/stacks +# verdict: RED (assertion) +=== RUN TestAfterSetup_AnOldComposeIsReportedNotFaked + after_setup_test.go:100: an unread switch was recorded as set: &{At:2026-09-29T14:18:34Z OK:true Detail:} native="applied" starts=1 +--- FAIL: TestAfterSetup_AnOldComposeIsReportedNotFaked (0.01s) diff --git a/documentation/audits/signup-lock-2026-09-29/redproofs/RP28.txt b/documentation/audits/signup-lock-2026-09-29/redproofs/RP28.txt new file mode 100644 index 00000000..be1bc7ba --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/redproofs/RP28.txt @@ -0,0 +1,9 @@ +# RP28 — close sign-up is offered once +# mutation in internal/stacks/after_setup.go: +# - '\tif !ok || !st.Deployed || st.AppConfig == nil || st.AppConfig.SetupGate != nil {\n\t\treturn false' +# + '\tif !ok || !st.Deployed || st.AppConfig == nil { // RED-PROOF RP28\n\t\treturn false' +# go test -run ^TestCloseSignup_OnceOnAnAppInstalledBeforeTheRule$ ./internal/stacks +# verdict: RED (assertion) +=== RUN TestCloseSignup_OnceOnAnAppInstalledBeforeTheRule + after_setup_test.go:139: offered twice +--- FAIL: TestCloseSignup_OnceOnAnAppInstalledBeforeTheRule (0.01s) diff --git a/documentation/audits/signup-lock-2026-09-29/redproofs/RP29.txt b/documentation/audits/signup-lock-2026-09-29/redproofs/RP29.txt new file mode 100644 index 00000000..86347eeb --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/redproofs/RP29.txt @@ -0,0 +1,10 @@ +# RP29 — R-715: a probe field indexes a list +# mutation in internal/stacks/setup_gate.go: +# - '\t\tcase []interface{}:\n\t\t\tn, err := strconv.Atoi(k)' +# + '\t\tcase []int: // RED-PROOF RP29\n\t\t\tn, err := strconv.Atoi(k)' +# go test -run ^TestProbe_ListIndexesAndADoneStatus$ ./internal/stacks +# verdict: RED (assertion) +=== RUN TestProbe_ListIndexesAndADoneStatus + after_setup_test.go:161: {"setup":[{"status":true}]} @ setup.0.status: false, want true + after_setup_test.go:161: [{"step":"user","done":true},{"step":"core_config","done":false}] @ 0.done: false, want true +--- FAIL: TestProbe_ListIndexesAndADoneStatus (0.00s) diff --git a/documentation/audits/signup-lock-2026-09-29/redproofs/RP30.txt b/documentation/audits/signup-lock-2026-09-29/redproofs/RP30.txt new file mode 100644 index 00000000..0ff2f4a5 --- /dev/null +++ b/documentation/audits/signup-lock-2026-09-29/redproofs/RP30.txt @@ -0,0 +1,9 @@ +# RP30 — R-715: a done status counts as done +# mutation in internal/stacks/setup_gate.go: +# - '\tif p.DoneStatus != 0 && status == p.DoneStatus {' +# + '\tif false && p.DoneStatus != 0 && status == p.DoneStatus { // RED-PROOF RP30' +# go test -run ^TestProbe_ListIndexesAndADoneStatus$ ./internal/stacks +# verdict: RED (assertion) +=== RUN TestProbe_ListIndexesAndADoneStatus + after_setup_test.go:174: gramps-web after its setup (405) not read as done: HTTP 405 +--- FAIL: TestProbe_ListIndexesAndADoneStatus (0.00s) diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index 39848d26..a3287e52 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -825,9 +825,11 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server` | **R-711** | **[P2-MEDIUM] About a dozen class-4 apps keep open sign-up after their first admin exists — the setup gate (decision 46) does not close that.** FOUND 2026-09-29 by the gate spike (`audits/login-gate-2026-09-29/B/B-VERDICT.md` F3). The gate decides who becomes the admin; once it opens, a stranger can still make an ordinary account on adventurelog, homebox, papra, plant-it, sparkyfitness, vikunja, wanderer, rallly, opengist, wishlist, termix, docmost (READ from `app-catalog-felhom.eu/FIRST-ADMIN.md`, not measured). **Fix direction:** per app, route (a) — disable sign-up after the first user (env or the app's own setting), measured on 9202. **Built and proven (decision 47, controller v0.281.0, catalog `6faf432`):** a `signup_block:` per app, written when the gate opens (before the gate comes down), answered "sign-up is closed"; the household's 15-minute window. Measured on 9202 (`audits/gate-rollout-2026-09-29/`B, C): 11 apps let a stranger sign up after the setup (gitea, calcom, adventurelog, homebox, papra, sparkyfitness, vikunja, opengist, wishlist, termix; gramps-web 500) — all refused with the block, the apps still answered, the window let a family member in and closed again; 11 more refuse a stranger by themselves. wanderer → R-714. | **CLOSED — 2026-09-29** | | **R-712** | **[P2-MEDIUM] wger refused every browser sign-in behind traefik: "CSRF verification failed".** MEASURED 2026-09-29 on 9202 (live catalog wger 2.6): a POST to `/en/user/login` with the browser's `Origin: https://…` answered 403 — Django saw the request as http (no trusted proxy header) and no `CSRF_TRUSTED_ORIGINS`. Found while proving R-707's wger route. **Fixed** (catalog `d0e7e2e`): `CSRF_TRUSTED_ORIGINS=https://${SUBDOMAIN}.${DOMAIN}` + `X_FORWARDED_PROTO_HEADER_SET=True`; proven on a fresh install: the generated password signs in (302) with the https Origin (`audits/login-gate-2026-09-29/D/D2-live.txt`). | **CLOSED — 2026-09-29** | | **R-713** | **[P3-LOW] claper's `after_install` pastes the household's password into Elixir code, and the controller does not refuse a value that would break such code.** FOUND 2026-09-29 by a background security review of the drill commit (mealie/wger had the same shape and were changed to pass the password as `sys.argv[1]`). claper's `bin/claper rpc '… "${ADMIN_PASSWORD}" …'` has no argv: a household-typed password with `"` or `#{` breaks the command (recorded as failed; the page then warns) or changes the Elixir it runs — inside the household's own claper container, as that app. The generated value (letters + digits) is safe. **Fix direction:** (1) controller: `expandAfterInstall` refuses a value holding a quote, a backslash, `$`, `{`, `}`, a backtick or a newline — or a declared per-field encoding; (2) claper: read the value some other way (a file the command reads, or `System.get_env` from a one-shot env). **Fixed in controller v0.281.0** (RP24): a code-bound value holding a quote, backslash, `$`, `{`, `}`, backtick or line break is refused; `${NAME|base64}` is new. claper (catalog `6faf432`) decodes `Base.decode64!("${ADMIN_PASSWORD|base64}")`; proven live with a typed password holding `"` and `#{`: default refused, typed signs in (`audits/gate-rollout-2026-09-29/`D). | **CLOSED — 2026-09-29** | -| **R-714** | **[P2-MEDIUM] wanderer cannot be gated: its web part calls its own database host through the public name.** MEASURED 2026-09-29 on 9202: `PUBLIC_POCKETBASE_URL=https://${SUBDOMAIN_DB}.${DOMAIN}` is fetched by the web server itself; a gate on that host would refuse the web part (no gate cookie) and the household could not finish the setup. (On 9202 the name points to another box, so the app answered 500 either way.) Meanwhile wanderer keeps open sign-up (`PUBLIC_DISABLE_SIGNUP=false`) and PocketBase's own first-run screen on the second host. **Fix direction:** point the web part at PocketBase on the docker network (if wanderer separates the internal and public URL), then gate both hosts; or gate only the web host and close PocketBase's `/_/` installer with a block. | **OPEN — P2; owner: CC** | -| **R-715** | **[P3-LOW] The setup gate's probe reads only an HTTP-200 JSON object, so three apps with a real status get the button.** MEASURED 2026-09-29 on 9202: ghost (`{"setup":[{"status":…}]}` — a list), home-assistant (`/api/onboarding` — a top-level list), gramps-web (405 after the setup). And a probe that never flips BLOCKS the household's press (fail closed — measured on gramps-web while its check was still in the catalog): a wrong probe in a template would keep an app closed to everyone but the household until the catalog is fixed. **Fix direction:** list indexes in `field`, an optional `status:` to match, and a catalog gate that refuses a probe without a before/after measurement in its comment. | **OPEN — P3; owner: CC** | -| **R-716** | **[P3-LOW] Apps installed before controller 0.281.0 keep their open sign-up — decision 47 closes it only on apps whose gate the box opened.** READ 2026-09-29 on the demo boxes after catalog `6faf432` synced: demo-hp's adventurelog and opengist, demo-felhom's opengist carry `signup_block:` in their synced template and no gate record, so no block (`audits/gate-rollout-2026-09-29/0/P0-3-demo-boxes-after-push.txt`). This is Part 0's rule working as designed (a catalog change never touches an installed app). **Needs an operator word** before anything changes on an installed app: a one-time "close sign-up now" press on the app page for an installed app, or leave them. Only the demo boxes have such installs today. | **WAITING-ON-OPERATOR — P3; owner: operator** | +| **R-714** | **[P2-MEDIUM] wanderer cannot be gated: its web part calls its own database host through the public name.** MEASURED 2026-09-29 on 9202: `PUBLIC_POCKETBASE_URL=https://${SUBDOMAIN_DB}.${DOMAIN}` is fetched by the web server itself; a gate on that host would refuse the web part (no gate cookie) and the household could not finish the setup. (On 9202 the name points to another box, so the app answered 500 either way.) Meanwhile wanderer keeps open sign-up (`PUBLIC_DISABLE_SIGNUP=false`) and PocketBase's own first-run screen on the second host. **Fix direction:** point the web part at PocketBase on the docker network (if wanderer separates the internal and public URL), then gate both hosts; or gate only the web host and close PocketBase's `/_/` installer with a block. **Resolved without a gate (2026-09-29 evening, decision 48):** measured — no separate internal DB URL; no first-admin screen for a stranger (PocketBase's installer needs the log link); sign-up closed by the household's "Close sign-up now" (controller 0.282.0): case-insensitive block on `/register` + PocketBase `POST /api/collections/(users|_pb_users_auth_)/records` (the id and `USERS` both got in before), and `PUBLIC_DISABLE_SIGNUP`. Proven on 9202 (`audits/signup-lock-2026-09-29/`D). | **CLOSED — 2026-09-29** | +| **R-715** | **[P3-LOW] The setup gate's probe reads only an HTTP-200 JSON object, so three apps with a real status get the button.** MEASURED 2026-09-29 on 9202: ghost (`{"setup":[{"status":…}]}` — a list), home-assistant (`/api/onboarding` — a top-level list), gramps-web (405 after the setup). And a probe that never flips BLOCKS the household's press (fail closed — measured on gramps-web while its check was still in the catalog): a wrong probe in a template would keep an app closed to everyone but the household until the catalog is fixed. **Fix direction:** list indexes in `field`, an optional `status:` to match, and a catalog gate that refuses a probe without a before/after measurement in its comment. **Fixed in controller v0.282.0** (RP29, RP30): list indexes in `field`, `done_status:`. ghost, home-assistant, gramps-web measured before/after on fresh installs; each gate opened by itself; a press before the setup refused on all three (`audits/signup-lock-2026-09-29/`E). New catalog gate `check-probe-measured.py` (5 decoys). | **CLOSED — 2026-09-29** | +| **R-716** | **[P3-LOW] Apps installed before controller 0.281.0 keep their open sign-up — decision 47 closes it only on apps whose gate the box opened.** READ 2026-09-29 on the demo boxes after catalog `6faf432` synced: demo-hp's adventurelog and opengist, demo-felhom's opengist carry `signup_block:` in their synced template and no gate record, so no block (`audits/gate-rollout-2026-09-29/0/P0-3-demo-boxes-after-push.txt`). This is Part 0's rule working as designed (a catalog change never touches an installed app). **Needs an operator word** before anything changes on an installed app: a one-time "close sign-up now" press on the app page for an installed app, or leave them. Only the demo boxes have such installs today. **Operator ruled A (decision 49); built in controller v0.282.0 and pressed** on demo-hp's adventurelog and opengist and demo-felhom's opengist: before, sign-up served; after, refused; adventurelog's own switch on (`audits/signup-lock-2026-09-29/`C). | **CLOSED — 2026-09-29** | +| **R-717** | **[P3-LOW] opengist and wishlist keep their sign-up switch only in their own database — the box closes them with the address block alone.** MEASURED 2026-09-29: opengist `disable-signup` is an admin-panel setting (no env, no CLI); wishlist `system_config.enableSignup` (Prisma). Their blocks are case-insensitive and refused every trick shape (`audits/signup-lock-2026-09-29/B/`). **Fix direction:** an `after_setup` command that sets the database value (wishlist: a Node/Prisma one-liner; opengist: needs its sqlite with the app stopped). | **OPEN — P3; owner: CC** | +| **R-718** | **[P3-LOW] "Close sign-up now" restarts an app with its own switch, and the card does not say so.** MEASURED 2026-09-29 on demo-hp: pressing it on adventurelog recreated its backend (~30 s, one 500 on its login page). The window's card says the app restarts; the close card does not. **Fix direction:** the close card and the gate-open moment say "the app restarts once" where `after_setup.env` exists. | **OPEN — P3; owner: CC** | <!-- DUE-CHECKS-BEGIN — machine-readable. Parsed by scripts/due_checks_gate.py. One row per dated check. The R-number must have a row above. Dates are UTC.