https only (R-1033, P1): controller v0.312.0 proven on Tester 1, demo-hp, demo-felhom (29 names); scripts/https_only_check.py; 01/13/poster facts; decision 246 (decided by CC unattended); R-1033 P1 narrowed; R-1041..R-1043
gates / gates (push) Successful in 6m34s

This commit is contained in:
2026-10-14 22:49:54 +02:00
parent 0144be8cdd
commit b7807c1198
17 changed files with 297 additions and 4 deletions
@@ -139,6 +139,19 @@ read the LEFTMOST entry have the chain removed on their router. Design and measu
MeTube) was SPIKED on this base and passed (`audits/permanent-gate-2026-10-01/VERDICT.md`) and is BUILT since
controller 0.287.0 (`09` §3 decision 64) — the family gate, at the end of this section.
**Every public name answers only over https (recorded 2026-10-14, R-1033 — P1 by the operator, `09` §3 decision 244;
controller ≥ 0.312.0).** A visitor who types `http://` reaches Cloudflare over plain http; the tunnel still delivers to
traefik's :443 and says so in `X-Forwarded-Proto: http` (measured), which traefik keeps only from the tunnel's address.
The first link of the entrypoint chain `felhom-forwarded` (`felhom-https-only`) answers it with a 301 to the same https
URL, path and query kept, BEFORE any app sees the request — so a login page is never served, and a password never sent,
over http. A name with no app route is redirected by a priority-1 catch-all that matches only `X-Forwarded-Proto: http`.
HSTS: one day, no subdomains, no preload (a longer value is the operator's decision). **Where it is enforced:** on the box
(`dynamic/forwarded.yml`, rewritten on every health tick if it drifts), not at Cloudflare: the boxes' Cloudflare keys
cannot change zone settings (403, measured on every zone) — Cloudflare's „Always Use HTTPS" is the operator's optional
second layer. **How it is checked:** `scripts/https_only_check.py` lists every public name from the box's own traefik
routes and demands 301/308 to the same https URL (negative control: a stub answering plain 200 must be convicted).
Evidence: `audits/https-only-2026-10-14/`.
**Who may reach an app, and through what (recorded 2026-09-29 — no document said it before; spike finding F1).**
Every app is reached only through the box's traefik (no catalog app publishes a host port except crafty-controller's
game ports; none uses host networking — read from the catalog 2026-09-29). traefik routes by host name: the tunnel's
@@ -922,6 +922,15 @@ its length, and both fixes cost something the household would notice — operato
245. **Drive events = option A.** On a drive added, removed or back, the box restarts the media apps; it makes a new
drive ready inside each media app; it pauses torrents before a safe removal; and it adds guards only where a
measurement shows loss. *Operator ruling 2026-10-14 22:13.* (R-1023; `15-media-library.md` §10)
246. **Where https-only is enforced: on the box (traefik), not at Cloudflare.** *Decided by CC unattended 2026-10-14
22:40 — operator may reverse.* Question: which switch makes every public name answer only over https tonight?
Options: (a) Cloudflare „Always Use HTTPS" + HSTS per zone — one switch per zone, nothing on the box can forget it,
but every box's `cf_api_token` lacks „Zone Settings" (403 on all three zones, measured) and no account key is
stored: costs a key change per customer first; (b) traefik on the box redirects when the trusted tunnel says
`X-Forwarded-Proto: http` — needs a controller release (v0.312.0), reaches a box with the floor, and is rewritten
on drift every health tick; (c) a Cloudflare firewall rule (the keys CAN edit those) — it can only BLOCK, which
breaks Sonarr's login (its own `http://` redirect). Chosen (b): it works with the keys we have, was measured
(bench + three zones), and leaves (a) open as a second layer once the operator widens the keys (R-1042).
### 2026-10-14 (evening, ~21:20) — the media folders and the N100 (recorded before the work; the media-folder-fix brief)
@@ -58,6 +58,7 @@ OS packages (exact versions, apt-signed), the installer script (by tag, R-980).
| boundary | verdict | evidence |
|---|---|---|
| end-user ↔ apps | holds, with gaps | traefik routing and header stripping hold; homepage has the Docker socket and no gate (R-981) |
| **added 2026-10-14 (R-1033, P1):** a visitor ↔ Cloudflare over plain http | holds on boxes with controller ≥ 0.312.0 | before: every public name answered `http://` (login pages too), and Sonarr/Radarr/Prowlarr redirected https visitors to an absolute `http://` login; a password could cross an untrusted network unencrypted (attacker: anyone on the visitor's network). Now traefik sends every `http://` request to https (301) before an app sees it, HSTS one day; boxes below 0.312.0 (Tester 2 at 0.292.0) keep the gap until the floor delivers it. `audits/https-only-2026-10-14/` |
| customer ↔ controller UI | partly | bcrypt and session hold; small gaps (R-983); family sessions survive a reset (R-976) |
| controller ↔ agent | partly | every route bound to the caller's guest; a compromised controller can still flush restore points (R-973), void the recovery code (R-978), and — since agent v0.155.0 — switch the whole machine off once per power-on, holding the hub's silence alarms for up to 14 days (R-998; mitigations and residual risk: `14-host-power-off.md` §5) |
| **added (R-1007):** controller → an app's own job queue | holds — nothing new | the `after_unclean_boot` commands run inside the app's container through the docker socket the controller already holds, and read the app's database with a read-only session: no key into the household's account, nothing a compromised controller could not already do (exec in any app, read its database). The household's lost-file names stay on the box (`app.yaml`, the app page); the operator event carries counts only, and a failing command's output is never logged (security review of controller `8d862e4`, fixed in `3b85a0c`) |
@@ -69,7 +69,7 @@ Inside, from bottom to top:
Note on the drawing: the data drives are attached to the guest from the host; the **whole-guest backups do not include the data drives** (that is what the per-app tiers are for).
Arrows out of the box:
- **Household's phone / laptop → Cloudflare → cloudflared → Traefik → app** (solid, green). Label: "Remote access. HTTPS ends at Cloudflare's edge, so Cloudflare could technically see this traffic (later item: our own relay)." Also a short local arrow: "At home on the LAN: direct to Traefik."
- **Household's phone / laptop → Cloudflare → cloudflared → Traefik → app** (solid, green). Label: "Remote access. HTTPS ends at Cloudflare's edge, so Cloudflare could technically see this traffic (later item: our own relay)." **Since 2026-10-14 (R-1033): https only** — a visitor who types `http://` is sent to `https://` by Traefik before any app answers (HSTS one day). Also a short local arrow: "At home on the LAN: direct to Traefik."
- **Box → hub** (dotted): "Reports every few minutes; asks for signed jobs. The hub never connects into a box."
- **Box → ep0** (dashed, through a small padlock tunnel labelled **WireGuard**): "Whole-guest backup, encrypted on the box, sent through WireGuard." — *fix (b), 2026-10-09: the label must name BOTH, because "encrypted on the box" alone read as if WireGuard were doing the encrypting.*
- **Box → Hetzner Storage Box** (dashed): "Per-app off-site backup (restic), encrypted on the box. The box's key can only ADD copies; old copies are removed only in a weekly clean-up window the hub opens."
@@ -0,0 +1,11 @@
# AFTER 0.312.0 on demo-felhom 9201 — 2026-10-14T22:45:01+02:00
https felhom.demo-felhom.eu/?probe=1 HTTP/2 302 location: /login strict-transport-security: max-age=86400
http felhom.demo-felhom.eu/?probe=1 HTTP/1.1 301 Moved Permanently location: https://felhom.demo-felhom.eu/?probe=1
https felhom.demo-felhom.eu/login?probe=1 HTTP/2 200 strict-transport-security: max-age=86400
http felhom.demo-felhom.eu/login?probe=1 HTTP/1.1 301 Moved Permanently location: https://felhom.demo-felhom.eu/login?probe=1
https files.demo-felhom.eu/?probe=1 HTTP/2 200 strict-transport-security: max-age=86400
http files.demo-felhom.eu/?probe=1 HTTP/1.1 301 Moved Permanently location: https://files.demo-felhom.eu/?probe=1
https gist.demo-felhom.eu/?probe=1 HTTP/2 302 location: https://gist.demo-felhom.eu/-/all strict-transport-security: max-age=86400
http gist.demo-felhom.eu/?probe=1 HTTP/1.1 301 Moved Permanently location: https://gist.demo-felhom.eu/?probe=1
https no-such-name-r1033.demo-felhom.eu/?probe=1 HTTP/2 404
http no-such-name-r1033.demo-felhom.eu/?probe=1 HTTP/1.1 301 Moved Permanently location: https://no-such-name-r1033.demo-felhom.eu/?probe=1
@@ -0,0 +1,11 @@
# BEFORE 0.312.0 on demo-felhom 9201 — 2026-10-14T22:44:10+02:00
https felhom.demo-felhom.eu/?probe=1 HTTP/2 302 location: /login
http felhom.demo-felhom.eu/?probe=1 HTTP/1.1 302 Found location: /login
https felhom.demo-felhom.eu/login?probe=1 HTTP/2 200
http felhom.demo-felhom.eu/login?probe=1 HTTP/1.1 200 OK
https files.demo-felhom.eu/?probe=1 HTTP/2 200
http files.demo-felhom.eu/?probe=1 HTTP/1.1 200 OK
https gist.demo-felhom.eu/?probe=1 HTTP/2 302 location: https://gist.demo-felhom.eu/-/all
http gist.demo-felhom.eu/?probe=1 HTTP/1.1 302 Found location: https://gist.demo-felhom.eu/-/all
https no-such-name-r1033.demo-felhom.eu/?probe=1 HTTP/2 404
http no-such-name-r1033.demo-felhom.eu/?probe=1 HTTP/1.1 404 Not Found
@@ -0,0 +1,5 @@
gitea.dooplex.hu/admin/felhom-controller:0.311.0
gitea.dooplex.hu/admin/felhom-controller:0.312.0
gitea.dooplex.hu/admin/felhom-controller:0.312.0 Up 45 seconds (healthy)
2026/10/14 20:44:10 [INFO] [infra] wrote the forwarded-header clean-up → /opt/docker/stacks/traefik/dynamic/forwarded.yml (felhom-forwarded)
0
@@ -0,0 +1,15 @@
# AFTER 0.312.0 on demo-hp 9201 — 2026-10-14T22:43:47+02:00
https felhom.enkisfelhom.hu/?probe=1 HTTP/2 302 location: /login strict-transport-security: max-age=86400
http felhom.enkisfelhom.hu/?probe=1 HTTP/1.1 301 Moved Permanently location: https://felhom.enkisfelhom.hu/?probe=1
https felhom.enkisfelhom.hu/login?probe=1 HTTP/2 200 strict-transport-security: max-age=86400
http felhom.enkisfelhom.hu/login?probe=1 HTTP/1.1 301 Moved Permanently location: https://felhom.enkisfelhom.hu/login?probe=1
https bookstack.enkisfelhom.hu/?probe=1 HTTP/2 404
http bookstack.enkisfelhom.hu/?probe=1 HTTP/1.1 301 Moved Permanently location: https://bookstack.enkisfelhom.hu/?probe=1
https paperless.enkisfelhom.hu/?probe=1 HTTP/2 404
http paperless.enkisfelhom.hu/?probe=1 HTTP/1.1 301 Moved Permanently location: https://paperless.enkisfelhom.hu/?probe=1
https no-such-name-r1033.enkisfelhom.hu/?probe=1 HTTP/2 404
http no-such-name-r1033.enkisfelhom.hu/?probe=1 HTTP/1.1 301 Moved Permanently location: https://no-such-name-r1033.enkisfelhom.hu/?probe=1
https papir.enkisfelhom.hu HTTP/2 302 location: /accounts/login/?next=/%3Fprobe%3D1 strict-transport-security: max-age=86400
http papir.enkisfelhom.hu HTTP/1.1 301 Moved Permanently location: https://papir.enkisfelhom.hu/?probe=1
https books.enkisfelhom.hu HTTP/2 302 location: /login?next=%2F%3Fprobe%3D1 strict-transport-security: max-age=86400
http books.enkisfelhom.hu HTTP/1.1 301 Moved Permanently location: https://books.enkisfelhom.hu/?probe=1
@@ -0,0 +1,11 @@
# BEFORE 0.312.0 on demo-hp 9201 — 2026-10-14T22:42:57+02:00
https felhom.enkisfelhom.hu/?probe=1 HTTP/2 302 location: /login
http felhom.enkisfelhom.hu/?probe=1 HTTP/1.1 302 Found location: /login
https felhom.enkisfelhom.hu/login?probe=1 HTTP/2 200
http felhom.enkisfelhom.hu/login?probe=1 HTTP/1.1 200 OK
https bookstack.enkisfelhom.hu/?probe=1 HTTP/2 404
http bookstack.enkisfelhom.hu/?probe=1 HTTP/1.1 404 Not Found
https paperless.enkisfelhom.hu/?probe=1 HTTP/2 404
http paperless.enkisfelhom.hu/?probe=1 HTTP/1.1 404 Not Found
https no-such-name-r1033.enkisfelhom.hu/?probe=1 HTTP/2 404
http no-such-name-r1033.enkisfelhom.hu/?probe=1 HTTP/1.1 404 Not Found
@@ -0,0 +1,5 @@
gitea.dooplex.hu/admin/felhom-controller:0.311.0
gitea.dooplex.hu/admin/felhom-controller:0.312.0
gitea.dooplex.hu/admin/felhom-controller:0.312.0 Up 45 seconds (healthy)
2026/10/14 20:43:02 infra.go:234: [INFO] [infra] wrote the forwarded-header clean-up → /opt/docker/stacks/traefik/dynamic/forwarded.yml (felhom-forwarded)
0
@@ -0,0 +1,6 @@
# Sonarr sign-in path after 0.312.0 — each hop (curl -L, GET only) 2026-10-14T22:42:33+02:00
HTTP/2 302
location: http://sonarr.enkicsifelhom.hu/login?returnUrl=%2F
HTTP/2 200
-- the login form's action (where the password would be POSTed):
(a relative action posts to the page's own origin — https after the redirect; with HSTS the browser upgrades the http hop itself, without sending it)
@@ -0,0 +1,13 @@
# AFTER 0.312.0 on Tester 1 — 2026-10-14T22:42:21+02:00
https felhom.enkicsifelhom.hu/?probe=1 HTTP/2 302 location: /login strict-transport-security: max-age=86400
http felhom.enkicsifelhom.hu/?probe=1 HTTP/1.1 301 Moved Permanently location: https://felhom.enkicsifelhom.hu/?probe=1
https felhom.enkicsifelhom.hu/login?probe=1 HTTP/2 200 strict-transport-security: max-age=86400
http felhom.enkicsifelhom.hu/login?probe=1 HTTP/1.1 301 Moved Permanently location: https://felhom.enkicsifelhom.hu/login?probe=1
https sonarr.enkicsifelhom.hu/?probe=1 HTTP/2 302 location: http://sonarr.enkicsifelhom.hu/login?returnUrl=%2F%3Fprobe%3D1 strict-transport-security: max-age=86400
http sonarr.enkicsifelhom.hu/?probe=1 HTTP/1.1 301 Moved Permanently location: https://sonarr.enkicsifelhom.hu/?probe=1
https torrent.enkicsifelhom.hu/?probe=1 HTTP/2 200 strict-transport-security: max-age=86400
http torrent.enkicsifelhom.hu/?probe=1 HTTP/1.1 301 Moved Permanently location: https://torrent.enkicsifelhom.hu/?probe=1
https filebrowser.enkicsifelhom.hu/?probe=1 HTTP/2 404
http filebrowser.enkicsifelhom.hu/?probe=1 HTTP/1.1 301 Moved Permanently location: https://filebrowser.enkicsifelhom.hu/?probe=1
https no-such-name-r1033.enkicsifelhom.hu/?probe=1 HTTP/2 404
http no-such-name-r1033.enkicsifelhom.hu/?probe=1 HTTP/1.1 301 Moved Permanently location: https://no-such-name-r1033.enkicsifelhom.hu/?probe=1
@@ -0,0 +1,13 @@
# BEFORE 0.312.0 on Tester 1 — 2026-10-14T22:41:13+02:00
https felhom.enkicsifelhom.hu/?probe=1 HTTP/2 302 location: /login
http felhom.enkicsifelhom.hu/?probe=1 HTTP/1.1 302 Found location: /login
https felhom.enkicsifelhom.hu/login?probe=1 HTTP/2 200
http felhom.enkicsifelhom.hu/login?probe=1 HTTP/1.1 200 OK
https sonarr.enkicsifelhom.hu/?probe=1 HTTP/2 302 location: http://sonarr.enkicsifelhom.hu/login?returnUrl=%2F%3Fprobe%3D1
http sonarr.enkicsifelhom.hu/?probe=1 HTTP/1.1 302 Found location: http://sonarr.enkicsifelhom.hu/login?returnUrl=%2F%3Fprobe%3D1
https torrent.enkicsifelhom.hu/?probe=1 HTTP/2 200
http torrent.enkicsifelhom.hu/?probe=1 HTTP/1.1 200 OK
https filebrowser.enkicsifelhom.hu/?probe=1 HTTP/2 404
http filebrowser.enkicsifelhom.hu/?probe=1 HTTP/1.1 404 Not Found
https no-such-name-r1033.enkicsifelhom.hu/?probe=1 HTTP/2 404
http no-such-name-r1033.enkicsifelhom.hu/?probe=1 HTTP/1.1 404 Not Found
@@ -0,0 +1,38 @@
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LC_CTYPE = "UTF-8",
LC_NUMERIC = (unset),
LC_COLLATE = (unset),
LC_TIME = (unset),
LC_MESSAGES = (unset),
LC_MONETARY = (unset),
LC_ADDRESS = (unset),
LC_IDENTIFICATION = (unset),
LC_MEASUREMENT = (unset),
LC_PAPER = (unset),
LC_TELEPHONE = (unset),
LC_NAME = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to a fallback locale ("en_US.UTF-8").
gitea.dooplex.hu/admin/felhom-controller:0.312.0
gitea.dooplex.hu/admin/felhom-controller:0.312.0 Up 45 seconds (healthy)
2026/10/14 20:41:28 [INFO] [infra] wrote the forwarded-header clean-up → /opt/docker/stacks/traefik/dynamic/forwarded.yml (felhom-forwarded)
2026/10/14 20:41:28 [INFO] [infra] connected felhom-controller to traefik-public
# Traefik dynamic config — the chain every websecure request passes: https only (R-1033), then the forwarded-header
# clean-up + HSTS. Managed by felhom-controller.
# WARNING: auto-generated at base-infra bring-up. Manual edits are overwritten. traefik.yml names felhom-forwarded on its
# websecure entrypoint, so a missing file would break every route: it is written before traefik.yml.
# An empty value REMOVES the header (traefik headers middleware).
http:
middlewares:
felhom-forwarded:
chain:
middlewares:
- felhom-https-only
- felhom-forwarded-headers
felhom-https-only:
redirectScheme:
scheme: https
@@ -0,0 +1,34 @@
control NEGATIVE (a stub answering 200 over plain http): convicted, as it must (200 -)
== tester-1 (enkicsifelhom.hu): 13 public name(s)
OK control POSITIVE http://felhom.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://felhom.enkicsifelhom.hu/r1033-check?q=1
OK http://files.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://files.enkicsifelhom.hu/r1033-check?q=1
OK http://media.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://media.enkicsifelhom.hu/r1033-check?q=1
OK http://no-such-name-r1033.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://no-such-name-r1033.enkicsifelhom.hu/r1033-check?q=1
OK http://paperless.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://paperless.enkicsifelhom.hu/r1033-check?q=1
OK http://paste.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://paste.enkicsifelhom.hu/r1033-check?q=1
OK http://plex.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://plex.enkicsifelhom.hu/r1033-check?q=1
OK http://prowlarr.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://prowlarr.enkicsifelhom.hu/r1033-check?q=1
OK http://radarr.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://radarr.enkicsifelhom.hu/r1033-check?q=1
OK http://sonarr.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://sonarr.enkicsifelhom.hu/r1033-check?q=1
OK http://torrent.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://torrent.enkicsifelhom.hu/r1033-check?q=1
OK http://transmission.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://transmission.enkicsifelhom.hu/r1033-check?q=1
OK http://wiki.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://wiki.enkicsifelhom.hu/r1033-check?q=1
== demo-hp (enkisfelhom.hu): 12 public name(s)
OK http://books.enkisfelhom.hu/r1033-check?q=1 -> 301 https://books.enkisfelhom.hu/r1033-check?q=1
OK http://docs.enkisfelhom.hu/r1033-check?q=1 -> 301 https://docs.enkisfelhom.hu/r1033-check?q=1
OK http://felhom.enkisfelhom.hu/r1033-check?q=1 -> 301 https://felhom.enkisfelhom.hu/r1033-check?q=1
OK http://files.enkisfelhom.hu/r1033-check?q=1 -> 301 https://files.enkisfelhom.hu/r1033-check?q=1
OK http://gist.enkisfelhom.hu/r1033-check?q=1 -> 301 https://gist.enkisfelhom.hu/r1033-check?q=1
OK http://ha.enkisfelhom.hu/r1033-check?q=1 -> 301 https://ha.enkisfelhom.hu/r1033-check?q=1
OK http://no-such-name-r1033.enkisfelhom.hu/r1033-check?q=1 -> 301 https://no-such-name-r1033.enkisfelhom.hu/r1033-check?q=1
OK http://papir.enkisfelhom.hu/r1033-check?q=1 -> 301 https://papir.enkisfelhom.hu/r1033-check?q=1
OK http://paste.enkisfelhom.hu/r1033-check?q=1 -> 301 https://paste.enkisfelhom.hu/r1033-check?q=1
OK http://pdf.enkisfelhom.hu/r1033-check?q=1 -> 301 https://pdf.enkisfelhom.hu/r1033-check?q=1
OK http://travel.enkisfelhom.hu/r1033-check?q=1 -> 301 https://travel.enkisfelhom.hu/r1033-check?q=1
OK http://wiki.enkisfelhom.hu/r1033-check?q=1 -> 301 https://wiki.enkisfelhom.hu/r1033-check?q=1
== demo-felhom (demo-felhom.eu): 4 public name(s)
OK http://felhom.demo-felhom.eu/r1033-check?q=1 -> 301 https://felhom.demo-felhom.eu/r1033-check?q=1
OK http://files.demo-felhom.eu/r1033-check?q=1 -> 301 https://files.demo-felhom.eu/r1033-check?q=1
OK http://gist.demo-felhom.eu/r1033-check?q=1 -> 301 https://gist.demo-felhom.eu/r1033-check?q=1
OK http://no-such-name-r1033.demo-felhom.eu/r1033-check?q=1 -> 301 https://no-such-name-r1033.demo-felhom.eu/r1033-check?q=1
== every public name redirects http -> https
+6 -3
View File
@@ -124,7 +124,7 @@ stopping line that lies.
| **R-494** | Install & onboarding | P4 | **NARROWED 2026-09-14 by operator ruling → [P3-LOW] the hub COULD create the tunnel at customer creation, for a domain already on Cloudflare. Not blocking: every customer has their own domain and the operator creates the tunnel per day-0 A.1 (`architecture/01-topology-and-trust.md`).** *Original finding, kept:* **[P1-HIGH] A new customer's dashboard has NO reachable address unless the operator hand-makes a Cloudflare tunnel — the link in the setup-code mail is dead.** MEASURED 2026-09-14 on a fresh install from the public ISO (drill intervention **I1**): the claim mail points at `https://felhom.drill0242.felhom.eu`; that name has **no A and no AAAA** record (`dig @1.1.1.1`, control `felhom.enkisfelhom.hu` resolves); the hub has **no tunnel- or DNS-creation code** (`hub/internal/cloudflare/` holds only geo-rule removal; `cf_tunnel_token` is a pasted, optional form field, `configs.go:1478`) — day-0 runbook A.1 makes it a manual Cloudflare-dashboard step that nothing on the customer-create page asks for; the box's own split-horizon resolver on the appliance LAN IP answered `google.com` but not the dashboard name at 13:27:39Z; the agent applied the record at **13:27:44Z** (`lanresolver: applied split-horizon record … ip=192.168.0.158`, 3 m 46 s after the controller started), so the box CAN answer the name — **but only to a device that uses the box as its DNS server, and no document, screen or mail tells a household to do that**; the router and the installer-offered DNS answer nothing. The page was reachable only at the guest's LAN address with the name forced (`curl --resolve …:443:192.168.0.158`). **A volunteer could not have done that.** **What it needs:** an operator ruling — the hub creates the tunnel and DNS at customer creation, or the product gives a household a LAN address that works with no DNS change. | **READY — rank P3-LOW; owner: CC** **Re-ranked 2026-10-03: P3→P4: operator-side automation; the operator creates the tunnel by hand per the day-0 runbook.** | — | — | CC |
| **R-504** | Install & onboarding | P4 | **[P3-LOW] `iso.felhom.eu` cannot show an index page on its own — its root returns 404, and the download page lives on the website instead.** MEASURED 2026-09-14: `https://iso.felhom.eu/` and `/index.html` → 404; only named objects answer. The host is an R2 bucket behind a custom domain; whether R2 would serve an uploaded `index.html` at `/` was **not measured** (uploading anything to the public bucket is a publication). The ISO v1.27.0 task puts the Hungarian download page at `felhom.eu/letoltes` (published with the ISO, after the operator's yes). **Remaining:** a redirect from `iso.felhom.eu/` to that page needs a Cloudflare rule the session has no credential for. | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator (Cloudflare rule)** **Re-ranked 2026-10-03: P3→P4: households are sent to the website's download page; the bare address is cosmetic.** | — | — | operator |
## Apps & catalog — 15 rows (P2 1, P3 10, P4 4)
## Apps & catalog — 16 rows (P2 1, P3 10, P4 5)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -143,6 +143,7 @@ stopping line that lies.
| **R-707** | Apps & catalog | P4 | **[P2] 37 apps still start with a login a stranger can take (`09` §3 decision 45).** Audit of all 53 apps: `app-catalog-felhom.eu/FIRST-ADMIN.md` (class, fix route, status, measured or read). Open: **3 hard-coded defaults** — calibre-web (`admin / admin123`, measured working on demo-hp and 9202; its own `cps.py -s` route needs a generated password WITH a special character — our generator is letters+digits, a controller change), mealie (`changeme@example.com / MyPassword`), wger (`admin / adminadmin`); **34 open first-run screens** (the first visitor creates the admin: actualbudget, adventurelog, audiobookshelf, calcom, docmost, emby, ghost, gitea, gramps-web, home-assistant, homebox, immich, jellyfin, komga, n8n, navidrome, opengist, outline, papra, plant-it, radarr, rallly, recipe-importer, romm, seerr, sonarr, sparkyfitness, tandoor, termix, uptime-kuma, vikunja, wanderer, wishlist, zipline). **Stale notes:** romm's `default_creds` `admin / admin` answers 401 on demo-hp (like a wrong password) — the page now warns with a login that does not exist; zipline's looks stale too. **Measured on demo-hp 2026-09-28 (read-only):** bookstack's default still logs in on the INSTALLED app (the fix is for new installs; the page now warns). Each fix: route (a) env or (b) the app's own CLI/API via `after_install:`, proven on 9202 with the default failing and the generated password working; route (c) a page sentence. Several sessions (operator, 2026-09-28). **2026-09-29 (controller v0.280.0, catalog `d0e7e2e`):** every class-3 app fixed — mealie, wger, calibre-web by `after_install` (calibre-web with the new `password:24:special`), proven on 9202 fresh installs (`audits/login-gate-2026-09-29/D/`); the setup gate (decision 46, spike PASSED) built and live on immich, n8n, audiobookshelf (probes measured) and uptime-kuma (button) (`…/C/`); romm's and zipline's stale notes removed. **Left: 30 class-4 apps** — gate each (probe measured on 9202 where one exists — 11 upstream candidates listed in `…/B/B-VERDICT.md` §3; the button otherwise). **2026-09-29 afternoon (controller v0.281.0, catalog `6faf432`):** 28 more class-4 apps gated — 32 of 34 — each proven on 9202 (`audits/gate-rollout-2026-09-29/`B): stranger → gate page / 401, household reached the first-setup screen, the gate opened (9 by a measured probe, the rest by the press), the app answered after. seerr, outline, rallly: gated, their opening needs a media server / e-mail (not proven). **Left:** wanderer (R-714); plant-it is not installable. | **NARROWED** (2026-10-03 triage: the row's verdict was finished, but it names open work no other row carries — seerr, outline and rallly are gated, but the gate OPENING is not proven (needs a media server / e-mail)) — **CLOSED — 2026-09-29 (the rest → R-714)** | — | — | CC |
| **R-905** | Apps & catalog | P4 | **wger's collected style files (283 MB) go into every wger backup, though the app rebuilds them at every start.** Since R-762's fix (`DJANGO_DEBUG=False` + `collectstatic`, catalog `cf1ed43`) the static files sit in a named volume that the backup legs copy like data (measured 283 MB, `audits/design-build-2026-10-06/`); `collectstatic` regenerates them, so they are rebuildable bytes in every Tier-1 unit, Tier-2 copy and off-site snapshot. wger is `lifecycle: hidden` and no box runs it (hub read 2026-10-08). Options to weigh: a non-backed-up volume class for regenerable data, or an anonymous volume. Found by the R-762 helpers 2026-10-08. | **OPEN — needs a design before wger is shown** | — | Design the „regenerable volume" exclusion (catalog + controller backup legs) | CC |
| **R-995** | Apps & catalog | P4 | **Removing an app through the product can leave unnamed Docker volumes and an empty import folder behind.** MEASURED 2026-10-13 on demo-felhom after 61 removals (`documentation/audits/sizing-2026-10-13/METHOD.md` „Teardown”, `evidence/leftover-volumes-after-bench.txt`): 8 unnamed volumes (4–72 KB; immich 1, romm 2, privatebin 3, onlyoffice 2 — volumes the images declare themselves) and `userdata/import/{paperless,calibre,grimmory}` (empty, created by the deploy). The removal answered 200 and its own leftover check was clean, because it looks only at labelled volumes. No data risk; disk and tidiness only. Removed by name on the bench. | **READY** — filed 2026-10-13 | — | Make the removal account for the containers' unnamed volumes (and an empty import folder it created), with a test that deploys an image that declares a VOLUME | CC |
| **R-1043** | Apps & catalog | P4 | **Sonarr, Radarr and Prowlarr build their login redirect as an absolute `http://` URL.** They trust forwarded headers only from their „Trusted networks” (the household set `192.168.0.0/24`; traefik is `172.18.x`), so they ignore `X-Forwarded-Proto: https` and answer `Location: http://<name>/login` to an https visitor. Since controller v0.312.0 that hop is sent back to https by traefik (a GET, no password), so it is cosmetic; a template could seed traefik's network as trusted. (`audits/https-only-2026-10-14/``1A-3-4.txt`) | **OPEN** — filed 2026-10-14 | — | Seed the docker network as a trusted proxy in the *arr templates (with the R-1030 seed), measure the redirect | CC |
## App updates — 4 rows (P3 4)
@@ -202,17 +203,19 @@ stopping line that lies.
| **R-332** | Storage & devices | P3 | **The new Hiba-from-counters path has never fired on real hardware.** v0.215.0's whole point is a verdict the product could not previously reach, and it is proven only against the committed fixture's values in unit tests (12 scenario groups, 11 of 12 red-proofs failing as required). The live validation on demo-hp proved the **negative** — three healthy disks still read Rendben across the deploy, no false alert — and the **severity wire** end to end, but no live disk has actually reached Hiba. **This is the honest gap and it must not be closed by pointing at the fixture tests**: the drive that produced the fixture is in DooPlex, which is Tier 2 and never a drill target, and the demo boxes are all-flash and healthy | **WATCHING — NEW 2026-08-14, NARROWED same day.** One item originally in this gap is now PROVEN LIVE: the **persisted state surviving a controller restart**. The v0.215.0→v0.216.0 redeploy destroyed and rebuilt the container, and the new one read back a `changed_at` written by the PREVIOUS version (`2026-08-14T07:23:14.640216851Z`, still intact at 09:31:35Z) instead of re-baselining — Scenario L on real hardware, not just the production-path unit test. **What remains unproven is the verdict itself, plus the stronger restart half: an already-ALERTED disk not re-alerting** | a real degrading disk, or an injection harness | **Closing condition:** a live disk reaching Hiba from counters, OR a deliberate injection through the REAL pipeline (agent `/disks` → controller check → hub event), not a hand-set verdict | CC |
| **R-331** | Storage & devices | P4 | **Disk health Phase 3 — growth-rate detection, and retiring the static 64.** The v0.215.0 count backstop (64 unreadable sectors → Hiba) is **a judgement from ONE drive**: the observed benign excursion peaked at 16 and cleared inside an hour, and the terminal run passed 64 at 13 Aug 11:28 and never came back. It is deliberately a backstop BEHIND the sustain rule, not the primary signal, but it is still a magic number tuned on a single sample and it will be wrong for some drive. With Phase 2's history the box can ask the question that actually matters — *is this count climbing, and how fast* — which distinguishes a drive with eight stable aging sectors from one adding forty a day, something no static threshold can do. Revisit 64 when that exists | **READY (M) — NEW 2026-08-14** | R-330 | Growth-rate rule over persisted samples; re-derive or delete the static 64 | CC |
## Security & access — 18 rows (P1 1, P2 4, P3 8, P4 5)
## Security & access — 20 rows (P1 2, P2 3, P3 10, P4 5)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
| **R-1033** | Security & access | P2 | **Every box answers its public names over plain `http://` — login pages included.** MEASURED 2026-10-14 from DooPlex over the public path (Cloudflare `Server: cloudflare`, `CF-RAY`): `http://felhom.enkicsifelhom.hu/login` → 200, `http://felhom.enkisfelhom.hu/login` (demo-hp) → 200, `http://torrent.enkicsifelhom.hu/` → 200; no redirect to https and no `Strict-Transport-Security`. Sonarr, Radarr and Prowlarr make it worse: their sign-in redirect is ABSOLUTE `http://<name>/login`, so a household that opened the https address lands on the plain page and sends its password unencrypted between the browser and Cloudflare (`audits/media-all-drives-2026-10-14/H-indexer-sync-ncore.txt`). On an untrusted network (café Wi-Fi) that password can be read. The operator may raise this to P1. | **READY** — filed 2026-10-14 | — | Redirect http → https for every public name (Cloudflare „Always Use HTTPS” on each customer zone, or traefik redirecting when the tunnel's `X-Forwarded-Proto` is `http`), then HSTS; a check in the box walk that every name answers 301/308 on http | CC |
| **R-1033** | Security & access | P1 | **Every box answers its public names over plain `http://` — login pages included.** MEASURED 2026-10-14 from DooPlex over the public path (Cloudflare `Server: cloudflare`, `CF-RAY`): `http://felhom.enkicsifelhom.hu/login` → 200, `http://felhom.enkisfelhom.hu/login` (demo-hp) → 200, `http://torrent.enkicsifelhom.hu/` → 200; no redirect to https and no `Strict-Transport-Security`. Sonarr, Radarr and Prowlarr make it worse: their sign-in redirect is ABSOLUTE `http://<name>/login`, so a household that opened the https address lands on the plain page and sends its password unencrypted between the browser and Cloudflare (`audits/media-all-drives-2026-10-14/H-indexer-sync-ncore.txt`). On an untrusted network (café Wi-Fi) that password can be read. The operator may raise this to P1. | **NARROWED** — filed 2026-10-14; **P1 by the operator 2026-10-14 22:13 (`09` §3 decision 244)**. FIXED in controller v0.312.0 (traefik sends every `http://` to `https://`, 301, path and query kept; HSTS one day; decision 246) and PROVEN on Tester 1, demo-hp and demo-felhom (29 public names, `scripts/https_only_check.py`; https answers unchanged), delivered by hand. Open: the fleet delivery — vouch 0.312.0 and the floor (morning); Tester 2 (`sajatfelhom.hu`, 0.292.0, off) gets it with the floor | the operator's vouch + floor | Vouch controller 0.312.0, raise the floor; then run `scripts/https_only_check.py` on every box and close | CC |
| **R-985** | Security & access | P3 | **Our zero-knowledge wording is stronger than the product: on a live box the operator can reach the off-site backup key (through the vaulted root credential and the operator tunnel) without the customer's recovery code.** Design-shaped; decision for the operator in `STATUS.md`. Fix options: change the wording, or remove operator root reach to the key. Security review 2026-10-12 (`audits/security-review-2026-10-12/`); detail held off-git for the operator (`/mnt/5_hdd/felhom.eu/evidence/security-review-2026-10-12-PRIVATE/`). | WAITING-ON-OPERATOR — design-proposal | operator decision | Decide the wording or the change | operator |
| **R-983** | Security & access | P3 | **Seven small dashboard weaknesses: after sign-in the browser can be sent to another site; the server-reboot route acts on a GET, so a link can reboot the guest (**FIXED 2026-10-13 in controller v0.310.0, R-998 session: POST only, `TestServerReboot_PostOnly` — six remain**); no anti-framing headers; parallel sign-in attempts get past the limiter; the settings page allows 8-character passwords; reset-code requests have no limit on the box; logout can be triggered from another site.** Fix each with a test, in one controller release. Security review 2026-10-12 (`audits/security-review-2026-10-12/`); detail held off-git for the operator (`/mnt/5_hdd/felhom.eu/evidence/security-review-2026-10-12-PRIVATE/`). | OPEN | — | One hardening release | CC |
| **R-977** | Security & access | P3 | **FileBrowser's default password can come back if its data volume is ever re-created, because the box checks it only once; and the tunnel can start before the box is claimed.** Static; needs a live check on 9202. Fix: check at every start, and do not start the tunnel before the claim. Security review 2026-10-12 (`audits/security-review-2026-10-12/`); detail held off-git for the operator (`/mnt/5_hdd/felhom.eu/evidence/security-review-2026-10-12-PRIVATE/`). | OPEN | — | Measure on 9202; then fix | CC |
| **R-976** | Security & access | P3 | **A family member's session can be copied out of a family-gated app, and a dashboard password reset does not end family or gate sessions; so a stolen session keeps working for up to 30 days.** Fix: give each app its own derived token instead of the session itself, and end every family and gate session on a reset. Security review 2026-10-12 (`audits/security-review-2026-10-12/`); detail held off-git for the operator (`/mnt/5_hdd/felhom.eu/evidence/security-review-2026-10-12-PRIVATE/`). | OPEN | — | Fix with tests in a controller release | CC |
| **R-970** | Security & access | P2 | **Whoever holds a box's own hub API key — the hub, or anyone with a copy of the hub database — can read the box's settings file with every secret in it and replace that file through the internet; the box does not check that such a change is signed.** This contradicts `01` §4 (the box exposes no control endpoint) and §10 (a hub compromise alone cannot forge commands). Nothing in the product calls this route any more. Fix: remove the route, or require an operator-signed op for it. Security review 2026-10-12 (`audits/security-review-2026-10-12/`); detail held off-git for the operator (`/mnt/5_hdd/felhom.eu/evidence/security-review-2026-10-12-PRIVATE/`). | OPEN — confirmed by a second reader | — | Remove the route with a test that it is gone; release the controller | CC |
| **R-954** | Security & access | P2 | **We cannot tell whether anyone used the old hub operator key (R-950) or the open hub read routes (R-951) before 2026-10-12; if someone did, they could have read every box's break-glass root credential and every customer's box report.** The exposure began in February 2026; the logs that could show it cover only 2026-10-12 from about 09:54 and one day in June, so about 238 days are not visible. Fix (operator decision): change the break-glass root password of every box (tester-1 holds a real person's data), or accept the risk; either way, keep ingress access logs long enough to answer this next time. | WAITING-ON-OPERATOR — filed 2026-10-12 by the security review; the log search was not completed in-session | operator decision | Decide: rotate every box's break-glass root password now, or accept | operator |
| **R-1041** | Security & access | P3 | **Decide how long browsers must remember „https only” (HSTS), and whether to preload.** Since controller v0.312.0 every box sends `Strict-Transport-Security: max-age=86400` (one day), no `includeSubDomains`, no preload — the night brief's limit (R-1033, decision 244). A longer value protects the first visit after a day away; preload makes even the first visit ever safe but is hard to undo (a browser list) and binds every subdomain of the customer's domain. | **WAITING-ON-OPERATOR** — filed 2026-10-14 | the operator | Choose: keep one day; or e.g. 180 days without preload; or preload (all subdomains https forever) | operator |
| **R-1042** | Security & access | P3 | **The boxes' Cloudflare keys cannot change zone settings, so Cloudflare's own „Always Use HTTPS” cannot be switched on.** Measured 2026-10-14 on all three zones (`audits/https-only-2026-10-14/``1A-1-zones-and-keys.txt`): each `cf_api_token` is valid and sees only its own zone, but GET/PATCH `settings/always_use_https`, `ssl`, `security_header` answer 403; the redirect and transform rule phases too. The https-only fix therefore lives on the box (decision 246). A second layer at Cloudflare needs „Zone Settings: Edit” (and, for headers, „Transform Rules”) added to each customer's key — the hub's zone-reach check stays. | **WAITING-ON-OPERATOR** — filed 2026-10-14 | a key permission only the operator can grant | Optional: widen each customer key, then switch „Always Use HTTPS” on per zone | operator |
| **R-955** | Security & access | P4 | **The cross-box check of the hub API is done in source and tests but not fully proven: if a later check masks a missing id check, a box's key could read or change another box's hub record without any test noticing.** Read 2026-10-12: every box-facing `/api/v1` route takes the box from its own key or refuses a different id; 14 routes already had a refusal test, 5 got one (`crossbox_refusal_test.go`). Left: red-prove the 4 new tests that were not red-proved (as the restore-directive one was), and the live read-only check with one demo box's key against the other demo box's id. | OPEN — filed 2026-10-12 by the security review | — | Red-prove the 4 tests; run the live GET check on the two demo boxes | CC |
| **R-924** | Security & access | P2 | **The operator signing keys exist only on DooPlex, with no passphrase and in no backup — a loss of DooPlex means no box ever accepts a signed agent update, config bundle or OS step again without an on-site re-enrollment.** MEASURED 2026-10-09: `/mnt/5_hdd/felhom.eu/felhom-op-operational`, `felhom-rec-recovery`, `felhom_op_ed25519` (0600 `kisfenyo`); `ssh-keygen -y -P ''` opens all three (no passphrase); `/mnt/5_hdd/felhom.eu` is in no backup set, the off-site job does not carry them. `04` §1 says the operational key is "passphrase-protected" and the recovery key "kept offline" — today neither holds (custody ruling 1 of 2026-09-16 put both on DooPlex for this phase; `04` §3.1). Both keys share one fate, so the cold key cannot do its one job (re-pin after losing the operational key). `runbooks/total-loss-of-dooplex.md` S6. | **NARROWED 2026-10-09 — operator ruling: option A** (print both AND copy them). The three key files and their public halves ride the nightly encrypted DooPlex→ep0 copy (`scripts/dooplex-offsite/`, a missing key refuses the push); the Sunday restore test proves each private key still derives its own public half (never printed). **LEFT:** the operator prints S6a/S6b (`runbooks/break-glass-sheet.md`); the keys still have no passphrase (custody ruling 1 of 2026-09-16 stands until the first sale). | operator: print | Operator: print the two keys; then close | operator |
| **R-132** | Security & access | P3 | **`curl -w '%{redirect_url}'` reconstructs the request URL WITH its basic-auth credential** — so a `-u ":$HUB_PW"` call that never put the password in a URL still printed it **Merged 2026-10-05 from R-350 (duplicate):** (1) 2026-08-20 occurrence: POST /configuration/artifacts answers 303; leak lives only in the CC transcript under ~/.claude/projects/ on DooPlex, not in git/evidence (checked then). (2) `-v` and `--libcurl` also re-render the credential, not only %{redirect_url}; confirm redirects with %{http_code} + follow-up GET. (3) Rotation path: hub /configuration form (current_password/new_password/confirm_password); DB override wins over ConfigMap (break-glass); CC can rotate file-to-file without printing (operator-present-one-time-secrets) if asked. | **WAITING-ON-OPERATOR** — **ACTION: rotate `HUB_PW`** **Folded R-580 2026-10-03** (the same `curl -w %{redirect_url}` credential echo, seen again 2026-09-18). **Note 2026-10-12 (security review):** `manifests/hub.yaml` also carries the hub login's bcrypt hash in this public repository (the break-glass field); when `HUB_PW` is rotated, decide whether that field should stay empty. | — | Happened on 2026-07-31 while red-proofing the R-120 gate: the hub operator password was written to the session transcript by the write-out format, not by the request. `-u` is safe; the *reporting* was not. Rule: read the redirect from `-D -` and grep `^Location:`, never `%{redirect_url}`, on any authenticated call. Rotate the hub password (`/configuration` → Login password; ConfigMap `auth.password_hash` is the reset path) and update `~/.config/credentials` | Viktor |