diff --git a/documentation/audits/login-gate-2026-09-29/B/B-VERDICT.md b/documentation/audits/login-gate-2026-09-29/B/B-VERDICT.md new file mode 100644 index 00000000..97a0bc36 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/B-VERDICT.md @@ -0,0 +1,112 @@ +# Part B — SPIKE: the setup gate (decision 46). Verdict: **PASS**, written before any build. + +Scratch guest 9202 (controller 0.279.0, traefik v3.6.7), 2026-09-29 06:11–06:19Z. Two real class-4 apps from the live +catalog, installed through the product: **n8n** (web only) and **immich** (has a phone app). The gate was a throwaway +prototype (`gate.py`, python in a container on `traefik-public`) plus one traefik file-provider file (`gate-spike.yml`). +Nothing was released. Both apps, the prototype and the file were removed at the end (`td.sh`, product remove). +Architecture read first: `01-topology-and-trust.md` §5 (trust boundaries), `04-control-plane-authorization.md` +(nothing there covers who may reach an app — see finding F1), controller `internal/infra/infra.go` +(`RenderControllerRoute`), `internal/stacks/infra.go` (`wireController`), `internal/web/auth.go` (session cookie). + +## How the prototype works (the shape a build would copy) + +- A traefik **file-provider router** per gated app: `Host()`, priority 100000, middleware `forwardAuth`, + service `@docker` (the app's own docker-label service). The app's compose and labels are **not touched**. +- The forwardAuth answerer lets a request through only with a **gate cookie** for that app host. +- Without one: a browser `GET` is sent to `https://felhom./__gate/start?rd=…` (the dashboard host, where the + household's dashboard cookie already is). With a valid dashboard session the answerer mints a **60-second, one-use + token bound to the app host** and sends the browser to `https:///__felhom_gate/cb?t=…`; the answerer swaps it + for a **host-only** gate cookie (`HttpOnly; Secure; SameSite=Lax`, HMAC over the host) and sends the browser back. + Anything else (API style, non-GET) gets `401 {"error":"this app is waiting for its first setup"}`. +- **Open** = the gate's router is removed. The app's own router is then the only one — exactly the never-gated state. + +## Answers + +**1. Can the gate see the dashboard session on the app's own address? — No, and it does not need to.** +The dashboard cookie `felhom_session` is set with no `Domain` (`auth.go:207-215`), so it is **host-only**: +`felhom.` only. Measured: after the household flow the browser holds `felhom_session` for `felhom.` only, +and a `felhom_gate` for each app host (`B2-…txt`, "dashboard cookie on an app host? False"). The smallest honest route is +the **redirect handshake** above — the dashboard cookie is never widened. Widening it (`Domain=`) would have +sent the household's full dashboard session to **every app's backend on every request** — third-party software that +logs and handles headers as it likes. Not done, not needed. + +**2. Browser, logged in / not logged in.** +- Household, logged in to the dashboard: `302 app → 302 felhom/__gate/start → 302 app/__felhom_gate/cb → 200 app`, + **0.21–0.22 s**, no extra click. n8n's page (52 kB) and immich's (10.7 kB) load (`B2d`). +- Household on a phone, not logged in: the gate page → its sign-in link → the dashboard login → straight back to the + app (`B2e`: `302 /login → 302 /__gate/start → 302 cb → 200 app`). One sign-in, nothing else. +- Stranger, browser: the gate page, 200, both sentences (checked with an English fragment and the ASCII fragment + `Felhom vez`); **no app HTML** (`B2a`). Stranger, API style: `401` on `GET /rest/settings`, `POST /rest/owner/setup`, + `GET /api/server/config`, `POST /api/auth/admin-sign-up` (`B2b`). A forged token → 403; `rd=https://evil.example/` → + 400; n8n's gate cookie presented to immich → 401 (`B2c`, `B2f`). +- **During the install** (`B1-stranger-during-install.log`): a stranger polled both hosts every second, API- and + browser-style, from before the deploy until after the setup. 06:11:53–06:16:47: **~530 polls, 0 app answers** — each + host went straight from traefik's `404` (app not yet up) to the gate's `401/302`. This held because the gate's file + was written **before** the app existed: traefik logged `the service "n8n@docker" does not exist` and enabled the + router the moment the app's service appeared. **A build must write the gate before the app's first start** (F2). + +**3. "Setup done" — how the box knows.** Measured on both apps, read-only, from the docker network (not through the gate): + +| app | probe | before setup | after the household's setup | +|---|---|---|---| +| n8n | `GET /rest/settings` → `data.userManagement.showSetupOnFirstLoad` | `True` | `False` | +| immich | `GET /api/server/config` → `isInitialized` | `False` | `True` | + +The household's setup went **through the gate** (`B3-setup.txt`: n8n owner setup 200, immich admin sign-up 201). +The 34 class-4 apps — **M** measured here; **U** upstream, read or remembered, NOT measured; "–" none known: + +| probe | apps | +|---|---| +| **M** | n8n, immich | +| **U** — a read-only status field exists upstream | actualbudget (`/account/needs-bootstrap`), audiobookshelf (`/status` `isInit`), emby + jellyfin (`/System/Info/Public` `StartupWizardCompleted`), ghost (`/ghost/api/admin/authentication/setup/`), home-assistant (`/api/onboarding`), komga (`/api/v1/claim` `isClaimed`), romm (`/api/heartbeat` setup-wizard flag), seerr (`/api/v1/settings/public` `initialized`), zipline (`/api/setup` `firstSetup`), navidrome (`firstTime` in the served app config), gitea (install lock) | +| – none known: the household's button | adventurelog, calcom (its measured signal is a POST, not a read), docmost, gramps-web, homebox, opengist, outline, papra, plant-it, radarr, rallly, recipe-importer (ours — can gain one), sonarr, sparkyfitness, tandoor, termix, uptime-kuma (socket.io only), vikunja, wanderer, wishlist | + +**14 of 34** have a probe (2 measured, 12 upstream). **20 need the button.** The brief's "most class-4 apps expose a +setup-done status" is wrong. + +**4. After the gate opens.** Opening = removing the gate's router (`B4-open.txt`): the app answered a cookie-less +request **0.2 s** later; the gate answerer was then **stopped**, and the app kept answering; immich's container labels +carry no middleware (the gate never touched them). immich's **phone-app flow** (no browser cookie, Bearer token): +`POST /api/auth/login` 201, `GET /api/users/me`, `/api/server/ping`, `/api/server/version` all 200. n8n's login without a +browser cookie: 200 (`B4-open-after.txt`). While gated, the same calls got `401` from the gate (`B4-gated.txt`). +Nothing of the gate remains in the request path. + +**5. Cost.** +- Latency while gated: household `GET /api/server/ping` ×40, median **52.2 ms** gated vs **50.1 ms** open (new TLS + connection each) — **~2 ms**, from a python prototype. +- The answerer down (models "the controller is down"): gated apps answer **500, empty** (`B5`) — closed, not open. + Acceptable only because a gated app is not in use yet; an opened app does not depend on the controller at all. +- A household that installs immich and opens the **phone app first**: the phone app gets 401 and shows an error until + the web setup is done. immich needs the web setup for its first admin anyway; the app page must say "finish the + setup in the browser first". +- An app with **no probe** stays gated until the household presses the button: family members without the dashboard + password, and phone apps, cannot reach it until then. The page must say so. + +**6. Exit test.** + +| condition | held? | evidence | +|---|---|---| +| a stranger never reaches the first-setup screen | **yes** — browser and API, during install and after, both apps | B1, B2a–c | +| the household reaches it with no extra step beyond being logged in | **yes** — 3 redirects, 0.2 s; one sign-in when not logged in | B2d, B2e | +| the gate opens by itself (probe) or by one press (button) | **yes, in principle** — both probes flip on the setup; the open itself was done by hand in the spike (the controller's polling is the build) | B3, B4-open | +| after opening, the app and its phone app work unchanged | **yes** | B4-open-after | +| nothing widens who can reach anything else | **yes** — dashboard cookie stays host-only; gate cookie bound to one app host; the one new path on the dashboard host (`/__gate/start`) refuses non-gated targets | B2c, B2d, B2f | + +**Verdict: PASS.** Part C may be built. + +## Findings for the build (and the record) + +- **F1 — no architecture document says who may reach an app and through what.** `01` §5 has one row ("end-user ↔ + apps: Tunnel → Traefik (Host routing)"); `04` covers the control plane only. The gate adds the controller in front of + a not-yet-set-up app — the design record belongs in `01` §5 (Part C4). +- **F2 — order matters.** The gate must exist before the app's first start. Written first, traefik holds the router + and enables it with the app's service — no window (measured). Written after, the app is open until it lands. +- **F3 — the gate answers "who becomes the admin", not "who may sign up".** About a dozen class-4 apps keep **open + registration** after setup (adventurelog, homebox, papra, plant-it, sparkyfitness, vikunja, wanderer, rallly, opengist, + wishlist, termix, docmost — READ from `FIRST-ADMIN.md`, not measured). Once the gate opens, a stranger can still make an ordinary account there. That is a + separate risk (route (a): disable sign-up after the first user) — recorded as a row, not solved by the gate. +- **F4 — a second host.** `wanderer` publishes its database admin on a second subdomain (`SUBDOMAIN_DB`); a gate must + cover every `Host(…)` an app's labels publish, and a path-scoped router (adventurelog's backend router) — hence the + explicit high priority. +- **F5 — the dashboard's sessions live in memory** (`auth.go`, `s.sessions`): a controller restart signs everyone out. + The gate cookie's key must be persisted, or a restart also re-gates every browser (harmless — one more sign-in). diff --git a/documentation/audits/login-gate-2026-09-29/B/B1-stranger-during-install.log b/documentation/audits/login-gate-2026-09-29/B/B1-stranger-during-install.log new file mode 100644 index 00000000..a27c7121 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/B1-stranger-during-install.log @@ -0,0 +1,659 @@ +# stranger.sh output, 06:11:53-06:19Z: per line time, host, API-style answer, browser-style answer. Gate opened 06:16:47. +06:11:53 g-n8n api=[404 page not found|404] browser=404 +06:11:53 g-immich api=[404 page not found|404] browser=404 +06:11:54 g-n8n api=[404 page not found|404] browser=404 +06:11:54 g-immich api=[404 page not found|404] browser=404 +06:11:55 g-n8n api=[404 page not found|404] browser=404 +06:11:55 g-immich api=[404 page not found|404] browser=404 +06:11:56 g-n8n api=[404 page not found|404] browser=404 +06:11:56 g-immich api=[404 page not found|404] browser=404 +06:11:57 g-n8n api=[404 page not found|404] browser=404 +06:11:57 g-immich api=[404 page not found|404] browser=404 +06:11:58 g-n8n api=[404 page not found|404] browser=404 +06:11:58 g-immich api=[404 page not found|404] browser=404 +06:11:59 g-n8n api=[404 page not found|404] browser=404 +06:12:00 g-immich api=[404 page not found|404] browser=404 +06:12:01 g-n8n api=[404 page not found|404] browser=404 +06:12:01 g-immich api=[404 page not found|404] browser=404 +06:12:02 g-n8n api=[404 page not found|404] browser=404 +06:12:02 g-immich api=[404 page not found|404] browser=404 +06:12:03 g-n8n api=[404 page not found|404] browser=404 +06:12:03 g-immich api=[404 page not found|404] browser=404 +06:12:04 g-n8n api=[404 page not found|404] browser=404 +06:12:04 g-immich api=[404 page not found|404] browser=404 +06:12:05 g-n8n api=[404 page not found|404] browser=404 +06:12:05 g-immich api=[404 page not found|404] browser=404 +06:12:06 g-n8n api=[404 page not found|404] browser=404 +06:12:06 g-immich api=[404 page not found|404] browser=404 +06:12:07 g-n8n api=[404 page not found|404] browser=404 +06:12:07 g-immich api=[404 page not found|404] browser=404 +06:12:08 g-n8n api=[404 page not found|404] browser=404 +06:12:08 g-immich api=[404 page not found|404] browser=404 +06:12:09 g-n8n api=[404 page not found|404] browser=404 +06:12:09 g-immich api=[404 page not found|404] browser=404 +06:12:10 g-n8n api=[404 page not found|404] browser=404 +06:12:11 g-immich api=[404 page not found|404] browser=404 +06:12:12 g-n8n api=[404 page not found|404] browser=404 +06:12:12 g-immich api=[404 page not found|404] browser=404 +06:12:13 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:13 g-immich api=[404 page not found|404] browser=404 +06:12:14 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:14 g-immich api=[404 page not found|404] browser=404 +06:12:15 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:15 g-immich api=[404 page not found|404] browser=404 +06:12:16 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:16 g-immich api=[404 page not found|404] browser=404 +06:12:17 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:17 g-immich api=[404 page not found|404] browser=404 +06:12:18 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:18 g-immich api=[404 page not found|404] browser=404 +06:12:19 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:19 g-immich api=[404 page not found|404] browser=404 +06:12:20 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:20 g-immich api=[404 page not found|404] browser=404 +06:12:22 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:22 g-immich api=[404 page not found|404] browser=404 +06:12:23 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:23 g-immich api=[404 page not found|404] browser=404 +06:12:24 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:24 g-immich api=[404 page not found|404] browser=404 +06:12:25 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:25 g-immich api=[404 page not found|404] browser=404 +06:12:26 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:26 g-immich api=[404 page not found|404] browser=404 +06:12:27 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:27 g-immich api=[404 page not found|404] browser=404 +06:12:28 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:28 g-immich api=[404 page not found|404] browser=404 +06:12:29 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:29 g-immich api=[404 page not found|404] browser=404 +06:12:30 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:30 g-immich api=[404 page not found|404] browser=404 +06:12:32 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:32 g-immich api=[404 page not found|404] browser=404 +06:12:33 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:33 g-immich api=[404 page not found|404] browser=404 +06:12:34 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:34 g-immich api=[404 page not found|404] browser=404 +06:12:35 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:35 g-immich api=[404 page not found|404] browser=404 +06:12:36 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:36 g-immich api=[404 page not found|404] browser=404 +06:12:37 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:37 g-immich api=[404 page not found|404] browser=404 +06:12:38 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:38 g-immich api=[404 page not found|404] browser=404 +06:12:39 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:39 g-immich api=[404 page not found|404] browser=404 +06:12:40 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:40 g-immich api=[404 page not found|404] browser=404 +06:12:41 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:41 g-immich api=[404 page not found|404] browser=404 +06:12:43 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:43 g-immich api=[404 page not found|404] browser=404 +06:12:44 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:44 g-immich api=[404 page not found|404] browser=404 +06:12:45 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:45 g-immich api=[404 page not found|404] browser=404 +06:12:46 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:46 g-immich api=[404 page not found|404] browser=404 +06:12:47 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:47 g-immich api=[404 page not found|404] browser=404 +06:12:48 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:48 g-immich api=[404 page not found|404] browser=404 +06:12:49 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:49 g-immich api=[404 page not found|404] browser=404 +06:12:50 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:50 g-immich api=[404 page not found|404] browser=404 +06:12:51 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:51 g-immich api=[404 page not found|404] browser=404 +06:12:52 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:53 g-immich api=[404 page not found|404] browser=404 +06:12:54 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:54 g-immich api=[404 page not found|404] browser=404 +06:12:55 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:55 g-immich api=[404 page not found|404] browser=404 +06:12:56 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:56 g-immich api=[404 page not found|404] browser=404 +06:12:57 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:57 g-immich api=[404 page not found|404] browser=404 +06:12:58 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:58 g-immich api=[404 page not found|404] browser=404 +06:12:59 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:12:59 g-immich api=[404 page not found|404] browser=404 +06:13:00 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:00 g-immich api=[404 page not found|404] browser=404 +06:13:01 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:01 g-immich api=[404 page not found|404] browser=404 +06:13:02 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:02 g-immich api=[404 page not found|404] browser=404 +06:13:04 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:04 g-immich api=[404 page not found|404] browser=404 +06:13:05 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:05 g-immich api=[404 page not found|404] browser=404 +06:13:06 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:06 g-immich api=[404 page not found|404] browser=404 +06:13:07 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:07 g-immich api=[404 page not found|404] browser=404 +06:13:08 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:08 g-immich api=[404 page not found|404] browser=404 +06:13:09 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:09 g-immich api=[404 page not found|404] browser=404 +06:13:10 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:10 g-immich api=[404 page not found|404] browser=404 +06:13:11 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:11 g-immich api=[404 page not found|404] browser=404 +06:13:12 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:12 g-immich api=[404 page not found|404] browser=404 +06:13:13 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:14 g-immich api=[404 page not found|404] browser=404 +06:13:15 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:15 g-immich api=[404 page not found|404] browser=404 +06:13:16 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:16 g-immich api=[404 page not found|404] browser=404 +06:13:17 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:17 g-immich api=[404 page not found|404] browser=404 +06:13:18 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:18 g-immich api=[404 page not found|404] browser=404 +06:13:19 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:19 g-immich api=[404 page not found|404] browser=404 +06:13:20 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:20 g-immich api=[404 page not found|404] browser=404 +06:13:21 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:21 g-immich api=[404 page not found|404] browser=404 +06:13:22 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:22 g-immich api=[404 page not found|404] browser=404 +06:13:23 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:23 g-immich api=[404 page not found|404] browser=404 +06:13:25 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:25 g-immich api=[404 page not found|404] browser=404 +06:13:26 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:26 g-immich api=[404 page not found|404] browser=404 +06:13:27 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:27 g-immich api=[404 page not found|404] browser=404 +06:13:28 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:28 g-immich api=[404 page not found|404] browser=404 +06:13:29 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:29 g-immich api=[404 page not found|404] browser=404 +06:13:30 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:30 g-immich api=[404 page not found|404] browser=404 +06:13:31 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:31 g-immich api=[404 page not found|404] browser=404 +06:13:32 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:32 g-immich api=[404 page not found|404] browser=404 +06:13:33 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:33 g-immich api=[404 page not found|404] browser=404 +06:13:34 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:35 g-immich api=[404 page not found|404] browser=404 +06:13:36 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:36 g-immich api=[404 page not found|404] browser=404 +06:13:37 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:37 g-immich api=[404 page not found|404] browser=404 +06:13:38 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:38 g-immich api=[404 page not found|404] browser=404 +06:13:39 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:39 g-immich api=[404 page not found|404] browser=404 +06:13:40 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:40 g-immich api=[404 page not found|404] browser=404 +06:13:41 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:41 g-immich api=[404 page not found|404] browser=404 +06:13:42 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:42 g-immich api=[404 page not found|404] browser=404 +06:13:43 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:43 g-immich api=[404 page not found|404] browser=404 +06:13:44 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:44 g-immich api=[404 page not found|404] browser=404 +06:13:46 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:46 g-immich api=[404 page not found|404] browser=404 +06:13:47 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:47 g-immich api=[404 page not found|404] browser=404 +06:13:48 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:48 g-immich api=[404 page not found|404] browser=404 +06:13:49 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:49 g-immich api=[404 page not found|404] browser=404 +06:13:50 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:50 g-immich api=[404 page not found|404] browser=404 +06:13:51 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:51 g-immich api=[404 page not found|404] browser=404 +06:13:52 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:52 g-immich api=[404 page not found|404] browser=404 +06:13:53 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:53 g-immich api=[404 page not found|404] browser=404 +06:13:54 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:54 g-immich api=[404 page not found|404] browser=404 +06:13:55 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:55 g-immich api=[404 page not found|404] browser=404 +06:13:57 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:57 g-immich api=[404 page not found|404] browser=404 +06:13:58 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:58 g-immich api=[404 page not found|404] browser=404 +06:13:59 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:13:59 g-immich api=[404 page not found|404] browser=404 +06:14:00 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:00 g-immich api=[404 page not found|404] browser=404 +06:14:01 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:01 g-immich api=[404 page not found|404] browser=404 +06:14:02 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:02 g-immich api=[404 page not found|404] browser=404 +06:14:03 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:03 g-immich api=[404 page not found|404] browser=404 +06:14:04 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:04 g-immich api=[404 page not found|404] browser=404 +06:14:05 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:05 g-immich api=[404 page not found|404] browser=404 +06:14:06 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:07 g-immich api=[404 page not found|404] browser=404 +06:14:08 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:08 g-immich api=[404 page not found|404] browser=404 +06:14:09 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:09 g-immich api=[404 page not found|404] browser=404 +06:14:10 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:10 g-immich api=[404 page not found|404] browser=404 +06:14:11 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:11 g-immich api=[404 page not found|404] browser=404 +06:14:12 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:12 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:13 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:13 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:14 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:14 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:15 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:15 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:16 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:17 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:18 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:18 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:19 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:19 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:20 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:20 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:21 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:21 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:22 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:22 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:23 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:23 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:24 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:24 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:25 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:25 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:26 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:27 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:28 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:28 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:29 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:29 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:30 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:30 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:31 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:31 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:32 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:32 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:33 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:33 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:34 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:34 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:35 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:35 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:36 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:37 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:38 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:38 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:39 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:39 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:40 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:40 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:41 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:41 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:42 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:42 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:43 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:43 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:44 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:44 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:45 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:45 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:46 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:47 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:48 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:48 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:49 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:49 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:50 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:50 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:51 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:51 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:52 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:52 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:53 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:53 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:54 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:54 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:55 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:55 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:56 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:56 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:58 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:58 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:59 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:14:59 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:00 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:00 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:01 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:01 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:02 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:02 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:03 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:03 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:04 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:04 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:05 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:05 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:06 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:06 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:07 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:08 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:09 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:09 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:10 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:10 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:11 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:11 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:12 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:12 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:13 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:13 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:14 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:14 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:15 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:15 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:16 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:16 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:17 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:18 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:19 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:19 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:20 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:20 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:21 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:21 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:22 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:22 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:23 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:23 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:24 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:24 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:25 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:25 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:26 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:26 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:27 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:28 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:29 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:29 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:30 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:30 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:31 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:31 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:32 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:32 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:33 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:33 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:34 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:34 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:35 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:35 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:36 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:36 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:37 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:37 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:39 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:39 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:40 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:40 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:41 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:41 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:42 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:42 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:43 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:43 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:44 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:44 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:45 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:45 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:46 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:46 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:47 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:47 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:49 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:49 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:50 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:50 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:51 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:51 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:52 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:52 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:53 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:53 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:54 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:54 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:55 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:55 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:56 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:56 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:57 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:57 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:59 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:15:59 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:00 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:00 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:01 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:01 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:02 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:02 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:03 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:03 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:04 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:04 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:05 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:05 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:06 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:06 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:07 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:07 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:08 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:09 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:10 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:10 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:11 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:11 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:12 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:12 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:13 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:13 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:14 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:14 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:15 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:15 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:16 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:16 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:17 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:17 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:18 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:19 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:20 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:20 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:21 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:21 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:22 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:22 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:23 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:23 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:24 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:24 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:25 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:25 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:26 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:26 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:27 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:27 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:28 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:29 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:30 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:30 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:31 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:31 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:32 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:32 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:33 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:33 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:34 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:34 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:35 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:35 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:36 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:36 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:37 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:37 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:38 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:38 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:40 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:40 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:41 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:41 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:42 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:42 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:43 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:43 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:44 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:44 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:45 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:45 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:46 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:46 g-immich api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:47 g-n8n api=[{"error":"this app is waiting for its first setup"}|401] browser=302 +06:16:47 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:16:48 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:16:49 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:16:50 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:16:50 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:16:51 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:16:51 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:16:52 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:16:52 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:16:53 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:16:53 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:16:54 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:16:54 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:16:55 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:16:55 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:16:56 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:16:56 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:16:57 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:16:58 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:16:59 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:16:59 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:00 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:00 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:01 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:01 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:02 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:02 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:03 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:03 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:04 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:04 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:05 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:06 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:07 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:07 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:08 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:08 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:09 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:09 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:10 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:10 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:11 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:11 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:12 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:12 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:13 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:13 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:15 g-n8n api=[|500] browser=500 +06:17:15 g-immich api=[|500] browser=500 +06:17:16 g-n8n api=[|500] browser=500 +06:17:16 g-immich api=[|500] browser=500 +06:17:17 g-n8n api=[|500] browser=500 +06:17:17 g-immich api=[|500] browser=500 +06:17:18 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:18 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:19 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:19 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:20 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:20 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:21 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:22 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:23 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:23 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:24 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:24 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:25 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:25 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:26 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:26 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:27 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:27 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:28 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:28 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:29 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:29 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:30 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:31 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:32 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:32 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:33 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:33 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:34 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:34 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:35 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:35 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:36 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:36 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:37 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:37 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:38 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:38 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:39 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:40 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:41 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:41 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:42 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:42 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:43 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:43 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:44 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:44 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:45 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:45 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:46 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:46 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:47 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:47 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:48 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:49 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:50 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:50 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:51 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:51 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:52 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:52 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:53 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:53 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:54 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:54 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:55 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:55 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:56 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:56 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 +06:17:57 g-n8n api=[{"data":{"settingsMode":"public","defaultLocale":"en","userManagement":{"authenticationMet] browser=200 +06:17:58 g-immich api=[{"loginPageMessage":"","trashDays":30,"userDeleteDelay":7,"oauthButtonText":"Login with OA] browser=200 diff --git a/documentation/audits/login-gate-2026-09-29/B/B2-stranger-household.txt b/documentation/audits/login-gate-2026-09-29/B/B2-stranger-household.txt new file mode 100644 index 00000000..c6678a62 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/B2-stranger-household.txt @@ -0,0 +1,26 @@ +== B2a STRANGER, browser, no dashboard session + https://g-n8n.enkisfelhom.hu/ : final 200 | 302 g-n8n.enkisfelhom.hu/ -> 200 felhom.enkisfelhom.hu/__gate/start | gate page (en, hu-ascii): (True, True) | app html seen: False + cookies the stranger holds: {} + https://g-immich.enkisfelhom.hu/ : final 200 | 302 g-immich.enkisfelhom.hu/ -> 200 felhom.enkisfelhom.hu/__gate/start | gate page (en, hu-ascii): (True, True) | app html seen: False + cookies the stranger holds: {} +== B2b STRANGER, API style (what a script would do) + GET https://g-n8n.enkisfelhom.hu/rest/settings -> 401 '{"error":"this app is waiting for its first setup"}' + POST https://g-n8n.enkisfelhom.hu/rest/owner/setup -> 401 '{"error":"this app is waiting for its first setup"}' + GET https://g-immich.enkisfelhom.hu/api/server/config -> 401 '{"error":"this app is waiting for its first setup"}' + POST https://g-immich.enkisfelhom.hu/api/auth/admin-sign-up -> 401 '{"error":"this app is waiting for its first setup"}' +== B2c STRANGER tricks + open-redirect try rd=evil -> 400 'not a gated app' + forged token -> 403 'token refused' +== B2d HOUSEHOLD, logged in to the dashboard first (the usual path: open the app from the dashboard) + dashboard login -> 200 | 302 felhom.enkisfelhom.hu/login -> 302 felhom.enkisfelhom.hu/ -> 200 felhom.enkisfelhom.hu/launcher + https://g-n8n.enkisfelhom.hu/ -> final 200 in 0.22s | 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 + https://g-immich.enkisfelhom.hu/ -> final 200 in 0.21s | 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 + cookies per host: {'felhom.enkisfelhom.hu': ['felhom_session'], 'g-n8n.enkisfelhom.hu': ['felhom_gate'], 'g-immich.enkisfelhom.hu': ['felhom_gate']} + dashboard cookie on an app host? False +== B2e HOUSEHOLD on a phone, NOT logged in: opens the app link first + step 1 -> 200 gate page (True, True) | 302 g-n8n.enkisfelhom.hu/ -> 200 felhom.enkisfelhom.hu/__gate/start + step 2 sign-in link -> 200 | login form shown: True + step 3 sign in -> final 200 | 302 felhom.enkisfelhom.hu/login -> 302 felhom.enkisfelhom.hu/__gate/start -> 302 g-n8n.enkisfelhom.hu/__felhom_gate/cb -> 200 g-n8n.enkisfelhom.hu/ | gate page: False +== B2f token replay + cookie reuse on another app + household2 reached n8n: 200 302 felhom.enkisfelhom.hu/__gate/start -> 302 g-n8n.enkisfelhom.hu/__felhom_gate/cb -> 200 g-n8n.enkisfelhom.hu/ + n8n's gate cookie presented to immich -> 401 '{"error":"this app is waiting for its first setup"' diff --git a/documentation/audits/login-gate-2026-09-29/B/B3-probe-after.txt b/documentation/audits/login-gate-2026-09-29/B/B3-probe-after.txt new file mode 100644 index 00000000..a2967a08 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/B3-probe-after.txt @@ -0,0 +1,2 @@ +n8n /rest/settings userManagement.showSetupOnFirstLoad = False +immich /api/server/config isInitialized = True diff --git a/documentation/audits/login-gate-2026-09-29/B/B3-probe-before.txt b/documentation/audits/login-gate-2026-09-29/B/B3-probe-before.txt new file mode 100644 index 00000000..43ad5358 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/B3-probe-before.txt @@ -0,0 +1,2 @@ +n8n /rest/settings userManagement.showSetupOnFirstLoad = True +immich /api/server/config isInitialized = False diff --git a/documentation/audits/login-gate-2026-09-29/B/B3-setup.txt b/documentation/audits/login-gate-2026-09-29/B/B3-setup.txt new file mode 100644 index 00000000..a17a59aa --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/B3-setup.txt @@ -0,0 +1,2 @@ +n8n POST /rest/owner/setup (household, through the gate) -> 200 {"data":{"createdAt":"2026-09-29T06:12:10.719Z","id":"a0bb2e +immich POST /api/auth/admin-sign-up (household, through the gate) -> 201 {"id":"f8c0d515-c748-468e-89d7-ee40964f9379","email":"admin@ diff --git a/documentation/audits/login-gate-2026-09-29/B/B4-gated.txt b/documentation/audits/login-gate-2026-09-29/B/B4-gated.txt new file mode 100644 index 00000000..03922e73 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/B4-gated.txt @@ -0,0 +1,3 @@ +[gated] immich phone app: POST /api/auth/login -> 401 token=no body={"error":"this app is waiting for its first setup"} +[gated] n8n login without a browser gate cookie: POST /rest/login -> 401 +[gated] latency household GET /api/server/ping x40: last=200 median=52.2 ms p90=53.0 ms (new TLS connection each) diff --git a/documentation/audits/login-gate-2026-09-29/B/B4-open-after.txt b/documentation/audits/login-gate-2026-09-29/B/B4-open-after.txt new file mode 100644 index 00000000..aae6b9e1 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/B4-open-after.txt @@ -0,0 +1,6 @@ +[open] immich phone app: POST /api/auth/login -> 201 token=yes body= +[open] GET /api/users/me (Bearer) -> 200 '{"id":"f8c0d515-c748-468e-89d7-ee40964f9379","emai' +[open] GET /api/server/ping (Bearer) -> 200 '{"res":"pong"}' +[open] GET /api/server/version (Bearer) -> 200 '{"major":3,"minor":2,"patch":2,"prerelease":null}' +[open] n8n login without a browser gate cookie: POST /rest/login -> 200 +[open] latency household GET /api/server/ping x40: last=200 median=50.1 ms p90=50.9 ms (new TLS connection each) diff --git a/documentation/audits/login-gate-2026-09-29/B/B4-open.txt b/documentation/audits/login-gate-2026-09-29/B/B4-open.txt new file mode 100644 index 00000000..c94415b8 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/B4-open.txt @@ -0,0 +1,6 @@ +06:16:47.695 +controller.yml +serverstransports.yml +immich answers a cookie-less request after 2 x 0.1s: 06:16:47.859 +gate prototype STOPPED (nothing of it can be in the path now) +immich-server labels: no middleware label (unchanged by the gate) diff --git a/documentation/audits/login-gate-2026-09-29/B/B5-answerer-down.txt b/documentation/audits/login-gate-2026-09-29/B/B5-answerer-down.txt new file mode 100644 index 00000000..64921ddc --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/B5-answerer-down.txt @@ -0,0 +1,4 @@ +g-n8n gated, answerer down: browser -> 500 body=[] +g-immich gated, answerer down: browser -> 500 body=[] +dashboard itself (controller UP; only the answerer is down): 200 +after removing the gate again: immich -> 200 diff --git a/documentation/audits/login-gate-2026-09-29/B/b2.py b/documentation/audits/login-gate-2026-09-29/B/b2.py new file mode 100644 index 00000000..51d73655 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/b2.py @@ -0,0 +1,70 @@ +# Part B2/B3 — stranger vs household, both apps, through the gate. Passwords generated here, kept in memory only. +import json, secrets, sys, time +sys.path.insert(0, '.') +from browser import Browser, hopstr +D = "enkisfelhom.hu" +PW = open('../.ctlpw').read().strip() +N8N, IMM = f"https://g-n8n.{D}", f"https://g-immich.{D}" + +def gatepage(body): + return ("waiting for its first setup" in body, "Felhom vez" in body) + +print("== B2a STRANGER, browser, no dashboard session") +for base in (N8N, IMM): + s = Browser("stranger") + st, body, hops = s.req(base + "/") + print(f" {base}/ : final {st} | {hopstr(hops)} | gate page (en, hu-ascii): {gatepage(body)} | app html seen: {'n8n' in body.lower()[:3000] and 'waiting' not in body or 'Immich' in body}") + print(f" cookies the stranger holds: {s.hosts_with_cookies()}") +print("== B2b STRANGER, API style (what a script would do)") +s = Browser("stranger") +for url, meth, body in ((N8N + "/rest/settings", "GET", None), + (N8N + "/rest/owner/setup", "POST", {"email": "x@x.hu", "firstName": "x", "lastName": "x", "password": "Xx" + secrets.token_hex(8)}), + (IMM + "/api/server/config", "GET", None), + (IMM + "/api/auth/admin-sign-up", "POST", {"email": "x@x.hu", "password": secrets.token_hex(8), "name": "x"})): + st, b, hops = s.req(url, meth, body=body, accept="application/json") + print(f" {meth} {url} -> {st} {b[:70]!r}") +print("== B2c STRANGER tricks") +st, b, h = s.req(f"https://felhom.{D}/__gate/start?rd=https://evil.example/", follow=False) +print(f" open-redirect try rd=evil -> {st} {b[:40]!r}") +st, b, h = s.req(N8N + "/__felhom_gate/cb?t=%5B%22g-n8n.enkisfelhom.hu%22%2C%229999999999%22%2C%22n%22%2C%22https%3A//g-n8n.enkisfelhom.hu/%22%2C%22forged%22%5D", follow=False) +print(f" forged token -> {st} {b[:40]!r}") + +print("== B2d HOUSEHOLD, logged in to the dashboard first (the usual path: open the app from the dashboard)") +hh = Browser("household") +st, b, hops = hh.login_dashboard(D, PW) +print(f" dashboard login -> {st} | {hopstr(hops)}") +for base in (N8N, IMM): + t0 = time.time(); st, body, hops = hh.req(base + "/"); dt = time.time() - t0 + print(f" {base}/ -> final {st} in {dt:.2f}s | {hopstr(hops)} | gate page: {gatepage(body)[0]} | len {len(body)}") +print(f" cookies per host: {hh.hosts_with_cookies()}") +print(" dashboard cookie on an app host?", any('felhom_session' in v for h, v in hh.hosts_with_cookies().items() if not h.startswith('felhom.'))) + +print("== B2e HOUSEHOLD on a phone, NOT logged in: opens the app link first") +ph = Browser("phone") +st, body, hops = ph.req(N8N + "/") +print(f" step 1 -> {st} gate page {gatepage(body)} | {hopstr(hops)}") +import re, html as H +link = H.unescape(re.search(r'href="([^"]+)"', body).group(1)) +st, body, hops = ph.req(f"https://felhom.{D}{link}", follow=True) +print(f" step 2 sign-in link -> {st} | login form shown: {'password' in body.lower()}") +import urllib.parse +nxt = urllib.parse.parse_qs(urllib.parse.urlsplit(link).query)['next'][0] +st, body, hops = ph.req(f"https://felhom.{D}/login?next=" + urllib.parse.quote(nxt), "POST", + body="password=" + urllib.parse.quote(PW) + "&next=" + urllib.parse.quote(nxt), + headers={"Content-Type": "application/x-www-form-urlencoded"}) +print(f" step 3 sign in -> final {st} | {hopstr(hops)} | gate page: {gatepage(body)[0]}") + +print("== B2f token replay + cookie reuse on another app") +hh2 = Browser("household2"); hh2.login_dashboard(D, PW) +st, b, hops = hh2.req(f"https://felhom.{D}/__gate/start?rd=" + urllib.parse.quote(N8N + "/"), follow=False) +cb = dict((k.lower(), v) for k, v in []) # placeholder +loc_hops = hops +import http.client +# follow once by hand to capture the callback URL +st2, b2, h2 = hh2.req(f"https://felhom.{D}/__gate/start?rd=" + urllib.parse.quote(N8N + "/"), follow=True) +print(f" household2 reached n8n: {st2} {hopstr(h2)}") +gc = hh2.jar.get(f"g-n8n.{D}", {}).get("felhom_gate") +x = Browser("thief"); x.jar[f"g-immich.{D}"] = {"felhom_gate": gc} +st, b, hops = x.req(IMM + "/api/server/config", accept="application/json") +print(f" n8n's gate cookie presented to immich -> {st} {b[:50]!r}") +json.dump({"hh": hh.jar, "ph": ph.jar}, open('jars.json', 'w')) diff --git a/documentation/audits/login-gate-2026-09-29/B/b3.py b/documentation/audits/login-gate-2026-09-29/B/b3.py new file mode 100644 index 00000000..7e519ff3 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/b3.py @@ -0,0 +1,16 @@ +# Part B3 — the household completes each app's first setup THROUGH the gate (browser session = gate cookie). +import json, secrets, string, sys +sys.path.insert(0, '.') +from browser import Browser, hopstr +D = "enkisfelhom.hu" +hh = Browser("household"); hh.jar = json.load(open('jars.json'))["hh"] +def pw(): + return "Hh" + secrets.token_hex(10) + "7" +creds = {"n8n": ("owner@spike.hu", pw()), "immich": ("admin@spike.hu", pw())} +json.dump(creds, open('creds.json', 'w')) # scratch only, 0600, deleted at teardown +st, b, h = hh.req(f"https://g-n8n.{D}/rest/owner/setup", "POST", accept="application/json", + body={"email": creds["n8n"][0], "firstName": "Spike", "lastName": "Owner", "password": creds["n8n"][1]}) +print("n8n POST /rest/owner/setup (household, through the gate) ->", st, b[:60].replace(creds['n8n'][1], '<pw>')) +st, b, h = 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"}) +print("immich POST /api/auth/admin-sign-up (household, through the gate) ->", st, b[:60]) diff --git a/documentation/audits/login-gate-2026-09-29/B/b4.py b/documentation/audits/login-gate-2026-09-29/B/b4.py new file mode 100644 index 00000000..e0a17592 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/b4.py @@ -0,0 +1,25 @@ +# Part B4/B5 — immich's phone app (no browser cookies; Bearer token) and latency. Run with arg "gated" or "open". +import json, statistics, sys, time +sys.path.insert(0, '.') +from browser import Browser +D = "enkisfelhom.hu"; phase = sys.argv[1] +creds = json.load(open('creds.json')) +app = Browser("immich-mobile") # the phone app: no cookie jar from any browser +st, b, h = app.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 +print(f"[{phase}] immich phone app: POST /api/auth/login -> {st} token={'yes' if tok else 'no'} body={b[:60] if not tok else '<token>'}") +if tok: + for p in ("/api/users/me", "/api/server/ping", "/api/server/version"): + st, b, h = app.req(f"https://g-immich.{D}{p}", accept="application/json", headers={"Authorization": "Bearer " + tok}) + print(f"[{phase}] GET {p} (Bearer) -> {st} {b[:50].replace(creds['immich'][0],'<email>')!r}") +n8 = Browser("n8n-api") +st, b, h = n8.req(f"https://g-n8n.{D}/rest/login", "POST", accept="application/json", + body={"emailOrLdapLoginId": creds["n8n"][0], "password": creds["n8n"][1]}) +print(f"[{phase}] n8n login without a browser gate cookie: POST /rest/login -> {st}") +# latency: the household browser (gate cookie when gated), 40 x GET /api/server/ping +hh = Browser("household"); hh.jar = json.load(open('jars.json'))["hh"] +ts = [] +for _ in range(40): + t0 = time.perf_counter(); st, b, h = hh.req(f"https://g-immich.{D}/api/server/ping", accept="application/json"); ts.append((time.perf_counter() - t0) * 1000) +print(f"[{phase}] latency household GET /api/server/ping x40: last={st} median={statistics.median(ts):.1f} ms p90={sorted(ts)[35]:.1f} ms (new TLS connection each)") diff --git a/documentation/audits/login-gate-2026-09-29/B/browser.py b/documentation/audits/login-gate-2026-09-29/B/browser.py new file mode 100644 index 00000000..1d0bd456 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/browser.py @@ -0,0 +1,74 @@ +# SPIKE evidence tool (2026-09-29): a minimal "browser" — per-host cookie jar (host-only cookies, as a browser keeps +# them), follows redirects across hosts, every host resolved to guest 9202's traefik. Never prints a password or a cookie. +import http.client, json, re, ssl, urllib.parse + +IP = "192.168.0.114" +CTX = ssl._create_unverified_context() # scratch guest 9202 serves traefik's self-signed default cert on the LAN + + +class Browser: + def __init__(self, name): + self.name = name + self.jar = {} # host -> {cookie: value} + + def req(self, url, method="GET", body=None, headers=None, follow=True, accept="text/html"): + hops = [] + for _ in range(12): + u = urllib.parse.urlsplit(url) + host = u.hostname + path = u.path + ("?" + u.query if u.query else "") + h = {"Host": host, "Accept": accept, "User-Agent": "felhom-spike-browser"} + ck = self.jar.get(host, {}) + if ck: + h["Cookie"] = "; ".join(f"{k}={v}" for k, v in ck.items()) + h.update(headers or {}) + data = body + if isinstance(body, dict): + data = json.dumps(body).encode() + h["Content-Type"] = "application/json" + elif isinstance(body, str): + data = body.encode() + conn = http.client.HTTPSConnection(IP, 443, context=CTX, timeout=30) + conn.connect = _sni_connect(conn, host) + conn.request(method, path, body=data, headers=h) + r = conn.getresponse() + rb = r.read() + for k, v in r.getheaders(): + if k.lower() == "set-cookie": + nv = v.split(";", 1)[0] + n, _, val = nv.partition("=") + if "max-age=-1" in v.lower() or val == "": + self.jar.setdefault(host, {}).pop(n, None) + else: + self.jar.setdefault(host, {})[n] = val + hops.append((r.status, host, u.path)) + loc = r.getheader("Location") + if follow and r.status in (301, 302, 303, 307, 308) and loc: + url = urllib.parse.urljoin(url, loc) + if r.status in (301, 302, 303): + method, body = "GET", None + continue + return r.status, rb.decode("utf-8", "replace"), hops + return 0, "", hops + + def login_dashboard(self, domain, password): + # The dashboard's own login form (POST /login), exactly as walk.login() does it. + return self.req(f"https://felhom.{domain}/login", "POST", + body="password=" + urllib.parse.quote(password), + headers={"Content-Type": "application/x-www-form-urlencoded"}) + + def hosts_with_cookies(self): + return {h: sorted(v) for h, v in self.jar.items()} + + +def _sni_connect(conn, host): + import socket + + def connect(): + sock = socket.create_connection((IP, 443), timeout=30) + conn.sock = CTX.wrap_socket(sock, server_hostname=host) + return connect + + +def hopstr(hops): + return " -> ".join(f"{s} {h}{p}" for s, h, p in hops) diff --git a/documentation/audits/login-gate-2026-09-29/B/down.sh b/documentation/audits/login-gate-2026-09-29/B/down.sh new file mode 100644 index 00000000..d591ef7e --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/down.sh @@ -0,0 +1,6 @@ +# B5: the gate's answerer is DOWN while an app is gated (models "the controller is down"). +cp /root/gate-spike/gate-spike.yml.removed /opt/docker/stacks/traefik/dynamic/gate-spike.yml; sleep 3 +for h in g-n8n g-immich; do echo "$h gated, answerer down: browser -> $(curl -sk -o /tmp/dn.$$ -w '%{http_code}' -H "Host: $h.enkisfelhom.hu" -H 'Accept: text/html' https://127.0.0.1/) body=[$(head -c 60 /tmp/dn.$$)]"; done +echo "dashboard itself (controller UP; only the answerer is down): $(curl -sk -o /dev/null -w '%{http_code}' -H 'Host: felhom.enkisfelhom.hu' https://127.0.0.1/login)" +rm -f /opt/docker/stacks/traefik/dynamic/gate-spike.yml /tmp/dn.$$; sleep 3 +echo "after removing the gate again: immich -> $(curl -sk -o /dev/null -w '%{http_code}' -H 'Host: g-immich.enkisfelhom.hu' https://127.0.0.1/api/server/ping)" diff --git a/documentation/audits/login-gate-2026-09-29/B/gate-spike.yml b/documentation/audits/login-gate-2026-09-29/B/gate-spike.yml new file mode 100644 index 00000000..3ca98bad --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/gate-spike.yml @@ -0,0 +1,32 @@ +# SPIKE ONLY (2026-09-29, decision 46) — scratch guest 9202. Removed at the end of the spike. +http: + middlewares: + felhom-gate-spike: + forwardAuth: + address: "http://felhom-gate-spike:8000/auth" + routers: + gate-spike-start: + rule: "Host(`felhom.enkisfelhom.hu`) && PathPrefix(`/__gate/`)" + priority: 100000 + entryPoints: [websecure] + tls: {} + service: gate-spike + gate-spike-n8n: + rule: "Host(`g-n8n.enkisfelhom.hu`)" + priority: 100000 + entryPoints: [websecure] + tls: {} + middlewares: [felhom-gate-spike] + service: n8n@docker + gate-spike-immich: + rule: "Host(`g-immich.enkisfelhom.hu`)" + priority: 100000 + entryPoints: [websecure] + tls: {} + middlewares: [felhom-gate-spike] + service: immich@docker + services: + gate-spike: + loadBalancer: + servers: + - url: "http://felhom-gate-spike:8000" diff --git a/documentation/audits/login-gate-2026-09-29/B/gate.py b/documentation/audits/login-gate-2026-09-29/B/gate.py new file mode 100644 index 00000000..8096b14c --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/gate.py @@ -0,0 +1,141 @@ +# SPIKE ONLY (2026-09-29, decision 46) — a throwaway prototype of the setup gate, run on scratch guest 9202. +# It is NOT product code. In a build, these handlers would live in the controller itself. +# +# GET /auth traefik forwardAuth target for a GATED app host. +# GET /__gate/start on the DASHBOARD host (felhom.<domain>): checks the household's dashboard session +# (host-only cookie, never widened) by asking the controller, then hands the app host a +# 60-second one-use token. +# <app>/__felhom_gate/cb?t=… answered by /auth: swaps the token for a host-only gate cookie on the app host. +# +# The dashboard cookie never reaches an app host. The gate cookie is host-only, HMAC-bound to ONE app host. +import hashlib, hmac, html, http.client, json, os, secrets, sys, time, urllib.parse +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +DOMAIN = os.environ["GATE_DOMAIN"] +GATED = set(h.strip() for h in os.environ["GATE_HOSTS"].split(",") if h.strip()) # full host names +KEY = secrets.token_bytes(32) +USED = {} +COOKIE = "felhom_gate" +DASH = "felhom." + DOMAIN +COUNT = {"auth": 0} + + +def log(*a): + print(time.strftime("%H:%M:%S"), *a, flush=True) + + +def sign(*parts): + return hmac.new(KEY, "|".join(parts).encode(), hashlib.sha256).hexdigest() + + +def dashboard_session_ok(cookie_header): + """Ask the controller whether this dashboard session is valid (401 without one, 200 with one).""" + sess = "" + for c in (cookie_header or "").split(";"): + k, _, v = c.strip().partition("=") + if k == "felhom_session": + sess = v + if not sess: + return False + conn = http.client.HTTPConnection("felhom-controller", 8080, timeout=5) + conn.request("GET", "/api/stacks", headers={"Host": DASH, "Cookie": "felhom_session=" + sess}) + ok = conn.getresponse().status == 200 + conn.close() + return ok + + +PAGE = """<!doctype html><html lang="hu"><meta charset="utf-8"><title>Felhom + +

