From b0f8ffb8017a9edef9dfc5f6c77f82474da9f897 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Tue, 29 Sep 2026 09:20:47 +0200 Subject: [PATCH] =?UTF-8?q?Part=20C/D:=20gate=20built=20+=20proven=20live?= =?UTF-8?q?=20(v0.280.0),=20defaults=20fixed,=20floor=200.280.0;=20decisio?= =?UTF-8?q?n=2046=20outcome;=2001=20=C2=A75=20who=20may=20reach=20an=20app?= =?UTF-8?q?;=20R-707=20narrowed,=20R-708/R-709/R-712=20closed,=20R-713=20f?= =?UTF-8?q?iled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- .../architecture/01-topology-and-trust.md | 12 + .../architecture/09-update-architecture.md | 10 +- .../login-gate-2026-09-29/C/C0-9202-0280.txt | 5 + .../login-gate-2026-09-29/C/C1-deploy.txt | 16 + .../C/C1-stranger-during-install.log | 837 ++++++++++++++++++ .../login-gate-2026-09-29/C/C2-stranger.txt | 4 + .../login-gate-2026-09-29/C/C3-household.txt | 6 + .../login-gate-2026-09-29/C/C4-setup.txt | 3 + .../C/C5-open-by-probe.txt | 4 + .../login-gate-2026-09-29/C/C6-restart.txt | 6 + .../login-gate-2026-09-29/C/C7-press.txt | 5 + .../login-gate-2026-09-29/C/C8-after-open.txt | 9 + .../login-gate-2026-09-29/C/C9-floor-0280.txt | 4 + .../audits/login-gate-2026-09-29/C/c_live.py | 127 +++ .../login-gate-2026-09-29/C/redproofs/RP1.txt | 9 + .../C/redproofs/RP10.txt | 9 + .../C/redproofs/RP11.txt | 9 + .../C/redproofs/RP12.txt | 9 + .../C/redproofs/RP13.txt | 10 + .../C/redproofs/RP14.txt | 9 + .../C/redproofs/RP15.txt | 9 + .../C/redproofs/RP16.txt | 9 + .../login-gate-2026-09-29/C/redproofs/RP2.txt | 9 + .../login-gate-2026-09-29/C/redproofs/RP3.txt | 9 + .../login-gate-2026-09-29/C/redproofs/RP4.txt | 9 + .../login-gate-2026-09-29/C/redproofs/RP5.txt | 9 + .../login-gate-2026-09-29/C/redproofs/RP6.txt | 9 + .../login-gate-2026-09-29/C/redproofs/RP7.txt | 10 + .../login-gate-2026-09-29/C/redproofs/RP8.txt | 9 + .../login-gate-2026-09-29/C/redproofs/RP9.txt | 9 + .../login-gate-2026-09-29/C/stranger4.sh | 11 + .../login-gate-2026-09-29/D/D1-probe.txt | 22 + .../login-gate-2026-09-29/D/D2-live.txt | 18 + .../D/D3-js-generator.txt | 2 + .../D/D4-grafana-r708.txt | 10 + .../D/D5-romm-zipline-pages.txt | 4 + .../login-gate-2026-09-29/D/D6-r709-live.txt | 3 + .../login-gate-2026-09-29/D/catpatch.py | 215 +++++ .../audits/login-gate-2026-09-29/D/d_cw.sh | 13 + .../audits/login-gate-2026-09-29/D/d_live.py | 46 + .../audits/login-gate-2026-09-29/D/d_login.sh | 13 + .../audits/login-gate-2026-09-29/D/d_probe.sh | 27 + .../audits/login-gate-2026-09-29/D/d_w2.sh | 10 + .../audits/login-gate-2026-09-29/D/genjs.js | 34 + .../audits/login-gate-2026-09-29/D/graf.sh | 6 + documentation/backlog/OPEN-ITEMS.md | 8 +- 46 files changed, 1632 insertions(+), 4 deletions(-) create mode 100644 documentation/audits/login-gate-2026-09-29/C/C0-9202-0280.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/C1-deploy.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/C1-stranger-during-install.log create mode 100644 documentation/audits/login-gate-2026-09-29/C/C2-stranger.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/C3-household.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/C4-setup.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/C5-open-by-probe.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/C6-restart.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/C7-press.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/C8-after-open.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/C9-floor-0280.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/c_live.py create mode 100644 documentation/audits/login-gate-2026-09-29/C/redproofs/RP1.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/redproofs/RP10.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/redproofs/RP11.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/redproofs/RP12.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/redproofs/RP13.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/redproofs/RP14.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/redproofs/RP15.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/redproofs/RP16.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/redproofs/RP2.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/redproofs/RP3.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/redproofs/RP4.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/redproofs/RP5.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/redproofs/RP6.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/redproofs/RP7.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/redproofs/RP8.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/redproofs/RP9.txt create mode 100644 documentation/audits/login-gate-2026-09-29/C/stranger4.sh create mode 100644 documentation/audits/login-gate-2026-09-29/D/D1-probe.txt create mode 100644 documentation/audits/login-gate-2026-09-29/D/D2-live.txt create mode 100644 documentation/audits/login-gate-2026-09-29/D/D3-js-generator.txt create mode 100644 documentation/audits/login-gate-2026-09-29/D/D4-grafana-r708.txt create mode 100644 documentation/audits/login-gate-2026-09-29/D/D5-romm-zipline-pages.txt create mode 100644 documentation/audits/login-gate-2026-09-29/D/D6-r709-live.txt create mode 100644 documentation/audits/login-gate-2026-09-29/D/catpatch.py create mode 100644 documentation/audits/login-gate-2026-09-29/D/d_cw.sh create mode 100644 documentation/audits/login-gate-2026-09-29/D/d_live.py create mode 100644 documentation/audits/login-gate-2026-09-29/D/d_login.sh create mode 100644 documentation/audits/login-gate-2026-09-29/D/d_probe.sh create mode 100644 documentation/audits/login-gate-2026-09-29/D/d_w2.sh create mode 100644 documentation/audits/login-gate-2026-09-29/D/genjs.js create mode 100644 documentation/audits/login-gate-2026-09-29/D/graf.sh diff --git a/documentation/architecture/01-topology-and-trust.md b/documentation/architecture/01-topology-and-trust.md index c79f9225..e6621f5d 100644 --- a/documentation/architecture/01-topology-and-trust.md +++ b/documentation/architecture/01-topology-and-trust.md @@ -124,6 +124,18 @@ a trust boundary too: a default password, or a "first visitor creates the admin" household acts. Rule and per-app status: `09` §3 decision 45 and `app-catalog-felhom.eu/FIRST-ADMIN.md` (the audit of all 53 apps, 2026-09-28). +**Who may reach an app, and through what (recorded 2026-09-29 — no document said it before; spike finding F1).** +Every app is reached only through the box's traefik (no catalog app publishes a host port except crafty-controller's +game ports; none uses host networking — read from the catalog 2026-09-29). traefik routes by host name: the tunnel's +`*.domain` and the LAN both land there. The dashboard (`felhom.`) has its own password; its session cookie is +**host-only** and never reaches an app host. An app answers anyone who reaches its host, with the app's own login — +**except while its setup gate is closed** (`09` §3 decision 46, controller ≥ 0.280.0): then traefik asks the +controller first (`forwardAuth`), and only a browser holding a gate cookie for that one host gets through. The cookie +is minted after a valid dashboard session vouched for the browser (a 60-second, one-use token bound to the host, on +the dashboard's own `/__gate/start`). The controller is in an app's request path ONLY while its gate is closed; once +open, the gate's traefik router is removed and the app is reached exactly as without it. The gate decides who creates +the first admin; it does not decide who may sign up afterwards (R-711). + --- ## 6. Enrollment & identity diff --git a/documentation/architecture/09-update-architecture.md b/documentation/architecture/09-update-architecture.md index 551cc47f..fa1f933c 100644 --- a/documentation/architecture/09-update-architecture.md +++ b/documentation/architecture/09-update-architecture.md @@ -502,7 +502,15 @@ R-636's louder repeated alarm. app is not yet set up, the box lets only a person logged in to the household's dashboard reach it; the gate opens when the app's own status says an admin exists, or when the household presses "Done, I set it up". Option B: one fix per app (route (b), R-707). A is built only if the spike passes its exit test in writing; otherwise B continues - and the spike's result is the recorded reason. Outcome: *(filled in by the 2026-09-29 session)*. + and the spike's result is the recorded reason. **Outcome (2026-09-29): the spike PASSED** (`audits/login-gate-2026-09-29/ + B/B-VERDICT.md`, written before any build): on 9202 a stranger never reached a first-setup screen (~530 polls during + two installs, 0 app answers), the household passed with its dashboard session in 0.2 s, both probes flipped on the + setup, immich's phone-app API worked unchanged once the gate's router was removed, and the dashboard cookie was never + widened (a redirect handshake mints a 60-second, one-use token per app host instead). **Built in controller v0.280.0** + and proven live on immich, n8n, audiobookshelf (probe) and uptime-kuma (button). Costs, stated: ~2 ms per gated + request; a gated app answers 500 while the controller is down (closed, not open); a phone app cannot reach a gated + app; an app with no probe waits for the household's press, and the press trusts the household. Not covered by the + gate: open sign-up after the setup (R-711). Design record: `01-topology-and-trust.md` §5. 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/login-gate-2026-09-29/C/C0-9202-0280.txt b/documentation/audits/login-gate-2026-09-29/C/C0-9202-0280.txt new file mode 100644 index 00000000..54596455 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/C0-9202-0280.txt @@ -0,0 +1,5 @@ +before: gitea.dooplex.hu/admin/felhom-controller:0.279.0 +gitea.dooplex.hu/admin/felhom-controller:0.280.0 Up 8 seconds (healthy) +2026/09/29 06:53:08 undo.go:675: [INFO] [stacks] applied-meta backfill (R-646): recorded 0 []; skipped 0 [] — not current with the catalog, their pinned version's .felhom.yml is no longer on the box +2026/09/29 06:53:08 scheduler.go:102: [INFO] [scheduler] Registered periodic job: conversion-copy-release (every 1h0m0s) +2026/09/29 06:53:08 scheduler.go:67: [DEBUG] [scheduler] periodic job registered: name="conversion-copy-release" interval=1h0m0s totalJobs=10 diff --git a/documentation/audits/login-gate-2026-09-29/C/C1-deploy.txt b/documentation/audits/login-gate-2026-09-29/C/C1-deploy.txt new file mode 100644 index 00000000..0a9d4098 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/C1-deploy.txt @@ -0,0 +1,16 @@ +09:03:37 immich catalog meta: setup_gate = True probe = isInitialized +09:03:40 [1] made the drive paths this app requires: ['/mnt/felhom-drives/scratch_hdd/userdata/immich'] +09:03:40 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['HDD_PATH'] +09:03:41 immich deploy -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'} +09:03:41 n8n catalog meta: setup_gate = True probe = data.userManagement.showSetupOnFirstLoad +09:03:41 n8n deploy -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'} +09:03:41 audiobookshelf catalog meta: setup_gate = True probe = isInit +09:03:44 [1] made the drive paths this app requires: ['/mnt/felhom-drives/scratch_hdd/userdata/audiobookshelf'] +09:03:44 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['HDD_PATH'] +09:03:44 audiobookshelf deploy -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'} +09:03:45 uptime-kuma catalog meta: setup_gate = True probe = None +09:03:45 uptime-kuma deploy -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'} +09:04:29 immich state running | gate file present | record {'state': 'closed', 'since': '2026-09-29T07:03:40Z', 'hosts': ['g-immich.enkisfelhom.hu']} +09:04:32 n8n state running | gate file present | record {'state': 'closed', 'since': '2026-09-29T07:03:41Z', 'hosts': ['g-n8n.enkisfelhom.hu']} +09:04:35 audiobookshelf state running | gate file present | record {'state': 'closed', 'since': '2026-09-29T07:03:44Z', 'hosts': ['g-abs.enkisfelhom.hu']} +09:04:49 uptime-kuma state running | gate file present | record {'state': 'closed', 'since': '2026-09-29T07:03:45Z', 'hosts': ['g-kuma.enkisfelhom.hu']} diff --git a/documentation/audits/login-gate-2026-09-29/C/C1-stranger-during-install.log b/documentation/audits/login-gate-2026-09-29/C/C1-stranger-during-install.log new file mode 100644 index 00000000..b3aec9e4 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/C1-stranger-during-install.log @@ -0,0 +1,837 @@ +# stranger4.sh, 9202, 2026-09-29 07:03:31-07:08Z (UTC): per second, each gated host, API-style + browser-style. Gates opened: immich 07:06:02, n8n+audiobookshelf 07:06:22 (probe), uptime-kuma 07:07:08 (press). The one 500 at 07:06:53 is the controller restart (C6): closed, not open. +07:03:30 g-n8n api=[404 page not found|404] browser=404 +07:03:30 g-immich api=[404 page not found|404] browser=404 +07:03:30 g-abs api=[404 page not found|404] browser=404 +07:03:30 g-kuma api=[404 page not found|404] browser=404 +07:03:31 g-n8n api=[404 page not found|404] browser=404 +07:03:31 g-immich api=[404 page not found|404] browser=404 +07:03:31 g-abs api=[404 page not found|404] browser=404 +07:03:31 g-kuma api=[404 page not found|404] browser=404 +07:03:32 g-n8n api=[404 page not found|404] browser=404 +07:03:32 g-immich api=[404 page not found|404] browser=404 +07:03:32 g-abs api=[404 page not found|404] browser=404 +07:03:32 g-kuma api=[404 page not found|404] browser=404 +07:03:33 g-n8n api=[404 page not found|404] browser=404 +07:03:33 g-immich api=[404 page not found|404] browser=404 +07:03:33 g-abs api=[404 page not found|404] browser=404 +07:03:33 g-kuma api=[404 page not found|404] browser=404 +07:03:34 g-n8n api=[404 page not found|404] browser=404 +07:03:35 g-immich api=[404 page not found|404] browser=404 +07:03:35 g-abs api=[404 page not found|404] browser=404 +07:03:35 g-kuma api=[404 page not found|404] browser=404 +07:03:36 g-n8n api=[404 page not found|404] browser=404 +07:03:36 g-immich api=[404 page not found|404] browser=404 +07:03:36 g-abs api=[404 page not found|404] browser=404 +07:03:36 g-kuma api=[404 page not found|404] browser=404 +07:03:37 g-n8n api=[404 page not found|404] browser=404 +07:03:37 g-immich api=[404 page not found|404] browser=404 +07:03:37 g-abs api=[404 page not found|404] browser=404 +07:03:37 g-kuma api=[404 page not found|404] browser=404 +07:03:38 g-n8n api=[404 page not found|404] browser=404 +07:03:38 g-immich api=[404 page not found|404] browser=404 +07:03:38 g-abs api=[404 page not found|404] browser=404 +07:03:38 g-kuma api=[404 page not found|404] browser=404 +07:03:39 g-n8n api=[404 page not found|404] browser=404 +07:03:39 g-immich api=[404 page not found|404] browser=404 +07:03:39 g-abs api=[404 page not found|404] browser=404 +07:03:39 g-kuma api=[404 page not found|404] browser=404 +07:03:40 g-n8n api=[404 page not found|404] browser=404 +07:03:41 g-immich api=[404 page not found|404] browser=404 +07:03:41 g-abs api=[404 page not found|404] browser=404 +07:03:41 g-kuma api=[404 page not found|404] browser=404 +07:03:42 g-n8n api=[404 page not found|404] browser=404 +07:03:42 g-immich api=[404 page not found|404] browser=404 +07:03:42 g-abs api=[404 page not found|404] browser=404 +07:03:42 g-kuma api=[404 page not found|404] browser=404 +07:03:43 g-n8n api=[404 page not found|404] browser=404 +07:03:43 g-immich api=[404 page not found|404] browser=404 +07:03:43 g-abs api=[404 page not found|404] browser=404 +07:03:43 g-kuma api=[404 page not found|404] browser=404 +07:03:44 g-n8n api=[404 page not found|404] browser=404 +07:03:44 g-immich api=[404 page not found|404] browser=404 +07:03:44 g-abs api=[404 page not found|404] browser=404 +07:03:44 g-kuma api=[404 page not found|404] browser=404 +07:03:45 g-n8n api=[404 page not found|404] browser=404 +07:03:45 g-immich api=[404 page not found|404] browser=404 +07:03:45 g-abs api=[404 page not found|404] browser=404 +07:03:45 g-kuma api=[404 page not found|404] browser=404 +07:03:46 g-n8n api=[404 page not found|404] browser=404 +07:03:47 g-immich api=[404 page not found|404] browser=404 +07:03:47 g-abs api=[404 page not found|404] browser=404 +07:03:47 g-kuma api=[404 page not found|404] browser=404 +07:03:48 g-n8n api=[404 page not found|404] browser=404 +07:03:48 g-immich api=[404 page not found|404] browser=404 +07:03:48 g-abs api=[404 page not found|404] browser=404 +07:03:48 g-kuma api=[404 page not found|404] browser=404 +07:03:49 g-n8n api=[404 page not found|404] browser=404 +07:03:49 g-immich api=[404 page not found|404] browser=404 +07:03:49 g-abs api=[404 page not found|404] browser=404 +07:03:49 g-kuma api=[404 page not found|404] browser=404 +07:03:50 g-n8n api=[404 page not found|404] browser=404 +07:03:50 g-immich api=[404 page not found|404] browser=404 +07:03:50 g-abs api=[404 page not found|404] browser=404 +07:03:50 g-kuma api=[404 page not found|404] browser=404 +07:03:51 g-n8n api=[404 page not found|404] browser=404 +07:03:51 g-immich api=[404 page not found|404] browser=404 +07:03:51 g-abs api=[404 page not found|404] browser=404 +07:03:51 g-kuma api=[404 page not found|404] browser=404 +07:03:52 g-n8n api=[404 page not found|404] browser=404 +07:03:52 g-immich api=[404 page not found|404] browser=404 +07:03:53 g-abs api=[404 page not found|404] browser=404 +07:03:53 g-kuma api=[404 page not found|404] browser=404 +07:03:54 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:03:54 g-immich api=[404 page not found|404] browser=404 +07:03:54 g-abs api=[404 page not found|404] browser=404 +07:03:54 g-kuma api=[404 page not found|404] browser=404 +07:03:55 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:03:55 g-immich api=[404 page not found|404] browser=404 +07:03:55 g-abs api=[404 page not found|404] browser=404 +07:03:55 g-kuma api=[404 page not found|404] browser=404 +07:03:56 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:03:56 g-immich api=[404 page not found|404] browser=404 +07:03:56 g-abs api=[404 page not found|404] browser=404 +07:03:56 g-kuma api=[404 page not found|404] browser=404 +07:03:57 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:03:57 g-immich api=[404 page not found|404] browser=404 +07:03:57 g-abs api=[404 page not found|404] browser=404 +07:03:57 g-kuma api=[404 page not found|404] browser=404 +07:03:58 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:03:59 g-immich api=[404 page not found|404] browser=404 +07:03:59 g-abs api=[404 page not found|404] browser=404 +07:03:59 g-kuma api=[404 page not found|404] browser=404 +07:04:00 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:00 g-immich api=[404 page not found|404] browser=404 +07:04:00 g-abs api=[404 page not found|404] browser=404 +07:04:00 g-kuma api=[404 page not found|404] browser=404 +07:04:01 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:01 g-immich api=[404 page not found|404] browser=404 +07:04:01 g-abs api=[404 page not found|404] browser=404 +07:04:01 g-kuma api=[404 page not found|404] browser=404 +07:04:02 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:02 g-immich api=[404 page not found|404] browser=404 +07:04:02 g-abs api=[404 page not found|404] browser=404 +07:04:02 g-kuma api=[404 page not found|404] browser=404 +07:04:03 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:03 g-immich api=[404 page not found|404] browser=404 +07:04:03 g-abs api=[404 page not found|404] browser=404 +07:04:03 g-kuma api=[404 page not found|404] browser=404 +07:04:04 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:05 g-immich api=[404 page not found|404] browser=404 +07:04:05 g-abs api=[404 page not found|404] browser=404 +07:04:05 g-kuma api=[404 page not found|404] browser=404 +07:04:06 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:06 g-immich api=[404 page not found|404] browser=404 +07:04:06 g-abs api=[404 page not found|404] browser=404 +07:04:06 g-kuma api=[404 page not found|404] browser=404 +07:04:07 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:07 g-immich api=[404 page not found|404] browser=404 +07:04:07 g-abs api=[404 page not found|404] browser=404 +07:04:07 g-kuma api=[404 page not found|404] browser=404 +07:04:08 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:08 g-immich api=[404 page not found|404] browser=404 +07:04:08 g-abs api=[404 page not found|404] browser=404 +07:04:08 g-kuma api=[404 page not found|404] browser=404 +07:04:09 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:09 g-immich api=[404 page not found|404] browser=404 +07:04:09 g-abs api=[404 page not found|404] browser=404 +07:04:09 g-kuma api=[404 page not found|404] browser=404 +07:04:11 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:11 g-immich api=[404 page not found|404] browser=404 +07:04:11 g-abs api=[404 page not found|404] browser=404 +07:04:11 g-kuma api=[404 page not found|404] browser=404 +07:04:12 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:12 g-immich api=[404 page not found|404] browser=404 +07:04:12 g-abs api=[404 page not found|404] browser=404 +07:04:12 g-kuma api=[404 page not found|404] browser=404 +07:04:13 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:13 g-immich api=[404 page not found|404] browser=404 +07:04:13 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:13 g-kuma api=[404 page not found|404] browser=404 +07:04:14 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:14 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:14 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:14 g-kuma api=[404 page not found|404] browser=404 +07:04:15 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:15 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:15 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:15 g-kuma api=[404 page not found|404] browser=404 +07:04:17 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:17 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:17 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:17 g-kuma api=[404 page not found|404] browser=404 +07:04:18 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:18 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:18 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:18 g-kuma api=[404 page not found|404] browser=404 +07:04:19 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:19 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:19 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:19 g-kuma api=[404 page not found|404] browser=404 +07:04:20 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:20 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:20 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:20 g-kuma api=[404 page not found|404] browser=404 +07:04:21 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:21 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:21 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:21 g-kuma api=[404 page not found|404] browser=404 +07:04:23 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:23 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:23 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:23 g-kuma api=[404 page not found|404] browser=404 +07:04:24 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:24 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:24 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:24 g-kuma api=[404 page not found|404] browser=404 +07:04:25 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:25 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:25 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:25 g-kuma api=[404 page not found|404] browser=404 +07:04:26 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:26 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:26 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:26 g-kuma api=[404 page not found|404] browser=404 +07:04:27 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:27 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:27 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:28 g-kuma api=[404 page not found|404] browser=404 +07:04:29 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:29 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:29 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:29 g-kuma api=[404 page not found|404] browser=404 +07:04:30 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:30 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:30 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:30 g-kuma api=[404 page not found|404] browser=404 +07:04:31 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:31 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:31 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:31 g-kuma api=[404 page not found|404] browser=404 +07:04:32 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:32 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:32 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:32 g-kuma api=[404 page not found|404] browser=404 +07:04:33 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:33 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:33 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:34 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:35 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:35 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:35 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:35 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:36 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:36 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:36 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:36 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:37 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:37 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:37 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:37 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:38 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:38 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:38 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:38 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:39 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:39 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:39 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:40 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:41 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:41 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:41 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:41 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:42 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:42 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:42 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:42 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:43 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:43 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:43 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:43 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:44 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:44 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:44 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:44 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:45 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:45 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:45 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:46 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:47 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:47 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:47 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:47 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:48 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:48 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:48 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:48 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:49 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:49 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:49 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:49 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:50 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:50 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:50 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:50 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:51 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:51 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:51 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:51 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:53 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:53 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:53 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:53 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:54 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:54 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:54 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:54 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:55 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:55 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:55 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:55 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:56 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:56 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:56 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:56 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:57 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:57 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:57 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:58 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:59 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:59 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:59 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:04:59 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:00 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:00 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:00 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:00 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:01 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:01 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:01 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:01 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:02 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:02 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:02 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:02 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:03 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:03 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:04 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:04 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:05 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:05 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:05 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:05 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:06 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:06 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:06 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:06 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:07 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:07 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:07 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:07 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:08 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:08 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:08 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:08 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:09 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:09 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:10 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:10 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:11 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:11 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:11 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:11 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:12 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:12 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:12 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:12 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:13 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:13 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:13 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:13 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:14 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:14 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:14 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:14 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:15 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:16 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:16 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:16 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:17 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:17 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:17 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:17 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:18 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:18 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:18 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:18 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:19 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:19 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:19 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:19 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:20 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:20 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:20 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:20 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:21 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:21 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:22 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:22 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:23 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:23 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:23 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:23 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:24 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:24 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:24 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:24 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:25 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:25 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:25 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:25 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:26 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:26 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:26 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:26 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:27 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:28 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:28 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:28 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:29 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:29 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:29 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:29 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:30 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:30 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:30 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:30 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:31 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:31 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:31 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:31 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:32 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:32 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:32 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:32 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:33 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:34 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:34 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:34 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:35 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:35 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:35 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:35 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:36 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:36 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:36 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:36 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:37 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:37 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:37 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:37 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:38 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:38 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:38 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:38 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:39 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:40 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:40 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:40 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:41 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:41 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:41 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:41 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:42 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:42 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:42 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:42 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:43 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:43 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:43 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:43 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:44 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:44 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:44 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:44 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:46 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:46 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:46 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:46 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:47 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:47 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:47 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:47 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:48 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:48 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:48 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:48 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:49 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:49 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:49 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:49 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:50 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:50 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:50 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:50 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:51 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:52 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:52 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:52 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:53 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:53 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:53 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:53 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:54 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:54 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:54 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:54 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:55 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:55 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:55 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:55 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:56 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:56 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:56 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:56 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:57 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:58 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:58 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:58 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:59 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:59 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:59 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:05:59 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:00 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:00 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:00 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:00 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:01 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:01 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:01 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:01 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:02 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:02 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:02 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:02 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:03 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:04 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:04 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:04 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:05 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:05 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:05 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:05 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:06 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:06 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:06 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:06 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:07 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:07 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:07 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:07 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:08 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:08 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:08 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:08 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:09 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:10 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:10 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:10 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:11 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:11 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:11 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:11 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:12 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:12 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:12 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:12 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:13 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:13 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:13 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:13 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:14 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:14 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:14 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:14 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:16 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:16 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:16 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:16 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:17 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:17 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:17 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:17 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:18 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:18 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:18 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:18 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:19 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:19 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:19 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:19 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:20 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:20 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:20 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:20 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:22 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:22 g-immich api=[{"message":"Cannot GET /api/x"}|404] browser=200 +07:06:22 g-abs api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:22 g-kuma api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +07:06:23 g-n8n api=[ 200 302 g-immich.enkisfelhom.hu/ -> 200 felhom.enkisfelhom.hu/__gate/start gate-page(en,hu-ascii)=(False, True) | api POST -> 401 '{"error":"this app is waiting for its first setup"}' +09:05:53 n8n STRANGER: browser -> 200 302 g-n8n.enkisfelhom.hu/ -> 200 felhom.enkisfelhom.hu/__gate/start gate-page(en,hu-ascii)=(False, True) | api POST -> 401 '{"error":"this app is waiting for its first setup"}' +09:05:53 audiobookshelf STRANGER: browser -> 200 302 g-abs.enkisfelhom.hu/ -> 200 felhom.enkisfelhom.hu/__gate/start gate-page(en,hu-ascii)=(False, True) | api POST -> 401 '{"error":"this app is waiting for its first setup"}' +09:05:53 uptime-kuma STRANGER: browser -> 200 302 g-kuma.enkisfelhom.hu/ -> 200 felhom.enkisfelhom.hu/__gate/start gate-page(en,hu-ascii)=(False, True) | api POST -> 401 '{"error":"this app is waiting for its first setup"}' diff --git a/documentation/audits/login-gate-2026-09-29/C/C3-household.txt b/documentation/audits/login-gate-2026-09-29/C/C3-household.txt new file mode 100644 index 00000000..11c261c9 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/C3-household.txt @@ -0,0 +1,6 @@ +09:05:54 dashboard login -> 200 302 felhom.enkisfelhom.hu/login -> 302 felhom.enkisfelhom.hu/ -> 200 felhom.enkisfelhom.hu/launcher +09:05:54 immich HOUSEHOLD browser -> 200 in 0.20s | 302 g-immich.enkisfelhom.hu/ -> 302 felhom.enkisfelhom.hu/__gate/start -> 302 g-immich.enkisfelhom.hu/__felhom_gate/cb -> 200 g-immich.enkisfelhom.hu/ | gate page: False | len 10699 +09:05:54 n8n HOUSEHOLD browser -> 200 in 0.21s | 302 g-n8n.enkisfelhom.hu/ -> 302 felhom.enkisfelhom.hu/__gate/start -> 302 g-n8n.enkisfelhom.hu/__felhom_gate/cb -> 200 g-n8n.enkisfelhom.hu/ | gate page: False | len 52122 +09:05:54 audiobookshelf HOUSEHOLD browser -> 200 in 0.20s | 302 g-abs.enkisfelhom.hu/ -> 302 felhom.enkisfelhom.hu/__gate/start -> 302 g-abs.enkisfelhom.hu/__felhom_gate/cb -> 200 g-abs.enkisfelhom.hu/ | gate page: False | len 4136 +09:05:54 uptime-kuma HOUSEHOLD browser -> 200 in 0.26s | 302 g-kuma.enkisfelhom.hu/ -> 302 felhom.enkisfelhom.hu/__gate/start -> 302 g-kuma.enkisfelhom.hu/__felhom_gate/cb -> 302 g-kuma.enkisfelhom.hu/ -> 200 g-kuma.enkisfelhom.hu/dashboard | gate page: False | len 1200 +09:05:54 cookies per host: {'felhom.enkisfelhom.hu': ['felhom_session'], 'g-immich.enkisfelhom.hu': ['felhom_gate'], 'g-n8n.enkisfelhom.hu': ['felhom_gate'], 'g-abs.enkisfelhom.hu': ['felhom_gate'], 'g-kuma.enkisfelhom.hu': ['felhom_gate']} diff --git a/documentation/audits/login-gate-2026-09-29/C/C4-setup.txt b/documentation/audits/login-gate-2026-09-29/C/C4-setup.txt new file mode 100644 index 00000000..5d941494 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/C4-setup.txt @@ -0,0 +1,3 @@ +09:06:02 immich admin-sign-up through the gate -> 201 +09:06:02 n8n owner setup through the gate -> 200 +09:06:02 audiobookshelf POST /init through the gate -> 200 OK diff --git a/documentation/audits/login-gate-2026-09-29/C/C5-open-by-probe.txt b/documentation/audits/login-gate-2026-09-29/C/C5-open-by-probe.txt new file mode 100644 index 00000000..8dc51a12 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/C5-open-by-probe.txt @@ -0,0 +1,4 @@ +09:06:06 immich gate OPEN after 0s: record {'state': 'open', 'since': '2026-09-29T07:03:40Z', 'hosts': ['g-immich.enkisfelhom.hu'], 'opened_at': '2026-09-29T07:06:02Z', 'opened_by': 'probe'} | file absent +09:06:30 audiobookshelf gate OPEN after 24s: record {'state': 'open', 'since': '2026-09-29T07:03:44Z', 'hosts': ['g-abs.enkisfelhom.hu'], 'opened_at': '2026-09-29T07:06:22Z', 'opened_by': 'probe'} | file absent +09:06:33 n8n gate OPEN after 27s: record {'state': 'open', 'since': '2026-09-29T07:03:41Z', 'hosts': ['g-n8n.enkisfelhom.hu'], 'opened_at': '2026-09-29T07:06:22Z', 'opened_by': 'probe'} | file absent +09:06:41 uptime-kuma (no probe): {'state': 'closed', 'since': '2026-09-29T07:03:45Z', 'hosts': ['g-kuma.enkisfelhom.hu']} | file present diff --git a/documentation/audits/login-gate-2026-09-29/C/C6-restart.txt b/documentation/audits/login-gate-2026-09-29/C/C6-restart.txt new file mode 100644 index 00000000..b7ff7921 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/C6-restart.txt @@ -0,0 +1,6 @@ +controller: Up 6 seconds (healthy) +controller.yml +serverstransports.yml +setup-gate-uptime-kuma.yml +after restart, stranger -> 200 302 g-kuma.enkisfelhom.hu/ -> 200 felhom.enkisfelhom.hu/__gate/start | gate page (hu-ascii): True +after restart, household with its gate cookie only (no dashboard session) -> 200 200 g-kuma.enkisfelhom.hu/dashboard diff --git a/documentation/audits/login-gate-2026-09-29/C/C7-press.txt b/documentation/audits/login-gate-2026-09-29/C/C7-press.txt new file mode 100644 index 00000000..dca96415 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/C7-press.txt @@ -0,0 +1,5 @@ +09:07:08 uptime-kuma app page: gate card True | button True +09:07:08 household presses 'Done, I set it up' -> 200 {'data': {'opened': True}, 'error': '', 'ok': True} +09:07:11 uptime-kuma record {'state': 'open', 'since': '2026-09-29T07:03:45Z', 'hosts': ['g-kuma.enkisfelhom.hu'], 'opened_at': '2026-09-29T07:07:08Z', 'opened_by': 'household'} | file absent +09:07:11 second press -> 409 {'data': None, 'error': 'Ez az alkalmazás már nyitva van.', 'ok': False} +09:07:14 uptime-kuma, stranger after the press -> 200 302 g-kuma.enkisfelhom.hu/ -> 200 g-kuma.enkisfelhom.hu/dashboard | gate page: False diff --git a/documentation/audits/login-gate-2026-09-29/C/C8-after-open.txt b/documentation/audits/login-gate-2026-09-29/C/C8-after-open.txt new file mode 100644 index 00000000..d9032448 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/C8-after-open.txt @@ -0,0 +1,9 @@ +09:07:14 immich AFTER OPEN, stranger browser -> 200 200 g-immich.enkisfelhom.hu/ | gate page: False +09:07:14 n8n AFTER OPEN, stranger browser -> 200 200 g-n8n.enkisfelhom.hu/ | gate page: False +09:07:14 audiobookshelf AFTER OPEN, stranger browser -> 200 200 g-abs.enkisfelhom.hu/ | gate page: False +09:07:15 immich phone app: login -> 201 token +09:07:15 immich phone app GET /api/users/me -> 200 +09:07:15 immich phone app GET /api/server/ping -> 200 +09:07:15 immich phone app GET /api/server/version -> 200 +09:07:15 n8n login, no browser gate cookie -> 200 +09:07:15 audiobookshelf login (its phone app's route), no gate cookie -> 200 diff --git a/documentation/audits/login-gate-2026-09-29/C/C9-floor-0280.txt b/documentation/audits/login-gate-2026-09-29/C/C9-floor-0280.txt new file mode 100644 index 00000000..f00d5c74 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/C9-floor-0280.txt @@ -0,0 +1,4 @@ +## 2026-09-29T07:19:29Z floor 0.279.0 -> 0.280.0 (min_agent 0.131.0 from the v0.280.0 header) +HTTP/1.1 303 See Other +Location: /configuration?flash=floor_set +name="min_controller_version" value="0.280.0" diff --git a/documentation/audits/login-gate-2026-09-29/C/c_live.py b/documentation/audits/login-gate-2026-09-29/C/c_live.py new file mode 100644 index 00000000..49c1d0cb --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/c_live.py @@ -0,0 +1,127 @@ +# Part C live proof (9202, controller 0.280.0, drill catalog): the BUILT gate on four class-4 apps. +# Stranger vs household through the product's own route; the gate opens by probe (immich, n8n, audiobookshelf) or by +# the household's press (uptime-kuma); the app and immich's phone-app API after. Test passwords are generated here, +# kept in memory only, never printed. +import json, secrets, sys, time, urllib.parse +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, '.') +import walk as w +from browser import Browser, hopstr + +D = "enkisfelhom.hu" +PW = open(w.SC + "/.ctlpw").read().strip() +APPS = {"immich": "g-immich", "n8n": "g-n8n", "audiobookshelf": "g-abs", "uptime-kuma": "g-kuma"} +phase = sys.argv[1] + + +def gatefile(app): + return w.guest(f"test -f /opt/docker/stacks/traefik/dynamic/setup-gate-{app}.yml && echo present || echo absent").strip() + + +def record(app): + return ((w.stack(app).get("app_config") or {}).get("setup_gate")) or None + + +def stranger(app): + s = Browser("stranger") + st, body, hops = s.req(f"https://{APPS[app]}.{D}/") + b = ("waiting for its first setup" in body, "Jelentkezz be a Felhom" in body) + st2, body2, _ = s.req(f"https://{APPS[app]}.{D}/api/x", "POST", body={"a": 1}, accept="application/json") + return f"browser -> {st} {hopstr(hops)} gate-page(en,hu-ascii)={b} | api POST -> {st2} {body2[:55]!r}" + + +w.login() +if phase == "deploy": + for app, sub in APPS.items(): + meta = (w.ctl("GET", f"/api/stacks/{app}/deploy-fields")[1].get("data") or {}).get("metadata") or {} + w.say(app, "catalog meta: setup_gate =", meta.get("setup_gate"), "probe =", (meta.get("setup_done_probe") or {}).get("field")) + v = w.deploy_values(app, sub) + code, d = w.ctl("POST", f"/api/stacks/{app}/deploy", {"values": v, "kept_data": "fresh"}) + w.say(app, "deploy ->", code, str(d)[:90]) + for app in APPS: + 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"), "| gate file", gatefile(app), "| record", record(app)) + +if phase == "stranger": + for app in APPS: + w.say(app, "STRANGER:", stranger(app)) + +if phase == "household": + hh = Browser("household") + st, b, hops = hh.login_dashboard(D, PW) + w.say("dashboard login ->", st, hopstr(hops)) + for app in APPS: + t0 = time.time(); st, body, hops = hh.req(f"https://{APPS[app]}.{D}/"); dt = time.time() - t0 + w.say(app, f"HOUSEHOLD browser -> {st} in {dt:.2f}s | {hopstr(hops)} | gate page: {'waiting for its first setup' in body} | len {len(body)}") + w.say("cookies per host:", {h: sorted(v) for h, v in hh.jar.items()}) + json.dump(hh.jar, open("c_jar.json", "w")) + +if phase == "setup": + hh = Browser("household"); hh.jar = json.load(open("c_jar.json")) + creds = {a: ("admin@spike.hu", "Hh" + secrets.token_hex(10) + "7") for a in APPS} + json.dump(creds, open("c_creds.json", "w")) + st, b, _ = hh.req(f"https://g-immich.{D}/api/auth/admin-sign-up", "POST", accept="application/json", + body={"email": creds["immich"][0], "password": creds["immich"][1], "name": "Spike"}) + w.say("immich admin-sign-up through the gate ->", st) + st, b, _ = hh.req(f"https://g-n8n.{D}/rest/owner/setup", "POST", accept="application/json", + body={"email": creds["n8n"][0], "firstName": "S", "lastName": "O", "password": creds["n8n"][1]}) + w.say("n8n owner setup through the gate ->", st) + st, b, _ = hh.req(f"https://g-abs.{D}/init", "POST", accept="application/json", + body={"newRoot": {"username": "root", "password": creds["audiobookshelf"][1]}}) + w.say("audiobookshelf POST /init through the gate ->", st, b[:40]) + +if phase == "watch": + t0 = time.time() + left = {"immich", "n8n", "audiobookshelf"} + while left and time.time() - t0 < 180: + for app in sorted(left): + r = record(app) + if r and r.get("state") == "open": + w.say(app, f"gate OPEN after {time.time() - t0:.0f}s: record {r} | file {gatefile(app)}") + left.discard(app) + time.sleep(5) + for app in left: + w.say(app, "gate still closed after 180 s:", record(app), gatefile(app)) + w.say("uptime-kuma (no probe):", record("uptime-kuma"), "| file", gatefile("uptime-kuma")) + +if phase == "after": + creds = json.load(open("c_creds.json")) + for app in ("immich", "n8n", "audiobookshelf"): + s = Browser("stranger") + st, body, hops = s.req(f"https://{APPS[app]}.{D}/") + w.say(app, f"AFTER OPEN, stranger browser -> {st} {hopstr(hops)} | gate page: {'waiting for its first setup' in body}") + m = Browser("immich-mobile") + st, b, _ = m.req(f"https://g-immich.{D}/api/auth/login", "POST", accept="application/json", + body={"email": creds["immich"][0], "password": creds["immich"][1]}) + tok = json.loads(b).get("accessToken") if st in (200, 201) else None + w.say("immich phone app: login ->", st, "token" if tok else b[:60]) + if tok: + for p in ("/api/users/me", "/api/server/ping", "/api/server/version"): + st, b, _ = m.req(f"https://g-immich.{D}{p}", accept="application/json", headers={"Authorization": "Bearer " + tok}) + w.say(" immich phone app GET", p, "->", st) + n = Browser("n8n-api") + st, b, _ = n.req(f"https://g-n8n.{D}/rest/login", "POST", accept="application/json", + body={"emailOrLdapLoginId": creds["n8n"][0], "password": creds["n8n"][1]}) + w.say("n8n login, no browser gate cookie ->", st) + a = Browser("abs-app") + st, b, _ = a.req(f"https://g-abs.{D}/login", "POST", accept="application/json", + body={"username": "root", "password": creds["audiobookshelf"][1]}) + w.say("audiobookshelf login (its phone app's route), no gate cookie ->", st) + +if phase == "press": + code, d = w.ctl("GET", "/apps/uptime-kuma", raw=True) + page = d if isinstance(d, str) else "" + w.say("uptime-kuma app page: gate card", 'id="setup-gate-card"' in page, "| button", "/apps/uptime-kuma/setup-gate/open" in page) + code, d = w.ctl("POST", "/apps/uptime-kuma/setup-gate/open", {}) + w.say("household presses 'Done, I set it up' ->", code, str(d)[:80]) + w.say("uptime-kuma record", record("uptime-kuma"), "| file", gatefile("uptime-kuma")) + code, d = w.ctl("POST", "/apps/uptime-kuma/setup-gate/open", {}) + w.say("second press ->", code, str(d)[:80]) + s = Browser("stranger") + time.sleep(3) + st, body, hops = s.req(f"https://g-kuma.{D}/") + w.say("uptime-kuma, stranger after the press ->", st, hopstr(hops), "| gate page:", "waiting for its first setup" in body) diff --git a/documentation/audits/login-gate-2026-09-29/C/redproofs/RP1.txt b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP1.txt new file mode 100644 index 00000000..6c207553 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP1.txt @@ -0,0 +1,9 @@ +# RP1 — gate file written before the first start +# mutation in internal/stacks/deploy.go: +# - '\tif meta.SetupGate {\n\t\tg, err := m.prepareSetupGate(' +# + '\tif false && meta.SetupGate { // RED-PROOF RP1\n\t\tg, err := m.prepareSetupGate(' +# go test -run ^TestSetupGate_WrittenBeforeTheFirstStartAndRecordedClosed$ ./internal/stacks +# verdict: RED (assertion) +=== RUN TestSetupGate_WrittenBeforeTheFirstStartAndRecordedClosed + setup_gate_test.go:82: the gate file did not exist when the app was first started — a stranger could reach its first-setup screen +--- FAIL: TestSetupGate_WrittenBeforeTheFirstStartAndRecordedClosed (0.01s) diff --git a/documentation/audits/login-gate-2026-09-29/C/redproofs/RP10.txt b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP10.txt new file mode 100644 index 00000000..d4c34189 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP10.txt @@ -0,0 +1,9 @@ +# RP10 — the pass is bound to its app host +# mutation in internal/web/setup_gate.go: +# - 'hmac.Equal([]byte(mac), []byte(s.gateMAC("cookie", host, exp)))' +# + 'hmac.Equal([]byte(mac), []byte(s.gateMAC("cookie", "gapp.example.hu", exp))) // RED-PROOF RP10' +# go test -run ^TestSetupGate_TheHouseholdPassesWithItsSession$ ./internal/web +# verdict: RED (assertion) +=== RUN TestSetupGate_TheHouseholdPassesWithItsSession + setup_gate_test.go:166: the pass for gapp opened gapp-db: 200 +--- FAIL: TestSetupGate_TheHouseholdPassesWithItsSession (0.06s) diff --git a/documentation/audits/login-gate-2026-09-29/C/redproofs/RP11.txt b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP11.txt new file mode 100644 index 00000000..41a25198 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP11.txt @@ -0,0 +1,9 @@ +# RP11 — the key survives a restart +# mutation in internal/web/setup_gate.go: +# - '\t\t\tif err := os.WriteFile(p, []byte(hex.EncodeToString(k)), 0o600); err != nil {' +# + '\t\t\tif err := error(nil); err != nil { // RED-PROOF RP11' +# go test -run ^TestSetupGate_ARestartKeepsTheHouseholdsPass$ ./internal/web +# verdict: RED (assertion) +=== RUN TestSetupGate_ARestartKeepsTheHouseholdsPass + setup_gate_test.go:197: after a restart the household's pass was refused: 401 +--- FAIL: TestSetupGate_ARestartKeepsTheHouseholdsPass (0.06s) diff --git a/documentation/audits/login-gate-2026-09-29/C/redproofs/RP12.txt b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP12.txt new file mode 100644 index 00000000..040b7dec --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP12.txt @@ -0,0 +1,9 @@ +# RP12 — an opened gate lets everything through +# mutation in internal/web/setup_gate.go: +# - '\tif !closed {\n\t\t// Opened;' +# + '\tif false && !closed { // RED-PROOF RP12\n\t\t// Opened;' +# go test -run ^TestSetupGate_AnOpenedGateLetsEverythingThrough$ ./internal/web +# verdict: RED (assertion) +=== RUN TestSetupGate_AnOpenedGateLetsEverythingThrough + setup_gate_test.go:212: an opened gate: 401, want 200 +--- FAIL: TestSetupGate_AnOpenedGateLetsEverythingThrough (0.07s) diff --git a/documentation/audits/login-gate-2026-09-29/C/redproofs/RP13.txt b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP13.txt new file mode 100644 index 00000000..2406fb15 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP13.txt @@ -0,0 +1,10 @@ +# RP13 — R-710: an absent record on an old install warns +# mutation in internal/web/known_login.go: +# - '\treturn err != nil || knownLoginNow().Sub(at) > afterInstallWindow\n' +# + '\t_ = at\n\treturn err != nil && false // RED-PROOF RP13 (the 0.279.0 shape)\n' +# go test -run ^TestKnownLogin_InEffectOnlyUntilReplaced$ ./internal/web +# verdict: RED (assertion) +=== RUN TestKnownLogin_InEffectOnlyUntilReplaced + known_login_test.go:46: R-710: installed long before the after_install existed: in effect = false, want true + known_login_test.go:46: R-710: no install time on record: in effect = false, want true +--- FAIL: TestKnownLogin_InEffectOnlyUntilReplaced (0.01s) diff --git a/documentation/audits/login-gate-2026-09-29/C/redproofs/RP14.txt b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP14.txt new file mode 100644 index 00000000..a52dbf14 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP14.txt @@ -0,0 +1,9 @@ +# RP14 — R-709: the page never carries an installed password +# mutation in internal/web/templates/deploy.html: +# - '' +# + '' +# go test -run ^TestR709_AnInstalledAppsPasswordIsNotInThePage$ ./internal/web +# verdict: RED (assertion) +=== RUN TestR709_AnInstalledAppsPasswordIsNotInThePage + r709_password_field_test.go:49: R-709: the installed app's admin password is in the HTML of its settings page +--- FAIL: TestR709_AnInstalledAppsPasswordIsNotInThePage (0.07s) diff --git a/documentation/audits/login-gate-2026-09-29/C/redproofs/RP15.txt b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP15.txt new file mode 100644 index 00000000..930a5c65 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP15.txt @@ -0,0 +1,9 @@ +# RP15 — R-709: the reveal endpoint serves an installed password +# mutation in internal/web/handlers.go: +# - '\tif !allowed && stack.Deployed {' +# + '\tif false && !allowed && stack.Deployed { // RED-PROOF RP15' +# go test -run ^TestR709_TheRevealEndpointServesAnInstalledPassword$ ./internal/web +# verdict: RED (assertion) +=== RUN TestR709_TheRevealEndpointServesAnInstalledPassword + r709_password_field_test.go:86: installed password: 403 {"data":null,"error":"Ez a mező nem kérhető le.","ok":false} +--- FAIL: TestR709_TheRevealEndpointServesAnInstalledPassword (0.06s) diff --git a/documentation/audits/login-gate-2026-09-29/C/redproofs/RP16.txt b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP16.txt new file mode 100644 index 00000000..96ed7212 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP16.txt @@ -0,0 +1,9 @@ +# RP16 — special generator +# mutation in internal/stacks/deploy.go: +# - '\t\tcase "special":\n\t\t\treturn randomWithSpecial(length)' +# + '\t\tcase "special":\n\t\t\treturn randomAlphanumeric(length) // RED-PROOF RP16' +# go test -run ^TestGenerateValue_PasswordWithASpecialCharacter$ ./internal/stacks +# verdict: RED (assertion) +=== RUN TestGenerateValue_PasswordWithASpecialCharacter + setup_gate_test.go:315: no special character +--- FAIL: TestGenerateValue_PasswordWithASpecialCharacter (0.00s) diff --git a/documentation/audits/login-gate-2026-09-29/C/redproofs/RP2.txt b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP2.txt new file mode 100644 index 00000000..9ad967bc --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP2.txt @@ -0,0 +1,9 @@ +# RP2 — an unwritable gate refuses the install +# mutation in internal/stacks/deploy.go: +# - '\t\tg, err := m.prepareSetupGate(req.StackName, stack.ComposePath, env)\n\t\tif err != nil {' +# + '\t\tg, err := m.prepareSetupGate(req.StackName, stack.ComposePath, env)\n\t\tif err != nil && false { // RED-PROOF RP2' +# go test -run ^TestSetupGate_AnUnwritableGateRefusesTheInstall$ ./internal/stacks +# verdict: RED (assertion) +=== RUN TestSetupGate_AnUnwritableGateRefusesTheInstall + setup_gate_test.go:114: an install whose gate could not be written was accepted +--- FAIL: TestSetupGate_AnUnwritableGateRefusesTheInstall (0.01s) diff --git a/documentation/audits/login-gate-2026-09-29/C/redproofs/RP3.txt b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP3.txt new file mode 100644 index 00000000..a7860a53 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP3.txt @@ -0,0 +1,9 @@ +# RP3 — the probe opens only when the app says set up +# mutation in internal/stacks/setup_gate.go: +# - '\treturn got == done, got\n' +# + '\treturn true, got // RED-PROOF RP3\n' +# go test -run ^TestSetupGate_TheProbeOpensItOnlyWhenTheAppSaysSetUp$ ./internal/stacks +# verdict: RED (assertion) +=== RUN TestSetupGate_TheProbeOpensItOnlyWhenTheAppSaysSetUp + setup_gate_test.go:176: not yet set up, but the gate opened +--- FAIL: TestSetupGate_TheProbeOpensItOnlyWhenTheAppSaysSetUp (0.00s) diff --git a/documentation/audits/login-gate-2026-09-29/C/redproofs/RP4.txt b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP4.txt new file mode 100644 index 00000000..676d5659 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP4.txt @@ -0,0 +1,9 @@ +# RP4 — opening removes the traefik file +# mutation in internal/stacks/setup_gate.go: +# - '\tif err := m.removeSetupGateFile(name); err != nil {\n\t\tm.logger.Printf("[ERROR] [stacks] %s: setup gate opened' +# + '\tif err := error(nil); err != nil { // RED-PROOF RP4\n\t\tm.logger.Printf("[ERROR] [stacks] %s: setup gate opened' +# go test -run ^TestSetupGate_TheButtonOpensItOnce$ ./internal/stacks +# verdict: RED (assertion) +=== RUN TestSetupGate_TheButtonOpensItOnce + setup_gate_test.go:210: file still there after the press +--- FAIL: TestSetupGate_TheButtonOpensItOnce (0.00s) diff --git a/documentation/audits/login-gate-2026-09-29/C/redproofs/RP5.txt b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP5.txt new file mode 100644 index 00000000..959f28ca --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP5.txt @@ -0,0 +1,9 @@ +# RP5 — the button opens once +# mutation in internal/stacks/setup_gate.go: +# - '\tif st.AppConfig == nil || !st.AppConfig.SetupGate.Closed() {\n\t\treturn ErrSetupGateNotClosed' +# + '\tif st.AppConfig == nil { // RED-PROOF RP5\n\t\treturn ErrSetupGateNotClosed' +# go test -run ^TestSetupGate_TheButtonOpensItOnce$ ./internal/stacks +# verdict: RED (assertion) +=== RUN TestSetupGate_TheButtonOpensItOnce + setup_gate_test.go:213: second press: setup gate gapp: the record could not be written +--- FAIL: TestSetupGate_TheButtonOpensItOnce (0.00s) diff --git a/documentation/audits/login-gate-2026-09-29/C/redproofs/RP6.txt b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP6.txt new file mode 100644 index 00000000..d9956a1e --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP6.txt @@ -0,0 +1,9 @@ +# RP6 — a restart rewrites the gate file from the record +# mutation in internal/stacks/setup_gate.go: +# - '\t\t\tif _, err := m.writeSetupGate(it.name, it.compose, cfg.Env); err != nil {' +# + '\t\t\tif _, err := "", error(nil); err != nil { // RED-PROOF RP6' +# go test -run ^TestSetupGate_ARestartKeepsItAndStaleFilesGo$ ./internal/stacks +# verdict: RED (assertion) +=== RUN TestSetupGate_ARestartKeepsItAndStaleFilesGo + setup_gate_test.go:234: the restart left the app open: the gate file was not rewritten from the record +--- FAIL: TestSetupGate_ARestartKeepsItAndStaleFilesGo (0.00s) diff --git a/documentation/audits/login-gate-2026-09-29/C/redproofs/RP7.txt b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP7.txt new file mode 100644 index 00000000..145838e6 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP7.txt @@ -0,0 +1,10 @@ +# RP7 — a restore keeps the gate record +# mutation in internal/stacks/life_records.go: +# - '\tcfg.SetupGate = prior.SetupGate\n' +# + '\t// RED-PROOF RP7\n' +# go test -run ^TestSetupGate_ARestoreKeepsTheRecord$ ./internal/stacks +# verdict: RED (assertion) +=== RUN TestSetupGate_ARestoreKeepsTheRecord + setup_gate_test.go:264: opened before: after the restore "", want "open" + setup_gate_test.go:264: still closed: after the restore "", want "closed" +--- FAIL: TestSetupGate_ARestoreKeepsTheRecord (0.00s) diff --git a/documentation/audits/login-gate-2026-09-29/C/redproofs/RP8.txt b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP8.txt new file mode 100644 index 00000000..abcaeea4 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP8.txt @@ -0,0 +1,9 @@ +# RP8 — a stranger is refused +# mutation in internal/web/setup_gate.go: +# - '\tif s.gateCookieValid(r, host) {' +# + '\tif true || s.gateCookieValid(r, host) { // RED-PROOF RP8' +# go test -run ^TestSetupGate_AStrangerIsRefused$ ./internal/web +# verdict: RED (assertion) +=== RUN TestSetupGate_AStrangerIsRefused + setup_gate_test.go:100: a stranger's browser: 200 "" — want 302 to the dashboard's gate page +--- FAIL: TestSetupGate_AStrangerIsRefused (0.06s) diff --git a/documentation/audits/login-gate-2026-09-29/C/redproofs/RP9.txt b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP9.txt new file mode 100644 index 00000000..a724bced --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/redproofs/RP9.txt @@ -0,0 +1,9 @@ +# RP9 — a token is one-use +# mutation in internal/web/setup_gate.go: +# - '\tif _, seen := s.gate.used[t.Nonce]; seen {' +# + '\tif _, seen := s.gate.used[t.Nonce]; seen && false { // RED-PROOF RP9' +# go test -run ^TestSetupGate_TheHouseholdPassesWithItsSession$ ./internal/web +# verdict: RED (assertion) +=== RUN TestSetupGate_TheHouseholdPassesWithItsSession + setup_gate_test.go:163: a token worked twice: 302 +--- FAIL: TestSetupGate_TheHouseholdPassesWithItsSession (0.06s) diff --git a/documentation/audits/login-gate-2026-09-29/C/stranger4.sh b/documentation/audits/login-gate-2026-09-29/C/stranger4.sh new file mode 100644 index 00000000..9e27a1fa --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/C/stranger4.sh @@ -0,0 +1,11 @@ +# A stranger polls both gated app hosts every second during the deploys: no cookies, API style and browser style. +B=https://192.168.0.114; D=enkisfelhom.hu +end=$(( $(date +%s) + ${1:-900} )) +while [ $(date +%s) -lt $end ]; do + for h in g-n8n g-immich g-abs g-kuma; do + p=/api/x + r=$(curl -sk -m 5 -H "Host: $h.$D" -w '|%{http_code}' $B$p | tr -d '\n' | cut -c1-90) + b=$(curl -sk -m 5 -H "Host: $h.$D" -H 'Accept: text/html' -o /dev/null -w '%{http_code}' $B/) + echo "$(date -u +%T) $h api=[$r] browser=$b" + done; sleep 1 +done diff --git a/documentation/audits/login-gate-2026-09-29/D/D1-probe.txt b/documentation/audits/login-gate-2026-09-29/D/D1-probe.txt new file mode 100644 index 00000000..e14f9c8d --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/D/D1-probe.txt @@ -0,0 +1,22 @@ +mealie BEFORE: default -> 200 | wrong -> 401 +wger BEFORE: default -> 404 | wrong -> 404 +mealie route output: FELHOM_AFTER_INSTALL_OK rc=0 +mealie AFTER: default -> 401 | new -> 200 | wrong -> 401 +wger route output: FELHOM_AFTER_INSTALL_OK rc=0 +wger AFTER: default -> 404 | new -> 404 | wrong -> 404 +uid=1000(wger) gid=1000(wger) groups=1000(wger),100(users) +/home/wger/src +wger (after the first set_password above): default -> 200-> | wrong -> 200-> +wger route output: FELHOM_AFTER_INSTALL_OK rc=0 +wger AFTER: default -> 200-> | new -> 200-> | wrong -> 200-> +# wger re-run: the form's field is 'credential'; no Origin header (a browser's https Origin is refused by CSRF — R-712) +wger (after the first set_password above): default -> 200-> | wrong -> 200-> +wger route output: FELHOM_AFTER_INSTALL_OK rc=0 +wger AFTER: default -> 200-> | new -> 200-> | wrong -> 200-> +# wger re-run 2: the username field is 'login' +wger (after the first set_password above): default -> 200-> | wrong -> 200-> +wger route output: FELHOM_AFTER_INSTALL_OK rc=0 +wger AFTER: default -> 200-> | new -> 302->/ | wrong -> 200-> +calibre-web BEFORE: default -> 302->/ | wrong -> 200-> +route output (no cd, absolute path, exec array): Password for user 'admin' changed rc=0 +calibre-web AFTER: default -> 200-> | new -> 302->/ | wrong -> 200-> diff --git a/documentation/audits/login-gate-2026-09-29/D/D2-live.txt b/documentation/audits/login-gate-2026-09-29/D/D2-live.txt new file mode 100644 index 00000000..292658d4 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/D/D2-live.txt @@ -0,0 +1,18 @@ +09:08:43 sync -> 200 +09:08:48 catalog synced: wger after_install passes the password as argv +09:08:48 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['ADMIN_PASSWORD'] +09:08:48 mealie deploy -> 202 +09:08:48 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['ADMIN_PASSWORD'] +09:08:48 wger deploy -> 202 +09:08:52 [1] made the drive paths this app requires: ['/mnt/felhom-drives/scratch_hdd/userdata/calibre-web'] +09:08:52 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['HDD_PATH', 'ADMIN_PASSWORD'] +09:08:52 calibre-web deploy -> 202 +09:09:23 calibre-web after_install record after 30s: ok=True detail='None' +09:09:23 mealie after_install record after 30s: ok=True detail='None' +09:09:53 wger after_install record after 60s: ok=True detail='None' +mealie (200 = signed in) default MyPassword -> 401 | generated -> 200 | wrong -> 401 +wger (302 = signed in; the browser's https Origin, R-712) default adminadmin -> 200 | generated -> 302 | wrong -> 200 +calibre-web (302 = signed in) default admin123 -> 200 | generated -> 302 | wrong -> 200 +09:10:11 mealie app page carries the known-login sentence (ASCII fragment "ismert, k"): False +09:10:11 wger app page carries the known-login sentence (ASCII fragment "ismert, k"): False +09:10:11 calibre-web app page carries the known-login sentence (ASCII fragment "ismert, k"): False diff --git a/documentation/audits/login-gate-2026-09-29/D/D3-js-generator.txt b/documentation/audits/login-gate-2026-09-29/D/D3-js-generator.txt new file mode 100644 index 00000000..44dfb720 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/D/D3-js-generator.txt @@ -0,0 +1,2 @@ +Status: Downloaded newer image for node:22-alpine +{"runs":2000,"bad":0,"plainLen":16,"plainHasSpecial":false} diff --git a/documentation/audits/login-gate-2026-09-29/D/D4-grafana-r708.txt b/documentation/audits/login-gate-2026-09-29/D/D4-grafana-r708.txt new file mode 100644 index 00000000..0c3e700d --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/D/D4-grafana-r708.txt @@ -0,0 +1,10 @@ +18: - GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD:?the admin password is required (R-708)} +--- empty password: +rc=1 +error while interpolating services.grafana.environment.[]: required variable GF_SECURITY_ADMIN_PASSWORD is missing a value: the admin password is required (R-708) +--- unset: +rc=1 +error while interpolating services.grafana.environment.[]: required variable GF_SECURITY_ADMIN_PASSWORD is missing a value: the admin password is required (R-708) +--- set (control): +1 +rc=0 diff --git a/documentation/audits/login-gate-2026-09-29/D/D5-romm-zipline-pages.txt b/documentation/audits/login-gate-2026-09-29/D/D5-romm-zipline-pages.txt new file mode 100644 index 00000000..a19a4f10 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/D/D5-romm-zipline-pages.txt @@ -0,0 +1,4 @@ +romm 9202 drill catalog: known-login sentence ('ismert, k'): False | 'admin / admin' on page: False | page bytes 40950 +zipline 9202 drill catalog: known-login sentence ('ismert, k'): False | 'admin / admin' on page: False | page bytes 40440 +bookstack 9202 drill catalog: known-login sentence ('ismert, k'): False | 'admin / admin' on page: False | page bytes 40553 +control: demo-hp live catalog 2026-09-29 06:10Z showed romm's sentence (A/A2-page-warning-after.txt) diff --git a/documentation/audits/login-gate-2026-09-29/D/D6-r709-live.txt b/documentation/audits/login-gate-2026-09-29/D/D6-r709-live.txt new file mode 100644 index 00000000..df941be2 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/D/D6-r709-live.txt @@ -0,0 +1,3 @@ +mealie: reveal -> ok=True value length 30 | settings page 68582 bytes | the value in the page HTML: False | reveal control present: True +wger: reveal -> ok=True value length 30 | settings page 68080 bytes | the value in the page HTML: False | reveal control present: True +calibre-web: reveal -> ok=True value length 30 | settings page 71491 bytes | the value in the page HTML: False | reveal control present: True diff --git a/documentation/audits/login-gate-2026-09-29/D/catpatch.py b/documentation/audits/login-gate-2026-09-29/D/catpatch.py new file mode 100644 index 00000000..5ead4116 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/D/catpatch.py @@ -0,0 +1,215 @@ +# Applies the 2026-09-29 catalog changes (decision 46 gate + Part D) to a catalog checkout: argv[1] = repo root. +# Identical on the drill repo and the live repo, so the drill proves exactly what ships. +import sys, os +R = sys.argv[1] +T = lambda *p: os.path.join(R, "templates", *p) + + +def edit(path, old, new, count=1): + s = open(path).read() + n = s.count(old) + if n != count: + sys.exit(f"{path}: expected {count} of {old[:60]!r}, found {n}") + open(path, "w").write(s.replace(old, new)) + + +def after_line(path, anchor, text): + edit(path, anchor, anchor + text) + + +GATE_NOTE = """ +# --- The setup gate (controller >= 0.280.0, `09` §3 decision 46) --- +# The first visitor would create the admin. So a fresh install is closed to everyone but the household (a browser +# signed in to the dashboard) until the first setup is done%s +setup_gate: true +""" + +# --- decision 46: the gate, on four class-4 apps ------------------------------------------------------------- +probes = { + "immich": ("; immich's own status says so (measured on 9202 2026-09-29:\n# isInitialized false -> true once the admin exists).", + "setup_done_probe:\n url: http://immich-server:2283/api/server/config\n field: isInitialized\n done: \"true\"\n"), + "n8n": ("; n8n's own settings say so (measured on 9202 2026-09-29:\n# showSetupOnFirstLoad true -> false once the owner exists).", + "setup_done_probe:\n url: http://n8n:5678/rest/settings\n field: data.userManagement.showSetupOnFirstLoad\n done: \"false\"\n"), + "audiobookshelf": ("; audiobookshelf's own status says so (`/status` isInit — upstream, measured on 9202 by the\n# 2026-09-29 live proof).", + "setup_done_probe:\n url: http://audiobookshelf:80/status\n field: isInit\n done: \"true\"\n"), + "uptime-kuma": (". uptime-kuma says it only over socket.io, so the household presses \"Done, I set it up\"\n# on the app page.", ""), +} +for app, (why, probe) in probes.items(): + p = T(app, ".felhom.yml") + s = open(p).read() + assert "setup_gate" not in s, app + i = s.index("\n# --- App info") + s = s[:i] + "\n" + (GATE_NOTE % why).rstrip("\n") + "\n" + probe + s[i:] + open(p, "w").write(s) + +# --- Part D1: mealie ------------------------------------------------------------------------------------------ +p = T("mealie", ".felhom.yml") +edit(p, """ description: "Az alkalmazás aldomainje" + +# --- App info (info page content) ---""", """ description: "Az alkalmazás aldomainje" + + # `09` §3 decision 45: Mealie starts with changeme@example.com / MyPassword. The box replaces that password with + # this generated one right after the install (after_install below); the app page shows it as the first password. + - env_var: ADMIN_PASSWORD + label: "Admin jelszó (changeme@example.com)" + type: password + generate: "password:24" + description: "Az első bejelentkezéshez: changeme@example.com és ez a jelszó. Utána a profilodban módosítható." + locked_after_deploy: true + +# --- App info (info page content) ---""") +edit(p, " - 'Jelentkezz be: changeme@example.com / MyPassword'\n - 'Változtasd meg azonnal az email címet és jelszót'\n", + " - 'Jelentkezz be: changeme@example.com és a Beállítások oldalon látható első jelszó'\n - 'Változtasd meg az email címet a sajátodra'\n") +edit(p, " - 'Sign in: changeme@example.com / MyPassword'\n - 'Change the e-mail address and password straight away'\n", + " - 'Sign in: changeme@example.com and the first password shown on the settings page'\n - 'Change the e-mail address to your own'\n") +edit(p, """# --- Controller-side health probe --- +healthcheck: + checks: + - type: tcp + port: 9000""", """# --- After a fresh install (controller >= 0.279.0, decision 45) --- +# Mealie's OWN user repository sets the seeded user's password (what its scripts/change_password.py does, without the +# prompts). Measured on 9202 2026-09-29: afterwards MyPassword answers 401 at /api/auth/token, the new one 200. +after_install: + service: mealie + env: [ADMIN_PASSWORD] + # The password is the LAST ARGUMENT (sys.argv[1]), never pasted into the code: a quote in it cannot break or change + # the program (security review 2026-09-29). + command: ["python3", "-c", "import sys\\nfrom mealie.core.security.security import hash_password\\nfrom mealie.db.db_setup import session_context\\nfrom mealie.repos.repository_factory import AllRepositories\\nwith session_context() as s:\\n r = AllRepositories(s, group_id=None, household_id=None)\\n u = r.users.get_one('changeme@example.com', 'email')\\n r.users.update_password(u.id, hash_password(sys.argv[1]))\\n print('FELHOM_AFTER_INSTALL_OK')\\n", "${ADMIN_PASSWORD}"] + success: "FELHOM_AFTER_INSTALL_OK" + +# --- Controller-side health probe --- +healthcheck: + checks: + - type: tcp + port: 9000""") +edit(p, """ - env_var: SUBDOMAIN + label: 'Subdomain' + description: 'The subdomain this app answers on' +""", """ - env_var: SUBDOMAIN + label: 'Subdomain' + description: 'The subdomain this app answers on' + - env_var: ADMIN_PASSWORD + label: 'Admin password (changeme@example.com)' + description: 'For the first sign-in: changeme@example.com and this password. Change it in your profile afterwards.' +""") + +# --- Part D1: wger (+ R-712: a browser's https Origin was refused by CSRF) -------------------------------------- +p = T("wger", ".felhom.yml") +edit(p, """ generate: "hex:32" + locked_after_deploy: true + +# --- App info (info page content) ---""", """ generate: "hex:32" + locked_after_deploy: true + + # `09` §3 decision 45: wger starts with admin / adminadmin. The box replaces that password with this generated + # one right after the install (after_install below); the app page shows it as the first password. + - env_var: ADMIN_PASSWORD + label: "Admin jelszó (admin)" + type: password + generate: "password:24" + description: "Az első bejelentkezéshez: admin és ez a jelszó. Utána a beállításokban módosítható." + locked_after_deploy: true + +# --- App info (info page content) ---""") +edit(p, " - 'Jelentkezz be: admin / adminadmin'\n - 'Változtasd meg azonnal a jelszót'\n", + " - 'Jelentkezz be: admin és a Beállítások oldalon látható első jelszó'\n - 'Add meg az email címedet a beállításokban'\n") +edit(p, " - 'Sign in: admin / adminadmin'\n - 'Change the password straight away'\n", + " - 'Sign in: admin and the first password shown on the settings page'\n - 'Add your e-mail address in the settings'\n") +edit(p, "\n# --- Controller-side health probe ---\nhealthcheck:", """ +# --- After a fresh install (controller >= 0.279.0, decision 45) --- +# Django's own set_password on the seeded admin. Measured on 9202 2026-09-29: afterwards adminadmin no longer signs in +# at /en/user/login, the new one does. +after_install: + service: wger + env: [ADMIN_PASSWORD] + # The password is the LAST ARGUMENT (sys.argv[1]), never pasted into the code: a quote in it cannot break or change + # the program (security review 2026-09-29). Django is set up the way manage.py does it (settings.main). + command: ["python3", "-c", "import os, sys; sys.path.insert(0, '/home/wger/src'); os.chdir('/home/wger/src'); os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.main'); import django; django.setup(); from django.contrib.auth.models import User; u = User.objects.get(username='admin'); u.set_password(sys.argv[1]); u.save(); print('FELHOM_AFTER_INSTALL_OK')", "${ADMIN_PASSWORD}"] + success: "FELHOM_AFTER_INSTALL_OK" + +# --- Controller-side health probe --- +healthcheck:""") +edit(p, """ - env_var: SECRET_KEY + label: 'Encryption key' +""", """ - env_var: SECRET_KEY + label: 'Encryption key' + - env_var: ADMIN_PASSWORD + label: 'Admin password (admin)' + description: 'For the first sign-in: admin and this password. Change it in the settings afterwards.' +""") +p = T("wger", "docker-compose.yml") +edit(p, " - DJANGO_DB_ENGINE=django.db.backends.sqlite3\n", """ # R-712 (measured 2026-09-29 on 9202): behind traefik wger saw the request as http and refused a browser's + # https Origin with "CSRF verification failed" — nobody could sign in from a browser. + - CSRF_TRUSTED_ORIGINS=https://${SUBDOMAIN}.${DOMAIN} + - X_FORWARDED_PROTO_HEADER_SET=True + - DJANGO_DB_ENGINE=django.db.backends.sqlite3 +""") + +# --- Part D2: calibre-web ----------------------------------------------------------------------------------------- +p = T("calibre-web", ".felhom.yml") +edit(p, """ description: "A külső merevlemez elérési útja, ahol a Calibre könyvtár található" + locked_after_deploy: true + +# --- App info (info page content) ---""", """ description: "A külső merevlemez elérési útja, ahol a Calibre könyvtár található" + locked_after_deploy: true + + # `09` §3 decision 45: Calibre-Web starts with admin / admin123. The box replaces that password with this + # generated one right after the install (after_install below). Its password policy demands a special character, + # hence `:special` (controller >= 0.280.0). + - env_var: ADMIN_PASSWORD + label: "Admin jelszó (admin)" + type: password + generate: "password:24:special" + description: "Az első bejelentkezéshez: admin és ez a jelszó. Kell benne kis- és nagybetű, szám és egy különleges karakter." + locked_after_deploy: true + +# --- App info (info page content) ---""") +edit(p, " - 'Jelentkezz be: admin / admin123'\n - 'Változtasd meg azonnal a jelszót'\n", + " - 'Jelentkezz be: admin és a Beállítások oldalon látható első jelszó'\n") +edit(p, " - 'Sign in: admin / admin123'\n - 'Change the password straight away'\n", + " - 'Sign in: admin and the first password shown on the settings page'\n") +edit(p, "\n# --- Controller-side health probe ---\nhealthcheck:", """ +# --- After a fresh install (controller >= 0.280.0, decision 45) --- +# Calibre-Web's OWN `cps.py -s user:password`, as the app user. Measured on 9202 2026-09-29: afterwards admin123 no +# longer signs in, the new one does; a password without a special character is refused by its policy. +after_install: + service: calibre-web + user: abc + env: [ADMIN_PASSWORD] + command: ["python3", "/app/calibre-web-automated/cps.py", "-p", "/config/app.db", "-s", "admin:${ADMIN_PASSWORD}"] + success: "Password for user 'admin' changed" + +# --- Controller-side health probe --- +healthcheck:""") +edit(p, """ label: 'E-book library path' + description: 'The path to the external hard drive where the Calibre library lives' + placeholder: '/mnt/felhom-drives/hdd_1' +""", """ label: 'E-book library path' + description: 'The path to the external hard drive where the Calibre library lives' + placeholder: '/mnt/felhom-drives/hdd_1' + - env_var: ADMIN_PASSWORD + label: 'Admin password (admin)' + description: 'For the first sign-in: admin and this password. It needs a lower and an upper case letter, a digit and a special character.' +""") + +# --- Part D3: romm and zipline — their default_creds notes are stale (they are class 4) --------------------------- +import re +for app, cred in (("romm", "admin / admin"), ("zipline", "admin / zipline")): + p = T(app, ".felhom.yml") + s = open(p).read() + s, n = re.subn(r"(?m)^[ \t]*default_creds: ['\"]" + re.escape(cred) + r"['\"][ \t]*\n", "", s) + assert n == 2 and "default_creds" not in s, (app, n) + open(p, "w").write(s) +edit(T("romm", ".felhom.yml"), ' - "Jelentkezz be az alapértelmezett admin / admin fiókkal"\n', + ' - "Az első megnyitáskor hozd létre az admin fiókodat"\n') +edit(T("romm", ".felhom.yml"), ' - "Sign in with the default admin / admin account"\n', + ' - "On the first visit, create your admin account"\n') +edit(T("zipline", ".felhom.yml"), " - 'Jelentkezz be: admin / zipline'\n - 'Változtasd meg azonnal a jelszót'\n", + " - 'Az első megnyitáskor hozd létre az admin fiókodat'\n") +edit(T("zipline", ".felhom.yml"), " - 'Sign in: admin / zipline'\n - 'Change the password straight away'\n", + " - 'On the first visit, create your admin account'\n") + +# --- Part D4: R-708 — grafana refuses to start without its admin password; never `admin` --------------------------- +edit(T("grafana", "docker-compose.yml"), "GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD:-admin}", + "GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD:?the admin password is required (R-708)}") +print("catalog patched:", R) diff --git a/documentation/audits/login-gate-2026-09-29/D/d_cw.sh b/documentation/audits/login-gate-2026-09-29/D/d_cw.sh new file mode 100644 index 00000000..0d8e40f7 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/D/d_cw.sh @@ -0,0 +1,13 @@ +set -u +DOM=enkisfelhom.hu; J=/tmp/cwj.$$ +login() { rm -f $J; T=$(curl -sk -c $J -b $J -H "Host: c-cw.$DOM" https://127.0.0.1/login | grep -o 'name="csrf_token" value="[^"]*"' | head -1 | sed 's/.*value="//;s/"$//') + curl -sk -o /dev/null -w '%{http_code}->%{redirect_url}' -c $J -b $J -H "Host: c-cw.$DOM" -X POST --data-urlencode "csrf_token=$T" --data-urlencode "username=admin" --data-urlencode "password=$1" --data-urlencode "remember_me=on" https://127.0.0.1/login | sed "s|https\?://[^/]*||"; } +echo "calibre-web BEFORE: default -> $(login admin123) | wrong -> $(login wrongxyz123)" +# a password in the shape of generate: password:24:special (made here, never printed) +S='-_.!@#%+=' +while :; do P=$(head -c 400 /dev/urandom | tr -dc 'A-Za-z0-9_.!@#%+=-' | head -c 24); case "$P" in [A-Za-z0-9]*) ;; *) continue;; esac + echo "$P" | grep -q '[a-z]' && echo "$P" | grep -q '[A-Z]' && echo "$P" | grep -q '[0-9]' && echo "$P" | grep -q '[-_.!@#%+=]' && break; done +OUT=$(docker exec -u abc calibre-web python3 /app/calibre-web-automated/cps.py -p /config/app.db -s "admin:$P" 2>&1; echo "rc=$?") +echo "route output (no cd, absolute path, exec array): $(echo "$OUT" | grep -v -iE 'ProxyFix|magic shel|SESSION_COOKIE' | tr '\n' ' ' | sed "s/$(printf '%s' "$P" | sed 's/[.[\*^$/]/\\&/g')//g" | cut -c1-200)" +echo "calibre-web AFTER: default -> $(login admin123) | new -> $(login "$P") | wrong -> $(login wrongxyz123)" +unset P; rm -f $J diff --git a/documentation/audits/login-gate-2026-09-29/D/d_live.py b/documentation/audits/login-gate-2026-09-29/D/d_live.py new file mode 100644 index 00000000..7305499c --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/D/d_live.py @@ -0,0 +1,46 @@ +# Part D live proof (9202, controller 0.280.0, drill catalog): fresh installs of mealie, wger, calibre-web; the box's +# after_install replaces each default login; then default / generated / wrong through each app's own login. +# The generated passwords live in this process and reach the guest over STDIN only; never printed. +import json, os, subprocess, sys, time +sys.path.insert(0, '/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/pg-calcom-claper-2026-09-28/tools') +import walk as w + +APPS = {"mealie": "d-mealie", "wger": "d-wger", "calibre-web": "d-cw"} +w.login() +code, _ = w.ctl("POST", "/api/sync", {}) +w.say("sync ->", code) +for _ in range(24): + time.sleep(5) + meta = (w.ctl("GET", "/api/stacks/wger/deploy-fields")[1].get("data") or {}).get("metadata") or {} + ai = meta.get("after_install") or {} + if ai and "sys.argv[1]" in " ".join(ai.get("command") or []): + w.say("catalog synced: wger after_install passes the password as argv"); break +for app, sub in APPS.items(): + v = w.deploy_values(app, sub) + code, d = w.ctl("POST", f"/api/stacks/{app}/deploy", {"values": v, "kept_data": "fresh"}) + w.say(app, "deploy ->", code) +t0 = time.time() +left = set(APPS) +while left and time.time() - t0 < 900: + for app in sorted(left): + rec = (w.stack(app).get("app_config") or {}).get("after_install") + if rec: + w.say(app, f"after_install record after {time.time() - t0:.0f}s: ok={rec.get('ok')} detail={str(rec.get('detail'))[:80]!r}") + left.discard(app) + time.sleep(10) +for app in left: + w.say(app, "NO after_install record after 900 s") +pw = {a: w.GENERATED[a]["ADMIN_PASSWORD"] for a in APPS} +script = open(os.path.join(os.path.dirname(__file__), "d_login.sh")).read() +subprocess.run(["ssh", "hp", "cat > /root/d_login.sh && pct push 9202 /root/d_login.sh /root/d_login.sh && rm /root/d_login.sh"], + input=script, text=True, check=True, capture_output=True) +r = subprocess.run(["ssh", "hp", "pct exec 9202 -- bash /root/d_login.sh; pct exec 9202 -- rm -f /root/d_login.sh"], + input="\n".join(pw[a] for a in ("mealie", "wger", "calibre-web")) + "\n", text=True, capture_output=True, timeout=300) +out = r.stdout +for a in pw.values(): + out = out.replace(a, "") +print(out, end="") +# the page: no known-login sentence once replaced (ASCII fragment; control: a class-3 app not installed still shows it) +for app in APPS: + page = w.page(f"/apps/{app}") + w.say(app, "app page carries the known-login sentence (ASCII fragment \"ismert, k\"):", "ismert, k" in page) diff --git a/documentation/audits/login-gate-2026-09-29/D/d_login.sh b/documentation/audits/login-gate-2026-09-29/D/d_login.sh new file mode 100644 index 00000000..0223da66 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/D/d_login.sh @@ -0,0 +1,13 @@ +# Part D live: default / generated / wrong through each app's OWN login. Generated passwords on STDIN. +set -u +DOM=enkisfelhom.hu; J=/tmp/dl.$$ +read -r PM; read -r PW; read -r PC +mealie() { curl -sk -o /dev/null -w '%{http_code}' -H "Host: d-mealie.$DOM" -X POST --data-urlencode "username=changeme@example.com" --data-urlencode "password=$1" https://127.0.0.1/api/auth/token; } +wger() { rm -f $J; T=$(curl -sk -c $J -b $J -H "Host: d-wger.$DOM" https://127.0.0.1/en/user/login | grep -o 'name="csrfmiddlewaretoken" value="[^"]*"' | head -1 | sed 's/.*value="//;s/"$//') + curl -sk -o /dev/null -w '%{http_code}' -c $J -b $J -H "Host: d-wger.$DOM" -H "Origin: https://d-wger.$DOM" -H "Referer: https://d-wger.$DOM/en/user/login" -H "X-Forwarded-Proto: https" -X POST --data-urlencode "csrfmiddlewaretoken=$T" --data-urlencode "login=admin" --data-urlencode "password=$1" https://127.0.0.1/en/user/login; } +cw() { rm -f $J; T=$(curl -sk -c $J -b $J -H "Host: d-cw.$DOM" https://127.0.0.1/login | grep -o 'name="csrf_token" value="[^"]*"' | head -1 | sed 's/.*value="//;s/"$//') + curl -sk -o /dev/null -w '%{http_code}' -c $J -b $J -H "Host: d-cw.$DOM" -X POST --data-urlencode "csrf_token=$T" --data-urlencode "username=admin" --data-urlencode "password=$1" --data-urlencode "remember_me=on" https://127.0.0.1/login; } +echo "mealie (200 = signed in) default MyPassword -> $(mealie MyPassword) | generated -> $(mealie "$PM") | wrong -> $(mealie wrongxyz123)" +echo "wger (302 = signed in; the browser's https Origin, R-712) default adminadmin -> $(wger adminadmin) | generated -> $(wger "$PW") | wrong -> $(wger wrongxyz123)" +echo "calibre-web (302 = signed in) default admin123 -> $(cw admin123) | generated -> $(cw "$PC") | wrong -> $(cw wrongxyz123)" +unset PM PW PC; rm -f $J diff --git a/documentation/audits/login-gate-2026-09-29/D/d_probe.sh b/documentation/audits/login-gate-2026-09-29/D/d_probe.sh new file mode 100644 index 00000000..5b0e4177 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/D/d_probe.sh @@ -0,0 +1,27 @@ +# Part D1 probe (9202): the default login before, the app's own route with a generated password (made HERE, never +# printed), then default / new / wrong through the app's own login API via traefik. +set -u +DOM=enkisfelhom.hu +mealie_login() { curl -sk -o /dev/null -w '%{http_code}' -H "Host: c-mealie.$DOM" -X POST --data-urlencode "username=changeme@example.com" --data-urlencode "password=$1" https://127.0.0.1/api/auth/token; } +wger_login() { curl -sk -o /dev/null -w '%{http_code}' -H "Host: c-wger.$DOM" -H 'Content-Type: application/json' -X POST -d "{\"username\":\"admin\",\"password\":\"$1\"}" https://127.0.0.1/api/v2/login/; } +echo "mealie BEFORE: default -> $(mealie_login MyPassword) | wrong -> $(mealie_login wrongxyz123)" +echo "wger BEFORE: default -> $(wger_login adminadmin) | wrong -> $(wger_login wrongxyz123)" +P=$(head -c 300 /dev/urandom | tr -dc A-Za-z0-9 | head -c 24) +OUT=$(docker exec mealie python3 -c " +from mealie.core.security.security import hash_password +from mealie.db.db_setup import session_context +from mealie.repos.repository_factory import AllRepositories +with session_context() as s: + r = AllRepositories(s, group_id=None, household_id=None) + u = r.users.get_one('changeme@example.com', 'email') + r.users.update_password(u.id, hash_password('$P')) + print('FELHOM_AFTER_INSTALL_OK') +" 2>&1; echo "rc=$?") +echo "mealie route output: $(echo "$OUT" | tail -3 | tr '\n' ' ' | sed "s/$P//g")" +echo "mealie AFTER: default -> $(mealie_login MyPassword) | new -> $(mealie_login "$P") | wrong -> $(mealie_login wrongxyz123)" +Q=$(head -c 300 /dev/urandom | tr -dc A-Za-z0-9 | head -c 24) +OUT=$(docker exec wger sh -c "cd /home/wger/src && python3 manage.py shell -c \"from django.contrib.auth.models import User; u = User.objects.get(username='admin'); u.set_password('$Q'); u.save(); print('FELHOM_AFTER_INSTALL_OK')\"" 2>&1; echo "rc=$?") +echo "wger route output: $(echo "$OUT" | tail -3 | tr '\n' ' ' | sed "s/$Q//g")" +echo "wger AFTER: default -> $(wger_login adminadmin) | new -> $(wger_login "$Q") | wrong -> $(wger_login wrongxyz123)" +docker exec wger sh -c 'id; pwd' | head -2 +unset P Q diff --git a/documentation/audits/login-gate-2026-09-29/D/d_w2.sh b/documentation/audits/login-gate-2026-09-29/D/d_w2.sh new file mode 100644 index 00000000..6bef00ac --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/D/d_w2.sh @@ -0,0 +1,10 @@ +set -u +DOM=enkisfelhom.hu; J=/tmp/wj.$$ +wger_login() { rm -f $J; T=$(curl -sk -c $J -b $J -H "Host: c-wger.$DOM" https://127.0.0.1/en/user/login | grep -o 'name="csrfmiddlewaretoken" value="[^"]*"' | head -1 | sed 's/.*value="//;s/"$//') + curl -sk -o /dev/null -w '%{http_code}->%{redirect_url}' -c $J -b $J -H "Host: c-wger.$DOM" -X POST --data-urlencode "csrfmiddlewaretoken=$T" --data-urlencode "login=admin" --data-urlencode "password=$1" https://127.0.0.1/en/user/login | sed "s|https\?://[^/]*||"; } +echo "wger (after the first set_password above): default -> $(wger_login adminadmin) | wrong -> $(wger_login wrongxyz123)" +Q=$(head -c 300 /dev/urandom | tr -dc A-Za-z0-9 | head -c 24) +OUT=$(docker exec wger sh -c "cd /home/wger/src && python3 manage.py shell -c \"from django.contrib.auth.models import User; u = User.objects.get(username='admin'); u.set_password('$Q'); u.save(); print('FELHOM_AFTER_INSTALL_OK')\"" 2>&1; echo "rc=$?") +echo "wger route output: $(echo "$OUT" | tail -2 | tr '\n' ' ' | sed "s/$Q//g")" +echo "wger AFTER: default -> $(wger_login adminadmin) | new -> $(wger_login "$Q") | wrong -> $(wger_login wrongxyz123)" +unset Q; rm -f $J diff --git a/documentation/audits/login-gate-2026-09-29/D/genjs.js b/documentation/audits/login-gate-2026-09-29/D/genjs.js new file mode 100644 index 00000000..60a3a4c7 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/D/genjs.js @@ -0,0 +1,34 @@ +function generatePassword(fieldId, confirmFieldId, spec) { + const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + const SPECIAL = '-_.!@#%+='; + var parts = (spec || '').split(':'); + var n = parseInt(parts[1], 10); + if (parts[0] !== 'password' || !(n >= 12 && n <= 64)) { n = 16; } + var special = parts[2] === 'special'; + var chars = special ? letters + SPECIAL : letters; + let pass = ''; + for (;;) { + pass = ''; + const arr = new Uint32Array(n); + crypto.getRandomValues(arr); + for (let i = 0; i < n; i++) { + pass += chars[arr[i] % chars.length]; + } + if (!special || (/[a-z]/.test(pass) && /[A-Z]/.test(pass) && /[0-9]/.test(pass) && /[-_.!@#%+=]/.test(pass) && /^[A-Za-z0-9]/.test(pass))) { + break; + } + } + document.getElementById(fieldId).value = pass; + if (confirmFieldId) { + var ce = document.getElementById(confirmFieldId); + if (ce) ce.value = pass; + } +} +var out={}; +function field(){return {value:''};} +global.document={getElementById:function(id){out[id]=out[id]||{value:''};return out[id];}}; +var bad=0, n=0, specialSeen=0; +for (var k=0;k<2000;k++){ generatePassword('f','c','password:24:special'); var p=out['f'].value; n++; + if (p.length!==24||!/[a-z]/.test(p)||!/[A-Z]/.test(p)||!/[0-9]/.test(p)||!/[-_.!@#%+=]/.test(p)||!/^[A-Za-z0-9]/.test(p)||/['"$\\: `]/.test(p)||out['c'].value!==p) bad++; } +generatePassword('f','c',''); var plain=out['f'].value; +console.log(JSON.stringify({runs:n, bad:bad, plainLen:plain.length, plainHasSpecial:/[^A-Za-z0-9]/.test(plain)})); diff --git a/documentation/audits/login-gate-2026-09-29/D/graf.sh b/documentation/audits/login-gate-2026-09-29/D/graf.sh new file mode 100644 index 00000000..50522ee8 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/D/graf.sh @@ -0,0 +1,6 @@ +cd /opt/docker/stacks/grafana || exit 1 +grep -n 'GF_SECURITY_ADMIN_PASSWORD=' docker-compose.yml +echo "--- empty password:"; GF_SECURITY_ADMIN_PASSWORD= DOMAIN=x SUBDOMAIN=y docker compose -f docker-compose.yml config >/dev/null 2>/tmp/gerr; echo "rc=$?"; head -2 /tmp/gerr +echo "--- unset:"; env -u GF_SECURITY_ADMIN_PASSWORD DOMAIN=x SUBDOMAIN=y docker compose -f docker-compose.yml config >/dev/null 2>/tmp/gerr; echo "rc=$?"; head -2 /tmp/gerr +echo "--- set (control):"; GF_SECURITY_ADMIN_PASSWORD=Xy12345678 DOMAIN=x SUBDOMAIN=y docker compose -f docker-compose.yml config 2>/dev/null | grep -c 'GF_SECURITY_ADMIN_PASSWORD'; echo "rc=${PIPESTATUS[0]}" +rm -f /tmp/gerr diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index b235ad59..710fd56a 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -818,11 +818,13 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server` | **R-704** | **[P3-LOW] The box's crash-loop stop (decision 28) outlives the app: after remove and reinstall, the new install is still held.** Measured 2026-09-28 on 9202: calcom crash-looped at 08:22 and 08:28 (`unhealthy_stop`, `crash_loop`, trip 2, recorded 08:28:59Z); it was then REMOVED through the product twice and installed fresh twice (09:14:51Z the last). At 09:45 the new, healthy install's Update was refused `409 held` with the crash-loop sentence („…újra és újra összeomlott…"), and `GET /api/stacks/calcom` carried the old `hold_reason` while `state=running`. Start lifted it (`the unhealthy stop is LIFTED by Start`). So a household that removes a crash-looping app and installs it again (the obvious fix) finds its updates refused for a crash of a previous install. Not measured: whether the nightly update leg also skips it; whether other holds (restore hold) behave the same. **Fix direction:** the remove clears the app's box-set holds, as `DeleteAppBackupPrefs` clears its backup preferences (R-474). Evidence: `audits/pg-calcom-claper-2026-09-28/box/calcom/hold.txt`, `…/box/calcom/move.txt`. **-- 2026-09-28 later: SECOND and worse instance, then FIXED in controller v0.278.0.** demo-hp's fresh nextcloud (installed 10:13) carried an UPDATE hold from a nextcloud of 2026-09-13 (set before v0.242.0 made removals clear update holds; nothing ever swept it). At the manual off-site run (15:17) the backup leg logged `Skipping volume dump for nextcloud — the app is HELD stopped`, captured no unit, and pushed a snapshot that `carried NO database dump and NO volume tar` — a freshly installed app silently NOT backed up. **Fix:** a removal also clears the crash-loop stop (`settings.ClearUpdateHold`), and a new install (plain or "use my kept data") drops a leftover update/crash-loop hold of an app that is not installed (`Router.dropLeftoverHold`); restore holds (R-379) untouched. Red-proofed RP4–RP6 (`audits/kept-offsite-2026-09-28/redproofs/`). Floor 0.278.0. **STILL OPEN: live proof of the install-time drop** (a box with a leftover hold on an uninstalled app). | **WATCHING — P2; owner: CC (install-time drop, live)** | | **R-705** | **[P3-LOW] There is no way to run the night's chain now — only its pieces.** Asked by the operator 2026-09-28 (to finish a proof in the day). What exists (read from source, controller v0.278.0): the backup page's off-site run-now (`POST /backup/offbox/run`) runs the dump leg first (the R-44 pre-phase: DB dumps, volume dumps with brief app stops, unit capture) and then the push — used live on demo-hp 2026-09-28 15:17, 3m57s; the debug API has `backup/dbdump`, `backup/crossdrive` (Tier 2), `backup/integrity`, `backup/offsite-proof`. **Missing:** the automatic update leg (`RunUpdateLeg`, chained only to the scheduled off-site job) and the whole-guest backup (the agent's, on its own 24 h / 7 d cadence) have no manual trigger; the only way to run the chain in order is to move the backup window (`POST /backups/window`), which takes W..W+2h at least. **Needs:** a debug action "run tonight's chain now" (dump → Tier 2 → off-site → update leg, in order, one at a time), and an agent-side "whole-guest backup now" for demo boxes. Not built. **-- 2026-09-28 evening: the controller half BUILT (v0.279.0):** debug `POST /api/debug/backup/night-chain` runs dump → Tier 2 → off-site → update leg in order, one at a time, refused while anything else runs; the leg gets its normal length from its own start. Proven on 9202: 44 s, a second press 409, the leg deadline 22:00. **Still open: the whole-guest backup (agent side) has no manual trigger.** | **OPEN — P3, agent half only; owner: CC** | | **R-706** | **[P3-LOW] Removing an app "with its backups" leaves its off-site verification copy on the drive.** Measured 2026-09-28 on demo-hp: after a full off-site restore of nextcloud (which leaves the downloaded copy in `backups/offsite-restore/nextcloud`, ~1 GB, by design, for the household to inspect), `POST /api/stacks/nextcloud/remove` with `remove_backups: true` removed the unit and listed `backup_paths_removed` WITHOUT the verification copy; it stayed until the restore page's own delete (`POST /backup/offbox/verify-copy/delete`, 302 `scratch_deleted`). A household that removes an app to free space keeps 1 GB it cannot see on the app list. **Fix direction:** the removal with backups also deletes the app's verification copy (the same `DeleteOffsiteRestoreCopy`). Evidence: `audits/kept-offsite-2026-09-28/E/E9-teardown.txt`. **-- 2026-09-28 evening: FIXED in controller v0.279.0** — a removal with its backups also deletes the verification copy and lists it among the removed paths (`TestR706_…`, red-proofed RP7). Not yet seen live (needs a full off-site restore then a removal). | **WATCHING — P3; owner: CC (live)** | -| **R-707** | **[P2] 37 apps still start with a login a stranger can take (`09` §3 decision 45).** Audit of all 53 apps: `app-catalog-felhom.eu/FIRST-ADMIN.md` (class, fix route, status, measured or read). Open: **3 hard-coded defaults** — calibre-web (`admin / admin123`, measured working on demo-hp and 9202; its own `cps.py -s` route needs a generated password WITH a special character — our generator is letters+digits, a controller change), mealie (`changeme@example.com / MyPassword`), wger (`admin / adminadmin`); **34 open first-run screens** (the first visitor creates the admin: actualbudget, adventurelog, audiobookshelf, calcom, docmost, emby, ghost, gitea, gramps-web, home-assistant, homebox, immich, jellyfin, komga, n8n, navidrome, opengist, outline, papra, plant-it, radarr, rallly, recipe-importer, romm, seerr, sonarr, sparkyfitness, tandoor, termix, uptime-kuma, vikunja, wanderer, wishlist, zipline). **Stale notes:** romm's `default_creds` `admin / admin` answers 401 on demo-hp (like a wrong password) — the page now warns with a login that does not exist; zipline's looks stale too. **Measured on demo-hp 2026-09-28 (read-only):** bookstack's default still logs in on the INSTALLED app (the fix is for new installs; the page now warns). Each fix: route (a) env or (b) the app's own CLI/API via `after_install:`, proven on 9202 with the default failing and the generated password working; route (c) a page sentence. Several sessions (operator, 2026-09-28). | **OPEN — P2; owner: CC** | -| **R-708** | **[P3-LOW] grafana falls back to password `admin` when its admin field is empty.** `templates/grafana/docker-compose.yml:18` `GF_SECURITY_ADMIN_PASSWORD=${…:-admin}` (read 2026-09-28, the audit). Today the field is generated and required, so it is never empty on a normal install — but an edit, an import or a restore that drops the value would publish grafana with `admin / admin`. **Fix direction:** no default in the compose (`${GF_SECURITY_ADMIN_PASSWORD:?}` refuses to start instead). | **OPEN — P3; owner: CC** | -| **R-709** | **[P3-LOW] The deploy page writes the generated admin passwords of installed apps into its HTML.** `internal/web/templates/deploy.html` renders a `type: password` field's decrypted value into a disabled `` (read 2026-09-28; used by the proofs of R-702/R-707 to read the first password as the household sees it). `type: secret` fields got a fetch-on-demand reveal in R-254; `type: password` fields did not. The page needs a login, so this is exposure to a logged-in session's HTML (browser cache, a shared screen, a saved page), not to strangers. **Fix direction:** the R-254 reveal for password fields too. | **OPEN — P3; owner: CC** | +| **R-707** | **[P2] 37 apps still start with a login a stranger can take (`09` §3 decision 45).** Audit of all 53 apps: `app-catalog-felhom.eu/FIRST-ADMIN.md` (class, fix route, status, measured or read). Open: **3 hard-coded defaults** — calibre-web (`admin / admin123`, measured working on demo-hp and 9202; its own `cps.py -s` route needs a generated password WITH a special character — our generator is letters+digits, a controller change), mealie (`changeme@example.com / MyPassword`), wger (`admin / adminadmin`); **34 open first-run screens** (the first visitor creates the admin: actualbudget, adventurelog, audiobookshelf, calcom, docmost, emby, ghost, gitea, gramps-web, home-assistant, homebox, immich, jellyfin, komga, n8n, navidrome, opengist, outline, papra, plant-it, radarr, rallly, recipe-importer, romm, seerr, sonarr, sparkyfitness, tandoor, termix, uptime-kuma, vikunja, wanderer, wishlist, zipline). **Stale notes:** romm's `default_creds` `admin / admin` answers 401 on demo-hp (like a wrong password) — the page now warns with a login that does not exist; zipline's looks stale too. **Measured on demo-hp 2026-09-28 (read-only):** bookstack's default still logs in on the INSTALLED app (the fix is for new installs; the page now warns). Each fix: route (a) env or (b) the app's own CLI/API via `after_install:`, proven on 9202 with the default failing and the generated password working; route (c) a page sentence. Several sessions (operator, 2026-09-28). **2026-09-29 (controller v0.280.0, catalog `d0e7e2e`):** every class-3 app fixed — mealie, wger, calibre-web by `after_install` (calibre-web with the new `password:24:special`), proven on 9202 fresh installs (`audits/login-gate-2026-09-29/D/`); the setup gate (decision 46, spike PASSED) built and live on immich, n8n, audiobookshelf (probes measured) and uptime-kuma (button) (`…/C/`); romm's and zipline's stale notes removed. **Left: 30 class-4 apps** — gate each (probe measured on 9202 where one exists — 11 upstream candidates listed in `…/B/B-VERDICT.md` §3; the button otherwise). | **OPEN — P2; owner: CC; 30 of 37 left** | +| **R-708** | **[P3-LOW] grafana falls back to password `admin` when its admin field is empty.** `templates/grafana/docker-compose.yml:18` `GF_SECURITY_ADMIN_PASSWORD=${…:-admin}` (read 2026-09-28, the audit). Today the field is generated and required, so it is never empty on a normal install — but an edit, an import or a restore that drops the value would publish grafana with `admin / admin`. **Fix direction:** no default in the compose (`${GF_SECURITY_ADMIN_PASSWORD:?}` refuses to start instead). **Fixed 2026-09-29** (catalog `d0e7e2e`): `${GF_SECURITY_ADMIN_PASSWORD:?…}` — `docker compose config` with it empty or unset exits 1 naming R-708, set → 0 (`audits/login-gate-2026-09-29/D/D4-grafana-r708.txt`). | **CLOSED — 2026-09-29** | +| **R-709** | **[P3-LOW] The deploy page writes the generated admin passwords of installed apps into its HTML.** `internal/web/templates/deploy.html` renders a `type: password` field's decrypted value into a disabled `` (read 2026-09-28; used by the proofs of R-702/R-707 to read the first password as the household sees it). `type: secret` fields got a fetch-on-demand reveal in R-254; `type: password` fields did not. The page needs a login, so this is exposure to a logged-in session's HTML (browser cache, a shared screen, a saved page), not to strangers. **Fix direction:** the R-254 reveal for password fields too. **Fixed in controller v0.280.0:** an installed app's password field renders empty with a reveal eye (`/stacks//auto-field/reveal` now serves `type: password` of an installed app, never a restore-generated one). Red-proofs RP14/RP15; live on 9202: mealie, wger, calibre-web — the revealed value is NOT in the settings page HTML (`…/D/D6-r709-live.txt`). | **CLOSED — 2026-09-29** | | **R-710** | **[P2-MEDIUM] An app installed before its template gained an `after_install:` is never warned about its default login.** MEASURED 2026-09-29 on demo-hp: bookstack's page carried no known-login sentence although its default `admin@admin.com / password` still logged in (the app was installed before the catalog added bookstack's `after_install` on 2026-09-28; the command never runs for an installed app). `internal/web/known_login.go` reads an ABSENT `after_install` record as "not run yet" for ever. Evidence `audits/login-gate-2026-09-29/A/A2-page-warning-after.txt`. Also: the page has no way to learn of a password the household changed by hand (the brief's Part A4). **Fix direction:** absent record + installed longer than the command's window = in effect; a household "I changed it" press recorded in `app.yaml`. | **OPEN — P2; owner: CC** | | **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. | **OPEN — P2; owner: CC** | +| **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). | **OPEN — P3; owner: CC** |