burndown 3: report, evidence, register (174 -> 168), 07 Redis RDB [DESIGN], 04 §6.3 the box's password doors
gates / gates (push) Successful in 7m2s

Closed R-1115, R-1111, R-1112, R-1113, R-1109, R-1110, R-1114, R-1096; opened R-1116 (decoy suites in the other
runners), R-1117 (Immich/Nextcloud Redis volume, deferred). Controller 0.325.0 is the floor since 08:21 UTC.

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-17 10:28:03 +02:00
parent 0314b8e7cf
commit e1dd1cadd3
40 changed files with 2654 additions and 14 deletions
+13
View File
@@ -26,6 +26,19 @@
---
## 2026-10-17 — burndown 3: limiters, family fingerprints, samba label, path guard, gates in a worktree, Redis RDB (controller v0.325.0, catalog `33a4b3b` + `5d9450b`)
| Row | What | Closed | Evidence |
|---|---|---|---|
| **R-1096** | **After a disk-full, an app's damaged Redis file keeps the app down for good, and nothing says what to do.** (P3) | CLOSED 2026-10-17 — catalog `5d9450b`: romm, docmost, paperless-ngx, immich, nextcloud Redis `--appendonly no`. Proved first on scratch 9202 (100/100 keys through the switch and through dump + restore; damaged AOF refuses to start; RDB on a full disk keeps the old file). `steps/` unchanged (`09` §6.4 part 5). Gate `redis-persistence`. demo-hp 9201: three apps restarted, each `aof_enabled:0`. Tester 1 stays on the drill catalog (R-1022). `07` [DESIGN] paragraph. Volume removal deferred → R-1117. | `audits/burndown-3-2026-10-17/` |
| **R-1109** | **A change that alters only `smb.conf` (not the compose file — e.g. a share renamed at the same path) is never applied to a running samba container.** (P4) | CLOSED 2026-10-17 — **controller v0.325.0**: the samba compose carries `felhom.smb-conf-sha256`; an smb.conf-only change recreates the container, an unchanged one renders a byte-identical compose. Tests red-proved. Live on 9201: share added, renamed on the same folder, a second share on the same folder — each read/written by `smbclient` from 9202, the old name refused, the label = the smb.conf hash at every step. Measured too: through the dashboard a rename was already applied (delete and add each change the compose); the label covers a controller update that changes smb.conf alone. Every box with sharing on recreates samba once. | `audits/burndown-3-2026-10-17/` |
| **R-1113** | **`/__family/start` with a household dashboard session creates a NEW 30-day household family session on every pass, so `family.json` keeps growing.** (P4) | CLOSED 2026-10-17 — **controller v0.325.0**: `Store.HouseholdSession` reuses the household's live family session (a new one only under half of its 30 days). `TestR1113_HouseholdPassesLeaveOneSession` (6 passes → 1), red-proved; live on 9201: 3 browser passes, household sessions stayed 2. | `audits/burndown-3-2026-10-17/` |
| **R-1112** | **Family sessions are stored in `family.json` as the raw session value, not as a fingerprint the way dashboard sessions are (R-35)** (P3) | CLOSED 2026-10-17 — **controller v0.325.0**: `family.json` holds `fp` (hex sha256 of the cookie); an old file is converted and rewritten — at controller START (rc.1 opened the store lazily; found live on demo-hp 9201, fixed in rc.2: „2 session(s) … rewritten as fingerprints”). Tests `TestR1112_*` (4) + `TestFamily_OldFileThatCannotBeRewrittenIsAnError`, each red-proved; headless Chrome ×3 through the family gate on 9201. | `audits/burndown-3-2026-10-17/` |
| **R-1111** | **The share-password limiter (`/s/<token>`, `shareRateLimited`) and the escrow re-auth limiter (`escrowRateLimited`) still count a try only AFTER the slow password check, so parallel tries get past them** (P3) | CLOSED 2026-10-17 — **controller v0.325.0**: one limiter body `reserveAttempt` for the dashboard sign-in, the share password (`shareReserve`) and the escrow re-auth (the login counter): the try is reserved before the compare. `TestR1111_ParallelSharePasswordsHitTheLimit`, `TestR1111_ParallelEscrowReauthsHitTheLimit` (20 parallel tries → at most 5 compares), red-proved. REUSE.md row updated; `04` §6.3 now records the four password doors. | `audits/burndown-3-2026-10-17/` |
| **R-1110** | **The R-1017 guard covers `UserdataDir` only; two sibling path helpers are still called with drive-named arguments whose comments claim a namespace root:** (P4) | CLOSED 2026-10-17 — **controller v0.325.0**: `TestNoBareDrivePathToPathHelpers` reads all 42 calls of `RecoveryUnitPath`/`AppDataDir`/`PrimaryBackupPath` in both packages. None passed a bare drive path; two only by a comment: `GetStackBackupData`'s parameter renamed `nsRoot` (its caller passes `AppNamespaceRoot`), `appDetailsForPath` now goes through `NamespaceRootFor`. Loop variables renamed in three files. Red-proved three ways. | `audits/burndown-3-2026-10-17/` |
| **R-1114** | **`controller_gates.py` cannot pass inside a worktree outside `git/` (e.g. `/mnt/5_hdd/felhom.eu/wt/<name>`): the reuse-refs, instructions and observations gates look for felhom.eu only at `../felhom.eu`, report MISSING, and the runner fails.** (P4) | CLOSED 2026-10-17 — **controller v0.325.0** + felhom.eu `0314b8e7`: `controller/scripts/shared_scripts.py` (env, else the main clone's sibling via `git worktree list`, else `../felhom.eu`) for `controller_gates.py`, `retrieval_promise_gate.py`, `golden_notice.py`; `reuse_refs_check.py` reads `FELHOM_WORKSPACE` and skips the worktree's own main clone. Tests red-proved; the runner passed in a worktree under `/mnt/5_hdd/felhom.eu/wt/` and in the clone. | `audits/burndown-3-2026-10-17/` |
| **R-1115** | **The catalog decoy suite fails one case: „FACT: a credential token rewritten in translation" — `check-copy-i18n.py` answers rc 0 where the decoy expects 1, so the gate may no longer convict a reworded credential token.** (P3) | CLOSED 2026-10-17 — the DECOY moved, not the gate: `326f846` (R-1102) removed gokapi's Hungarian login line, and the case appended an English login to a template with nothing to compare. The case now rewrites wger's existing English line in place (a removed premise stops the suite), plus a new case (calibre-web `admin123` → `admin1234`); both refused, rc 1, the old case rc 0 on today's tree. `gate-decoys` is a row of `catalog_gates.py`'s full run (196 s measured, PyYAML — not `--fast`); `test_catalog_gates.py`, red since the night before (count 18), fixed. Copy gate over the whole catalog: OK, no template convicted. Catalog `33a4b3b`. | `audits/burndown-3-2026-10-17/` |
## 2026-10-17 — night: restore after a reinstall, restic 0.19.1, hardening (controller v0.324.0, catalog `af75dc0`)
| Row | What | Closed | Evidence |
+7 -13
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 — 20 rows (P2 1, P3 11, P4 8)
## Apps & catalog — 20 rows (P2 1, P3 10, P4 9)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -145,9 +145,9 @@ stopping line that lies.
| **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-1067** | Apps & catalog | P4 | **The catalog still carries `pi_compatible:` on every template, and nothing reads it any more.** controller v0.315.0 removed the „Pi kompatibilis" badge and the „csak x86" tag from every page (Raspberry Pi is no longer supported, so one was noise and the other was a warning on almost every app). The `ResourceHints.PiCompatible` struct field and the catalog key were deliberately LEFT: a box still on 0.314.0 reads the key, and removing it would make that box show „csak x86" on every app. **Gated on the floor:** once the fleet floor is at or above 0.315.0, drop `pi_compatible:` from the templates and the field from `stacks.ResourceHints` in the same pair of commits. Harmless until then — an unread key costs nothing. | **BLOCKED** — filed 2026-10-15. Checked 2026-10-16 (apps burndown): the floor is 0.321.0 and demo-hp, Tester 1 and the N100 run 0.321.0, but **Tester-2 still reports 0.292.0** (its newest report 2026-10-04; hub DB with WAL) — so nothing removed. | every box on ≥ 0.315.0 — today Tester-2 | Drop the catalog key and the struct field; grep the hub and the website for Pi wording at the same time (none found 2026-10-15) | 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 |
| **R-1096** | Apps & catalog | P3 | **After a disk-full, an app's damaged Redis file keeps the app down for good, and nothing says what to do.** FOUND 2026-10-16 on demo-hp 9201: the standing RomM (installed 2026-08-21) was stopped; the dashboard's Start re-created it, `romm-redis` crash-looped (unhealthy), the box stopped it again (`crash_loop`, `09` §3 decision 28) and mailed `app_start_failed`. Its Redis volume dates from the 2026-09-24 thin-pool fill (R-672): Docmost's damaged append-only file was repaired by hand that day with `redis-check-aof --fix` (`audits/r672-2026-09-24/B7-redis-aof-fix.txt`); RomM's was not (the copy `romm_romm_redis_data.pre-aof-fix-20260924T135937Z` is still on the box). The Redis log was not read: the box removed the containers first. That RomM was then removed by operator ruling (`audits/romm-ghost-2026-10-16/A1/`). | **READY** — 2026-10-17 night: researched, NOT changed (`audits/night-restore-2026-10-17/P6/redis-persistence.md`). Seven Redis services: romm, docmost, paperless-ngx, immich, nextcloud run append-only (AOF) — a file damaged in the middle stops Redis until `redis-check-aof --fix`; outline and dawarich run RDB snapshots, which a full disk cannot damage. Proposed: AOF → RDB for romm/docmost/paperless (queues + sessions), OFF (tmpfs) for immich/nextcloud (upstream runs them without a volume). Not edited unattended: the same lines sit in 7 ladder step files, a removed volume orphans on installed boxes, and keeping data across AOF → RDB is unproven. **Correction:** RomM's AOF WAS repaired on 2026-09-24 (`audits/r672-2026-09-24/B7-redis-aof-fix.txt`); the 2026-10-16 crash loop has no proven cause (its Redis log was never read). | — | Prove AOF → RDB keeps data on 9202 (and reproduce a damaged AOF there); decide the step-file rule; then the template change | CC |
| **R-1100** | Apps & catalog | P3 | **One sign-in for the dashboard and FileBrowser (proxy auth) — spike first.** Since controller 0.320.0 every app with a folder has „Fájlok ↗", and the box can show FileBrowser's password — but FileBrowser still asks for its OWN login the first time, so the household signs in twice. FileBrowser Quantum has a proxy-auth method (`auth.methods.proxyAuth`, header-trusted) that could take the dashboard session through traefik; nobody has measured it. Design: `architecture/02-controller-module-map.md` „Logins and folders". | OPEN — filed 2026-10-16 (task logins-folders). | — | Spike on demo-hp: proxy auth at the pinned tag (source + measured), a forwardAuth in traefik that answers only for a dashboard session, what a stranger reaching `files.<domain>` with a forged header gets, and what happens to FileBrowser's shares. | CC |
| **R-1099** | Apps & catalog | P4 | **The N100 may hold RomM games in the old upload place, now shown under a fake „roms” platform.** Until 2026-10-16 RomM's own upload wrote to the drive's `roms/roms/<platform>/`; after the mapping fix (catalog `fb18471`, R-1089) those files are at RomM's `library/roms/roms/…`. Measured on 9201: such a game showed as missing in its platform and again under a platform „roms” (`audits/romm-ghost-2026-10-16/B/b-after.txt`). Files put in with the file manager are not affected — they are now found. The N100 runs RomM (hub `/apps/romm`); CC does not log in to the lived-in box. | OPEN — filed 2026-10-16 | Operator: look at `roms/roms/` on the N100's drive in the file manager | If it holds platform folders: move them up one level into `roms/`, then run „Scan” in RomM; if it is empty, close | Operator |
| **R-1117** | Apps & catalog | P4 | **Immich's and Nextcloud's Redis keep a volume they do not need** (upstream runs both without one; queues and caches only). Deferred from R-1096 on purpose: removing a volume from the compose leaves the old volume on installed boxes, and the nightly dump / an old backup's restore for a volume the compose no longer declares is unchecked. The RDB switch (catalog `5d9450b`) already removed the full-disk danger. | DEFERRED — filed 2026-10-17 | — | Measure the dump and an old-backup restore without the volume on 9202; then the template change + an orphan-volume note | CC |
## App updates — 4 rows (P3 4)
@@ -193,7 +193,7 @@ stopping line that lies.
| **R-32** | Backup & restore | P4 | **[P2-HIGH] RESET must purge the customer base dir; the orphan card must stay honest; unattributed bytes must be visible.** The rehearsal's S7 said in advance that an orphan card would BE a finding — and one appeared (16:58:14). Cause: RESET's `"hetzner":"ok"` leg destroys the sub-account, but **a Hetzner sub-account is an access-control object, not a data object** — its directory survives, so re-enabling offsite recreated an account over the previous lifecycle's ciphertext, encrypted under a key that same RESET had destroyed. **MIGRATED FROM `ROADMAP.md` 2026-08-22 (R-369) — originally filed 2026-07-21, size M, roadmap state `idea`.** Moved verbatim; nothing added or reinterpreted. The roadmap keeps its copy as history, marked moved. | **OPEN — migrated from ROADMAP 2026-08-22, rank unchanged** **2026-10-06 night: design written** (`audits/night-burndown-2026-10-06/design-R-32.md`) — pick A: purge the repo + set-aside copies through the sub-account's own password login BEFORE deleting it (no main-account credential); the „repo data destroyed" comment at `hub/internal/offsite/offsite.go:276-280` is false for the shared tier; part (3) waits on a read-only measurement (`du` on port 23). Operator: agree to route A. **2026-10-07 07:58: `09` §3 decision 167 — option A yes, and delete the old RESET leftovers once.** **2026-10-07 (Part D step 2, read only):** the hub shows bx11 holding 3.3 GB of data while the four live customers account for ~0.6 GB → **~2.7 GB belongs to no live customer**; no live customer shows a set-aside folder, so the leftovers are in homes whose sub-account is gone, reachable only by the pool box's MAIN account. **The one-time clean-up is STOPPED:** no main-account login for bx11 is held on DooPlex (the API token in the credentials file sees no box). Needs: the main-account login, or the operator lists and deletes in the Hetzner console (`audits/day-2026-10-07/D/D2-listing-readonly.txt`). **2026-10-07: option A DELIVERED (hub 0.142.0):** RESET purges the repo and every `.orphaned-*` through the sub-account's own login before deleting it, and keeps the sub-account if the purge fails (red-proved, `audits/day-2026-10-07/D/`). Not yet seen live (no scratch RESET today). **The one-time clean-up of the ~2.7 GB of leftovers is STOPPED:** it needs the pool box's main-account login (see above). **2026-10-07 14:43 — `09` §3 decision 173: leave the ~2.7 GB of old leftovers for now; re-ranked P2 → P4.** Option A itself is delivered (hub 0.142.0). What stays open is ONLY the leftovers: they were computed (box total minus live customers), never listed, and deleting them needs the pool box's MAIN login, which reaches every customer's data. **Blocker: a safe main-account path.** | — | **Ruling from the run (three parts, deliberately separate):** (1) because RESET destroys custody, the ciphertext it leaves behind is unrecoverable **BY DESIGN** → RESET gains a **main-account purge of the customer base dir** (the existing operator ack already covers it); (2) the **move-aside guard STAYS** for reinstall-*without*-RESET — there custody survives and the card's "history recoverable" promise is true (R-26 depends on exactly that); (3) the operator **Restic tab shows per-customer directory bytes vs attributed snapshot bytes**, so dead data cannot hide. Measured on the pool box that night: **49 M attributed** (2 snapshots, 48.717 MiB) against **1.4 G + 3.0 M unattributed** across TWO `.orphaned-*` dirs. Evidence `restic-and-pool.txt` | CC |
| **R-1087** | Backup & restore | P4 | **Whole-box copies left on the OLD local target are never pruned after the target moves.** MEASURED 2026-10-15 (storage-identity build B space check): the N100's host root (`local`) holds three copies from 2026-07-28 (5.5–5.7 GB each, ≈ 16.8 GB); its agent's `local_backup_target` is now `felhom-backup` (the USB disk, retention 3), and retention prunes only the current target. On a small-root box such leftovers would starve the local copies (R-548). Evidence: `audits/storage-identity-buildB-2026-10-15/space/felhom-pve-9201.txt`. | OPEN | — | Decide whether the agent prunes its own `vzdump-lxc-<vmid>-*` files on a former target (after a newer copy exists on the new one), or the target move asks; until then, the N100's three July files can be removed by hand with the operator's word. | CC |
## Storage & devices — 13 rows (P3 6, P4 7)
## Storage & devices — 12 rows (P3 6, P4 6)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -209,17 +209,13 @@ stopping line that lies.
| **R-1076** | Storage & devices | P4 | **A cheap SSD reports a vendor-less WWN, so a WWN-first disk key may not be unique.** MEASURED 2026-10-15 on the N100's system SSD (AirDisk 512 GB): `ID_WWN=0x5000000123456f1f` — NAA-5 with OUI `000000`, a pattern that looks like a placeholder; its serial `QDF922W009654S30EX` looks real. The agent's wipe binding prefers `wwn-` links (A/internal/storage/durable_device.go:32). Not known whether two units share the WWN. Evidence: `audits/storage-identity-2026-10-15/B-n100.txt`. | OPEN | — | Check a second unit of the same model (or vendor data); if shared, make the agent skip OUI-000000 WWNs (the design already keys on model+serial, `architecture/16-storage-physical-identity.md` §3). | CC |
| **R-1077** | Storage & devices | P4 | **No USB enclosure that hides or fakes a disk's serial has been measured.** The storage-identity spike (2026-10-15) had none at hand and found none in earlier evidence; the design's `usb_bridge` unknown rule (`architecture/16-storage-physical-identity.md` §3) is therefore an assumption. Cheap single- and dual-bay bridges are known to report one bridge serial for any disk. Evidence: `audits/storage-identity-2026-10-15/README.md` case 4b. | OPEN | — | Buy or borrow one cheap USB enclosure (single and dual bay), run `tools/physdisk_probe.py` against it, and set the rule from what it reports. | Operator (a device) then CC |
| **R-1094** | Storage & devices | P4 | **The whole-disk format refusal tells the household to format the free part — which is refused while the other part is in use.** SEEN 2026-10-16 (controller 0.318.0): 409 „Ezen a lemezen egy használatban lévő meghajtó van, ezért a teljes lemez nem formázható. Csak a szabad részét formázhatod." — since agent 0.158.1 a part next to a mounted part can only be USED (the root wrapper trusts no „Felhom's own" claim). Evidence: `audits/storage-identity-buildB-2026-10-15/live/14-samedisk-refusal-offer.txt`. | READY | — | Reword `err.web.meghajto_hasznalatban_formazas` (hu + en): „… nem formázható. A szabad részét használatba veheted." Next controller release; the copy gates run. | CC |
| **R-1109** | Storage & devices | P4 | **A change that alters only `smb.conf` (not the compose file — e.g. a share renamed at the same path) is never applied to a running samba container.** `smb.conf` is a bind-mounted FILE written by atomic rename, so a running container keeps the old inode; `reconcileSambaAt` runs `compose up -d`, which recreates only when the COMPOSE changed. Read from code 2026-10-16 night while fixing R-1108 (the helper session that wrote that fix). | OPEN — filed 2026-10-17 | — | Recreate when `smb.conf` changed (`--force-recreate`, or a config hash label in the compose); a test with a same-path rename | CC |
## Security & access — 21 rows (P1 1, P2 2, P3 11, P4 7)
## Security & access — 18 rows (P1 1, P2 2, P3 9, P4 6)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
| **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-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-1113** | Security & access | P4 | **`/__family/start` with a household dashboard session creates a NEW 30-day household family session on every pass, so `family.json` keeps growing.** Found by the R-976 helper session 2026-10-16 night (read from code). | OPEN — filed 2026-10-17 | — | Reuse the household's live family session (or bound the list); a test that N passes leave one session | CC |
| **R-1112** | Security & access | P3 | **Family sessions are stored in `family.json` as the raw session value, not as a fingerprint the way dashboard sessions are (R-35)** — anyone who reads the controller's data folder can lift a family session. Found by the R-976 helper session 2026-10-16 night. | OPEN — filed 2026-10-17 | — | Store a fingerprint (the R-35 pattern); a test that the file holds no usable value | CC |
| **R-1111** | Security & access | P3 | **The share-password limiter (`/s/<token>`, `shareRateLimited`) and the escrow re-auth limiter (`escrowRateLimited`) still count a try only AFTER the slow password check, so parallel tries get past them** — the shape v0.324.0 fixed for the dashboard and family sign-in (R-983, „reserve before compare"). Found by the R-983 helper session 2026-10-16 night. | OPEN — filed 2026-10-17 | — | The same reserve-before-compare pattern (REUSE.md) on both, with a parallel test each | 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-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-1104** | Security & access | P3 | **Check https-only on Tester 2 when it reports again.** R-1033 (plain http on every public name) is closed on the three boxes that report (35 names → 301, 2026-10-16). Tester 2 (`sajatfelhom.hu`) has been off since 2026-10-04 on controller 0.292.0; its addresses answer Cloudflare 530 (tunnel down), so nothing is served in plain http while it is off. When it comes back it takes the floor (≥ 0.312.0 carries the redirect). | OPEN — filed 2026-10-16 | Tester 2 reporting again | Run `scripts/https_only_check.py` with Tester 2 in it; close | CC |
@@ -309,17 +305,14 @@ stopping line that lies.
| **R-990** | Business & legal | P4 | **Operator: read `documentation/audits/forum-research-2026-10-12/FELHOM.md` §5 and rule on each recommendation.** READ-ONLY research 2026-10-12: three years (#7943–#11092, 2,908 posts, 195 people, 82 beginner threads) of the public PROHARDVER! topic „Home server / házi szerver építése”, tagged and counted by person (`FINDINGS.md`, method and spot-check in `METHOD.md`; no username or post text in git). Ten ranked recommendations, none acted on; the top three: a „which machine works” page (8 of 33 beginners who name their hardware own only a Synology, a router disk or a Raspberry Pi — the installer cannot run there; the same gap is R-989's first recommendation from a different source, and the shipped-page half is R-991), „you do not choose an operating system — we do” (the biggest beginner question, 20 of 82 threads), and „bring your old office PC” as the cheap entry. Catalog gap (≥ 3 people): a DLNA server (7), an ad/DNS blocker (5), a camera recorder (5). | **WAITING-ON-OPERATOR** — filed 2026-10-12 | the operator's reading | Operator: rule on each of the ten (yes / no / later). If nothing is done: nothing changes; the research ages | operator |
| **R-1035** | Business & legal | P4 | **Did the „felhő tárhely” page earn anything? Two dated checks.** **Baseline (reported by the operator from Search Console, 2026-10-14, last 3 months):** 1 click, 48 impressions, CTR 2.1 %, average position **73.6**; „felhő tárhely” 19 impressions, „felhőtárhely” 5, „felhő alapú tárhely” 3. CC measured separately that the phrase „felhő tárhely” appeared **0 times** on the home, FAQ and backup pages — Google was showing the home page for those queries by accident. `/sajat-felho-tarhely` + `/en/own-cloud-storage` published **2026-10-14**; full baseline, price sources and the power provenance in `audits/seo-2026-10-14/BASELINE.md`. **What to measure, both times:** impressions and average position for „felhő tárhely” and „saját felhő tárhely”, plus the new page's own impressions, clicks and position. **A null result at +4 weeks means nothing on its own** — a new page on a site at position 73.6 takes weeks to months to move, and „felhő tárhely” alone is held by Microsoft, Google and the big list sites; the page aims at the two longer phrases. That is why there are two checks and not one, and why neither should be read as a verdict on the copy. **There is no Search Console connector in this project, so CC cannot read these numbers** — the operator reads them, CC records and compares. **Blocked until the operator asks for indexing** (Search Console → URL-ellenőrzés → the URL → „Indexelés kérése”): until that happens, or Google finds the page itself, both checks read zero for a reason that is not the page. **This is a slow channel and does not replace talking to people.** | **WATCHING** | operator: request indexing, then supply the Search Console read | 2026-11-11: first read (+4 weeks); 2026-12-09: second (+8 weeks), compared with the first | operator + CC |
## Process & tooling — 31 rows (P3 11, P4 20)
## Process & tooling — 29 rows (P3 10, P4 19)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
| **R-1083** | Process & tooling | P3 | **The scratch box 9202 cannot use any feature that needs the agent: its `bootstrap.json` has no `local_api` block, so its controller never gets an agent client.** MEASURED 2026-10-15 (storage-identity build A): 9202 put on controller 0.317.0 by hand to show that a NAS share no longer hides the one-copy notice (R-1075); its `bootstrap.json` (read on the box) holds no `local_api`, so the controller has no agent to ask and keeps the old `st_dev` rule — the check could not tell old from new — 9202 was put back on 0.314.0. Every agent-coupled feature (drives, same-disk, format, network storage) is untestable on the scratch box today, and the NAS leg of build A is proven by a red-proofed unit test only. Evidence: `audits/storage-identity-buildA-2026-10-15/9202-upgrade.txt`. | OPEN | — | Give 9202 a local-API token the normal way (the agent mints it for its guest), or record in `runbooks/target-selection.md` that 9202 is controller-only; then show the NAS notice live. | CC |
| **R-1081** | Process & tooling | P4 | **Two published controller tags hold content that is not what their number says: `0.315.0` and `0.316.3`.** Both were caused by CC on 2026-10-15, and both by the same shape — a guard that was not on the path the work takes. **`0.316.3`:** the build directory's `build.sh` is a symlink into the SHARED clone's working copy, which predated the `FELHOM_REPO_DIR` override, so the override was silently ignored and the build copied that tree — which held 31 uncommitted files of another session's physical-disk storage work. The banner printed `Commit: 0225255` with no `Repo:` line. **Nobody deployed it; that was luck, not a control.** Fixed at the root in controller v0.316.4: `build.sh` now refuses `--push` from a tree that is dirty, not at `origin/main`, or not a git checkout, with four test cases red-proved. **Neither tag is re-pushed or deleted** — a version number is pushed once, and that applies to numbers CC spoiled. **No box runs either:** demo-hp 9201 is on 0.317.0, the floor is 0.314.0, and the button offers the highest tag. **`0.315.0`:** Caused by CC on 2026-10-15 while proving R-1080's own fix: the guard was added to `controller/build.sh` in the repo, but the documented release command runs `$FELHOM_ROOT/build/felhom-controller/build.sh`, which was a **separate copy taken 2026-07-18** and had no guard. Running the refusal against a real existing tag therefore rebuilt and re-pushed 0.315.0 — a fourth time — with v0.316.0's code under it (digest `8887343011d6` → `b809201841f7`). **Fixed at the root in the same session:** the build directory's script is now a symlink to the repo's, the old copy kept beside it as `build.sh.stale-copy-2026-07-18`, and the refusal was then proven to fire from the documented path. **No box is affected:** nothing runs 0.315.0 (demo-hp 9201 is on 0.316.2), the floor is 0.314.0, and the household button offers the highest tag. What remains is two tags that must never be deployed and are no longer reproducible from the registry. | **OPEN** — filed 2026-10-15; nothing to do unless someone needs one of those two images, and then the answer is the current release | — | Leave them. If an audit ever needs the real 0.315.0 or 0.316.3, build it from the matching commit under a NEW number and say so | CC |
| **R-992** | Process & tooling | P3 | **The drill-catalog route that `09` §6.5 documents cannot be followed: `admin/app-catalog-drill` is PRIVATE and no box has catalog credentials.** Measured 2026-10-12 while publishing the Usenet pair: the box's clone fails with `fatal: could not read Username for 'https://gitea.dooplex.hu'` (exit 128), the Gitea API reports the drill repo `private=true` and the live catalog `private=false`, and NEITHER the current controller.yaml on 9202 NOR any of its 30 saved copies has ever carried `git.username` or `git.token` (the controller supports both — `internal/config/config.go` GitConfig). The route worked for the campaigns of 2026-10-10 and the torrent pair earlier the same day, so the repository's visibility changed between then and now. **Worked around, not fixed:** that campaign staged its two templates in the box's own catalog cache (`audits/usenet-2026-10-12/tools/stage_in_cache.py`), which is where the sync copies a catalog change anyway, with the sync interval raised to 12h for the duration and rescan used in place of sync. The real fix is an operator decision: either a read-only catalog credential on the scratch box (a secret on a Tier-0 guest) or making the drill repository readable. Until then every new-app campaign hits this. | WAITING-ON-OPERATOR | the operator's choice between a box credential and a readable drill repo | Decide; then either set `git.username`/`git.token` on 9202 from the credentials store, or flip the drill repo, and correct `09` §6.5 either way | operator |
| **R-1110** | Process & tooling | P4 | **The R-1017 guard covers `UserdataDir` only; two sibling path helpers are still called with drive-named arguments whose comments claim a namespace root:** `stacks/delete.go` `RecoveryUnitPath(drivePath, name)` and `web/handlers.go` `backup.AppDataDir(storagePath, name)`. The guard `TestNoBareDrivePathToUserdataDir` found one real defect of this shape tonight (`userdataKept`, fixed in v0.324.0). | OPEN — filed 2026-10-17 | — | Extend the guard to `RecoveryUnitPath`/`AppDataDir`/`PrimaryBackupPath`, fix what it finds | CC |
| **R-1114** | Process & tooling | P4 | **`controller_gates.py` cannot pass inside a worktree outside `git/` (e.g. `/mnt/5_hdd/felhom.eu/wt/<name>`): the reuse-refs, instructions and observations gates look for felhom.eu only at `../felhom.eu`, report MISSING, and the runner fails.** Measured 2026-10-16 night by both helper sessions (each ran the four gates by hand from felhom.eu). | OPEN — filed 2026-10-17 | — | Resolve the sibling from `FELHOM_SHARED_SCRIPTS` or the main clone (`git worktree list`), as retrieval-promise already does | CC |
| **R-1115** | Process & tooling | P3 | **The catalog decoy suite fails one case: „FACT: a credential token rewritten in translation" — `check-copy-i18n.py` answers rc 0 where the decoy expects 1, so the gate may no longer convict a reworded credential token.** Measured 2026-10-16 night (`python3 scripts/test_gate_decoys.py`, with and without the new `db-credentials` cases — the same failure both ways). `catalog_gates.py --fast` is green because the decoy suite is not one of its gates. | OPEN — filed 2026-10-17 | — | Find whether the gate or the decoy moved; red-prove the gate on the case again | CC |
| **R-1022** | Process & tooling | P3 | **Tester 1 follows the drill catalog and carries two test drives — teardown owed (operator ruling (e), 2026-10-14).** Steps, only on the operator's word: (1) remove the test apps (Sonarr, Radarr, qBittorrent, Transmission, Plex, Jellyfin) through the dashboard WITHOUT „delete data” (R-1027); (2) decommission Media A and Media B through Tester 1's dashboard; (3) `repoint.py restore` — `controller.yaml` from `controller.yaml.pre-media-2026-10-14`, read back byte-identical, `repo_url` the live catalog; remove `drill-catalog.git` from the controller volume; (4) on demo-hp: `qm set 341 --delete scsi2,scsi3`, then delete the two volumes BY NAME (`nvme-scratch:341/vm-341-disk-3.raw`, `-disk-4.raw`) — never `qm destroy`; (5) VM memory unchanged (8192, not raised); (6) drill repo reset to live `main`. Tools: `audits/media-all-drives-2026-10-14/``tools/`. **Added 2026-10-14 night:** (4b) Media C — `qm set 341 --delete scsi4`, then the volume `nvme-scratch:341/vm-341-disk-5.raw` by name (decommission it on the dashboard first; it holds 4 test files); (7) DONE 2026-10-15: `plex-k6` and its volume removed after the claim (0 left) — the operator removes its server entry „plex-k6” from plex.tv (Settings → Authorized Devices); (8) drill commit `925f3fd` (media_c binds) goes with step 6; Prowlarr and the test drive tag „driva-a” go with step 1. Tools also in `audits/media-drive-events-2026-10-14/tools/`. **Added 2026-10-15 (media build):** (4c) Media D — decommission it on the dashboard, then `qm set 341 --delete scsi5` and the volume `nvme-scratch:341/vm-341-disk-6.raw` by name (20 GB; 2 own-bytes torrents on it); (9) Tester 1 runs controller `0.313.0-rc.3/rc.4` by hand — put the vouched release back (`/etc/felhom-controller-image`); (10) the rig's Sonarr/Radarr hold two sets of drive tags and clients (the night script's `drive-a…` and the product's `media-a…`) — they go with step 1. | **WAITING-ON-OPERATOR** — filed 2026-10-14 | the operator's teardown word | Run the six steps; quote each read-back | CC |
| **R-1022** | Process & tooling | P3 | **Tester 1 follows the drill catalog and carries two test drives — teardown owed (operator ruling (e), 2026-10-14).** Steps, only on the operator's word: (1) remove the test apps (Sonarr, Radarr, qBittorrent, Transmission, Plex, Jellyfin) through the dashboard WITHOUT „delete data” (R-1027); (2) decommission Media A and Media B through Tester 1's dashboard; (3) `repoint.py restore` — `controller.yaml` from `controller.yaml.pre-media-2026-10-14`, read back byte-identical, `repo_url` the live catalog; remove `drill-catalog.git` from the controller volume; (4) on demo-hp: `qm set 341 --delete scsi2,scsi3`, then delete the two volumes BY NAME (`nvme-scratch:341/vm-341-disk-3.raw`, `-disk-4.raw`) — never `qm destroy`; (5) VM memory unchanged (8192, not raised); (6) drill repo reset to live `main`. Tools: `audits/media-all-drives-2026-10-14/``tools/`. **Added 2026-10-14 night:** (4b) Media C — `qm set 341 --delete scsi4`, then the volume `nvme-scratch:341/vm-341-disk-5.raw` by name (decommission it on the dashboard first; it holds 4 test files); (7) DONE 2026-10-15: `plex-k6` and its volume removed after the claim (0 left) — the operator removes its server entry „plex-k6” from plex.tv (Settings → Authorized Devices); (8) drill commit `925f3fd` (media_c binds) goes with step 6; Prowlarr and the test drive tag „driva-a” go with step 1. Tools also in `audits/media-drive-events-2026-10-14/tools/`. **Added 2026-10-15 (media build):** (4c) Media D — decommission it on the dashboard, then `qm set 341 --delete scsi5` and the volume `nvme-scratch:341/vm-341-disk-6.raw` by name (20 GB; 2 own-bytes torrents on it); (9) Tester 1 runs controller `0.313.0-rc.3/rc.4` by hand — put the vouched release back (`/etc/felhom-controller-image`); (10) the rig's Sonarr/Radarr hold two sets of drive tags and clients (the night script's `drive-a…` and the product's `media-a…`) — they go with step 1. | **WAITING-ON-OPERATOR** — filed 2026-10-14 2026-10-17 (burndown 3): the drill repo's `main` is now live main + one docmost commit (`9bad48a`, `daff3d2`); the DRILL prowlarr commits stay in its history — re-mirroring Tester 1 would carry that tree. Tester 1 therefore did not take catalog `5d9450b` (Redis RDB). | the operator's teardown word | Run the six steps; quote each read-back | CC |
| **R-1034** | Process & tooling | P3 | **Gate 17 checks the FAQ JSON-LD on two pages only, by a hard-coded list — a FAQPage anywhere else is unchecked.** `scripts/site_gates.py` gate 17 loops `for p in ("gyik.html", os.path.join("en", "faq.html"))`, so its „JSON-LD equals the visible questions and answers” guarantee stops at those two files. **Since 2026-10-14 two more pages carry a FAQPage block the gate never reads:** `/sajat-felho-tarhely` and `/en/own-cloud-storage`. Their JSON-LD was *derived* from the visible headings and paragraphs by a script and then verified verbatim against the rendered text, so it is correct today — but nothing stops the next copy edit moving the visible text and leaving the structured data behind, which is exactly the failure gate 17 exists to catch, and the one Google penalises. **Why it is not a one-line fix:** gate 17's extractor is written for the FAQ pages' accordion markup (`<button class="faq-question">` … `<div class="faq-answer-inner">`); the new pages use plain `<h2>` + `<h3>`/`<p>` sections, so widening the scope needs a second extractor beside the first. **Scope is a fact** (repo `CLAUDE.md`: prefer a walk over a hand-maintained list) — the gate should scan **every page containing a FAQPage block**, not a named pair, or the next page to carry one is unguarded in the same way. A decoy is owed for the widened gate (R-421). | **OPEN** | — | widen gate 17 to every page carrying a FAQPage block, second extractor + decoy | CC |
| **R-987** | Process & tooling | P4 | **The 53 catalog app images have never been scanned for known vulnerabilities.** Fix: a monthly trivy pass over the images the catalog pins, reported, not gating. | OPEN | — | Add to the monthly re-test | CC |
| **R-986** | Process & tooling | P3 | **No dependency or secret scanner runs on any push, so a known-vulnerable library or a committed key is found only by a review like this one.** Proposed gate: govulncheck (called vulns only), gitleaks on the diff, trivy on our own images; start as WARN with an accepted-hits file, then FAIL. Tools and pinned versions: `audits/security-review-2026-10-12/SCANNERS.md`. Note: gitleaks' default rules did not find R-908's key. | OPEN | — | Build the gate, warn-first | CC |
@@ -344,6 +337,7 @@ stopping line that lies.
| **R-1025** | Process & tooling | P4 | **A power drill on demo-hp cuts Tester 1 too, and nothing warns the session using it.** 2026-10-14 17:25 CEST another session cut demo-hp's plug (`plugcut.py --plug hp`, the Immich drill) while this session ran downloads on Tester 1 (VM 341 runs on demo-hp); Tester 1 rebooted and came back by itself. The drill's fence checks Tester 1 for backups and package jobs only. | **OPEN** — filed 2026-10-14 | — | Add „a test runs on Tester 1” to the fence (a marker file the testing session writes), or name the rule in `runbooks/target-selection.md` | CC |
| **R-1039** | Process & tooling | P4 | **qBittorrent stalls on the LAST piece of an archive.org torrent; Transmission does not.** Seen three times on 2026-10-14 (Tester 1 S2 and S3, demo-hp 9201 live read-back): the film reaches 99.994 %, the piece shared with archive.org's side files (`__ia_thumb.jpg`, `_meta.sqlite`, `_meta.xml`) never arrives, qBittorrent's log shows „500 Internal Server Error” from the archive.org web seed, while Transmission finishes the same torrent and curl range requests on the same files answer 206. A household that uses archive.org torrents with qBittorrent hits it too; our tests finish only with a local peer. | **OPEN** — filed 2026-10-14 | — | Note it in qBittorrent's onboarding record; for test films use Transmission, or a public-domain torrent with real peers | CC |
| **R-1078** | Process & tooling | P4 | **A controller comment still describes `mp1 /mnt/sys_drive` as a separate volume.** `C/cmd/controller/main.go:2546` („the SYSTEM-DATA volume (`mp1`, `/mnt/sys_drive`)…”) — `mp1` was removed by R-165; `sys_drive` is a folder of `mp0` (measured on all three boxes, `audits/storage-identity-2026-10-15/README.md`). The same stale cell in `07-backup-architecture.md` §6.1 was corrected in this session. Not fixed here: this run allowed no product-repo change. | READY | — | Correct the comment in the next controller change. | CC |
| **R-1116** | Process & tooling | P4 | **The controller, agent and felhom.eu gate runners do not run their own decoy suites — the shape that let R-1115 fail unseen for a night.** The catalog's runs since 2026-10-17 (`gate-decoys`, full run). Measured today: the controller's suite is green (53 decoys). Found by burndown 3. | OPEN — filed 2026-10-17 | — | Add each repo's `test_gate_decoys.py` as a runner row (fast if it is quick, else the full run), with the count test updated | CC |
<!-- DUE-CHECKS-BEGIN — machine-readable. Parsed by scripts/due_checks_gate.py.
One row per dated check. The R-number must have a row above. Dates are UTC.