Ez az alkalmazás még beállításra vár. Jelentkezz be a Felhom vezérlőpultba.

+

This app is waiting for its first setup. Sign in to the Felhom dashboard.

+

Bejelentkezés / Sign in

""" + + +class H(BaseHTTPRequestHandler): + def log_message(self, *a): + pass + + def send(self, code, body=b"", headers=()): + self.send_response(code) + for k, v in headers: + self.send_header(k, v) + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + u = urllib.parse.urlsplit(self.path) + if u.path == "/auth": + return self.auth() + if u.path == "/__gate/start": + return self.start(urllib.parse.parse_qs(u.query)) + self.send(404) + + # forwardAuth may call with the original method; the body is never needed. + def _any(self): + n = int(self.headers.get("Content-Length") or 0) + if n: + self.rfile.read(n) + self.do_GET() + do_POST = do_PUT = do_PATCH = do_DELETE = do_HEAD = do_OPTIONS = _any + + # forwardAuth: traefik forwards every method as a GET here, with X-Forwarded-* headers. + def auth(self): + COUNT["auth"] += 1 + host = self.headers.get("X-Forwarded-Host", "") + uri = self.headers.get("X-Forwarded-Uri", "/") + method = self.headers.get("X-Forwarded-Method", "GET") + if host not in GATED: + log("auth", host, "not gated -> 403 (fail closed)") + return self.send(403) + u = urllib.parse.urlsplit(uri) + if u.path == "/__felhom_gate/cb": + t = urllib.parse.parse_qs(u.query).get("t", [""])[0] + try: + h, exp, nonce, rd, mac = json.loads(urllib.parse.unquote(t)) + except Exception: + return self.send(400, b"bad token") + if (h != host or int(exp) < time.time() or nonce in USED + or not hmac.compare_digest(mac, sign("tok", h, str(exp), nonce, rd))): + log("cb", host, "token refused") + return self.send(403, b"token refused") + USED[nonce] = 1 + cexp = str(int(time.time()) + 7 * 86400) + val = cexp + "." + sign("cookie", host, cexp) + log("cb", host, "token ok -> gate cookie, back to", rd) + return self.send(302, headers=[("Location", rd), ("Set-Cookie", + f"{COOKIE}={val}; Path=/; Max-Age=604800; HttpOnly; Secure; SameSite=Lax")]) + for c in (self.headers.get("Cookie") or "").split(";"): + k, _, v = c.strip().partition("=") + if k == COOKIE and "." in v: + cexp, mac = v.split(".", 1) + if cexp.isdigit() and int(cexp) > time.time() and hmac.compare_digest(mac, sign("cookie", host, cexp)): + return self.send(200) + wants_html = "text/html" in (self.headers.get("Accept") or "") and method == "GET" + log("auth", host, method, uri[:60], "no gate cookie ->", "302 to dashboard" if wants_html else "401") + if wants_html: + rd = f"https://{host}{uri}" + return self.send(302, headers=[("Location", f"https://{DASH}/__gate/start?" + + urllib.parse.urlencode({"rd": rd}))]) + return self.send(401, b'{"error":"this app is waiting for its first setup"}', + [("Content-Type", "application/json")]) + + def start(self, q): + rd = (q.get("rd") or [""])[0] + host = urllib.parse.urlsplit(rd).hostname or "" + if host not in GATED or not rd.startswith("https://" + host + "/"): + return self.send(400, b"not a gated app") + if not dashboard_session_ok(self.headers.get("Cookie")): + login = "/login?" + urllib.parse.urlencode({"next": "/__gate/start?" + urllib.parse.urlencode({"rd": rd})}) + log("start", host, "no dashboard session -> gate page") + return self.send(200, PAGE.format(login=html.escape(login)).encode(), + [("Content-Type", "text/html; charset=utf-8")]) + exp, nonce = str(int(time.time()) + 60), secrets.token_hex(8) + t = urllib.parse.quote(json.dumps([host, exp, nonce, rd, sign("tok", host, exp, nonce, rd)])) + log("start", host, "dashboard session ok -> token") + return self.send(302, headers=[("Location", f"https://{host}/__felhom_gate/cb?t={t}")]) + + +if __name__ == "__main__": + log("gate spike up; domain", DOMAIN, "gated", sorted(GATED)) + ThreadingHTTPServer(("0.0.0.0", 8000), H).serve_forever() diff --git a/documentation/audits/login-gate-2026-09-29/B/open.sh b/documentation/audits/login-gate-2026-09-29/B/open.sh new file mode 100644 index 00000000..4d471268 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/open.sh @@ -0,0 +1,6 @@ +# Open the gate for both apps: the gate's routers go; the app's own docker-label routers remain, untouched. +date -u +%T.%N | cut -c1-12; mv /opt/docker/stacks/traefik/dynamic/gate-spike.yml /root/gate-spike/gate-spike.yml.removed +ls /opt/docker/stacks/traefik/dynamic +for i in $(seq 1 50); do c=$(curl -sk -o /dev/null -w '%{http_code}' -H 'Host: g-immich.enkisfelhom.hu' -H 'Accept: application/json' https://127.0.0.1/api/server/ping); [ "$c" = 200 ] && { echo "immich answers a cookie-less request after $i x 0.1s: $(date -u +%T.%N | cut -c1-12)"; break; }; sleep 0.1; done +docker stop felhom-gate-spike >/dev/null && echo "gate prototype STOPPED (nothing of it can be in the path now)" +docker inspect -f '{{range $k,$v := .Config.Labels}}{{if eq (slice $k 0 7) "traefik"}}{{$k}}={{$v}}{{"\n"}}{{end}}{{end}}' immich-server | grep -i middle || echo "immich-server labels: no middleware label (unchanged by the gate)" diff --git a/documentation/audits/login-gate-2026-09-29/B/probe.sh b/documentation/audits/login-gate-2026-09-29/B/probe.sh new file mode 100644 index 00000000..ac77182f --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/probe.sh @@ -0,0 +1,11 @@ +# Read-only "setup done?" probes, straight from the docker network (not through traefik, not through the gate). +docker exec felhom-gate-spike python -c ' +import json, urllib.request as u +def g(url): + try: return json.load(u.urlopen(url, timeout=10)) + except Exception as e: return {"_err": str(e)[:80]} +s = g("http://n8n:5678/rest/settings") +print("n8n /rest/settings userManagement.showSetupOnFirstLoad =", (s.get("data") or {}).get("userManagement", {}).get("showSetupOnFirstLoad"), s.get("_err","")) +c = g("http://immich-server:2283/api/server/config") +print("immich /api/server/config isInitialized =", c.get("isInitialized"), c.get("_err","")) +' diff --git a/documentation/audits/login-gate-2026-09-29/B/stranger.sh b/documentation/audits/login-gate-2026-09-29/B/stranger.sh new file mode 100644 index 00000000..b8199433 --- /dev/null +++ b/documentation/audits/login-gate-2026-09-29/B/stranger.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; do + p=/rest/settings; [ $h = g-immich ] && p=/api/server/config + 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/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index 5a1eabc7..b235ad59 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -822,6 +822,7 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server` | **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-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** |