security watch sees runtimes (R-1327) and DooPlex (R-1328); HTTP2Shell rows R-1329–R-1332; R-1326 closed
gates / gates (push) Successful in 3m27s

- scripts/image_runtime.py: Node / Python Brotli / brotlicffi / apk / dpkg read from an image's registry config and
  layers (no pull; HEAD for the nightly digest; zstd via the zstd program where present; quay.io, index.docker.io).
- security_watch.py: a „runtime" source (security_watch_runtimes.json), cached by digest in state.json (154 images,
  filled on DooPlex tonight); ghcr.io tag lists are paged; the DooPlex group (own block, homelab next-step).
- dooplex_inventory.py + documentation/security-watch/dooplex-workloads.txt (89 images, homelab-manifests 5aeec8d).
- Tests W10–W12 (58 checks) + test_image_runtime.py (13), each red-proved.
- audits/http2shell-2026-10-27: the Home Assistant lane walk (proven), HOMELAB-LIST.md (19 to update), the run log.
- 08 §11 design line; 09 §3 decision 276 (HA monthly = same main version; decided by CC unattended).
This commit is contained in:
admin committed 2026-10-27 23:23:02 +01:00
1 parent 503692a1da
commit f5a4995da5
21 files changed
+3498 -29

No files matched your search

+6
View File
@@ -26,6 +26,12 @@
---
## 2026-10-27 (late evening) — R-1326 closed: the hub gets time to start (full text of the row: `git show 503692a1:documentation/backlog/OPEN-ITEMS.md`)
| Row | What | Closed | Evidence |
|---|---|---|---|
| **R-1326** | **The hub's start-up eats half its liveness margin: hub 0.174.0 took 36 s from „starting" to „Listening on :8080" (0.174.1: 31 s), and the liveness probe (`initialDelaySeconds` 5, `periodSeconds` 30, `failureThreshold` 3) kills the container after ~65 s of failed probes.** (P3) | CLOSED 2026-10-27 22:22 CET — a `startupProbe` (36 × 5 s = 180 s, 5× the measured 31–36 s start) holds the liveness probe off until the hub answers; `manifests/hub.yaml` `a69ddbde` (CI 2441), deployed on the operator's yes (hub-only sync). The pod started ONCE: „starting" 22:21:47 → „Listening" 22:22:22 (35 s), only the startup probe failed meanwhile, 0 restarts; 30-minute watch: 0 warning-or-worse events, 0 WARN lines, 12 box reports, Synced/Healthy. | `manifests/hub.yaml`; this entry |
## 2026-10-27 (evening) — R-1299 closed: the clean-up window button (full text of the row: `git show 714ee236:documentation/backlog/OPEN-ITEMS.md`)
| Row | What | Closed | Evidence |
+10 -5
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 — 19 rows (P3 8, P4 11)
## Apps & catalog — 23 rows (P2 2, P3 10, P4 11)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -147,6 +147,10 @@ stopping line that lies.
| **R-1215** | Apps & catalog | P4 | **One Immich photo upload hung 10 minutes with no answer and was not stored, while the box was healthy.** Night soak 2026-10-21 (`audits/night-2026-10-21/P1-soak/08-load/proof.log`): cycle 36, `IMG_20120021.jpg` sent 19:38:30 UTC, the uploader gave up at its 600 s read limit; Immich's database holds 302 photos for that user, the other cycles 303 (`P1-soak/09-immich-cycle36.txt`). At that moment memory „full" pressure was 3.9 % and `/api/health` answered in 0.26 s. 1 of about 26 000 uploads that night. Not diagnosed: Immich restarted at 02:16 for the off-site dump, so its log of 19:38 is gone. The Immich phone app retries a failed upload. | WATCHING — **2026-10-22 night:** not reproducible tonight; the Immich server log of 19:38 is gone (Immich restarted at 02:16 for the off-site dump). What the household sees: the upload fails after 10 minutes and the Immich phone app sends it again. | — | Watch the next long upload run with the Immich server log saved; file upstream only with a log | CC |
| **R-1267** | Apps & catalog | P4 | **Every recreate of Immich leaves one empty anonymous Docker volume behind: the image declares `VOLUME /data`, the template does not map it.** Found 2026-10-24 (review round 2, while putting 9202 back): 9202 holds 296 empty anonymous volumes; each night chain's recreate of `immich-server` made a new one (12:58, 13:00, 13:01 — the three test runs; the two dangling ones this session made were removed by name, nothing pruned). The photos are NOT affected: Immich picks `/usr/src/app/upload` whenever both folders exist (`detectMediaLocation` in the image's `server/dist/services/storage.service.js:28-39`), and the template binds that folder (`templates/immich/docker-compose.yml:49`) — NOT-A-FINDING for data placement. **Seen again 2026-10-24 night on Tester 1:** privatebin's image declares `/run`, `/tmp` and `/var/lib/nginx/tmp` as volumes — each recreate (the night dump's stop/start) leaves three empty anonymous volumes; the same class as Immich's `/data` (`audits/restore-holes-2026-10-24/P1-tester1/42-anon-volumes.txt`). | OPEN **2026-10-24 (catalog sweep):** the same class for OnlyOffice and RomM (RomM's image volume `/romm` holds only the empty mount points of its binds); after every catalog app had been removed WITH data, the sweep box still held 52 such volumes (PrivateBin 21, OnlyOffice 8, RomM 8, Immich 3, and 12 from 12:50–12:52 before the batch, holding the same nginx folders as PrivateBin's) — remove-with-data does not see them (no compose project label). All small (4–72 KB), no household data (`audits/catalog-sweep-2026-10-24/_box/anon-volumes.txt`). | — | declare the image's VOLUME paths as tmpfs or named volumes in the catalog (Immich, privatebin), or remove the dangling ones a recreate leaves, by name | CC |
| **R-1268** | Backup & restore | P3 | **Remove with „remove backups” leaves a restore's undo copy on the drive (`backups/restore-undo/<app>/`).** Seen 2026-10-24 night on Tester 1 (restore-holes Part 1, run 1): MeTube was held for support after a failed crash put-back, then removed with `remove_backups=true` — the 1.5 GB copy stayed (the removal deletes `backups/primary` and the second-drive mirror, not `restore-undo`), and nothing lists it (`audits/restore-holes-2026-10-24/P1-tester1/14-run1-teardown.txt`; removed by hand, CC's own test data). The copy is kept on purpose while a hold exists; after the app is gone it is orphaned. | OPEN | — | Remove with `remove_backups` also deletes the app's `restore-undo` folder (under the same path guard), or lists it as a kept copy | CC |
| **R-1329** | Apps & catalog | P3 | **cal.com runs on Node.js 20, which gets no security fixes any more** — v6.2.0 carries Node 20.20.0 (no fixed release on the 20 line for CVE-2026-111497) and Debian 12's unpatched `libbrotli1` 1.0.9-2+b6 (fixed: 1.0.9-2+deb12u1); the same image runs in the catalog and on DooPlex. Read from the image 2026-10-27. | OPEN | — | a cal.com release on Node 22/24 (a big step: its own walk); until then the watch lists it nightly | CC |
| **R-1330** | Apps & catalog | P2 | **RomM's newest release (5.4.1) still ships Node.js 24.18.1 (CVE-2026-111497, fixed 24.22.1)** — no newer RomM exists (tags read 2026-10-27). Exposure: Node is IN the image but no Node process runs (RomM serves through Python gunicorn/rq and nginx — `ps` on demo-hp 9201), and RomM sits behind the setup gate. | OPEN — **CC's pick: keep RomM offered, take the first RomM release with Node ≥ 24.22.1 as a lane step** (decided by CC unattended 2026-10-27 — operator may reverse; STATUS asks) | — | the watch flags it nightly (R-1327); the step when RomM ships | CC |
| **R-1331** | Apps & catalog | P3 | **Paperless-ngx 2.20.15 carries Python Brotli 1.1.0 (CVE-2026-111497, fixed 1.1.1/1.2.1)** — its web side only COMPRESSES (whitenoise, compression_middleware); the DECODERS are urllib3's and httpx's, for replies to requests Paperless itself makes (its own outbound calls), never for what a visitor sends. Read in the running container on demo-hp 9201, 2026-10-27. | OPEN — no step tonight (decided by CC unattended — operator may reverse): the bad path needs a hostile server Paperless calls | — | take the first Paperless release with Brotli ≥ 1.2.1 as a normal step | CC |
| **R-1332** | Apps & catalog | P2 | **Twenty catalog apps ship a runtime with HTTP2Shell (CVE-2026-111497) inside their image — the first runtime read of the whole catalog, 2026-10-27 night (R-1327):** actualbudget 26.9.0 (Node.js 24.20.0 → 24.22.1); adventurelog v0.13.0 (Node.js 26.3.0 → 26.12.1); calcom v6.2.0 (Node.js 20.20.0 → 22.23.4); calibre-web v4.0.8 (py-Brotli 1.2.0 → 1.2.1); dawarich 1.15.3 (Node.js 20.19.2 → 22.23.4); ghost 6.67.0-alpine (Node.js 22.23.3 → 22.23.4); home-assistant 2026.9.4 (py-Brotli 1.2.0 → 1.2.1); homepage v1.13.2 (Node.js 22.22.3 → 22.23.4); immich v3.2.4 (Node.js 24.21.0 → 24.22.1); karakeep 0.33.2 (Node.js 24.18.1 → 24.22.1); metube 2026.09.29 (py-Brotli 1.2.0 → 1.2.1); paperless-ngx 2.20.15 (py-Brotli 1.1.0 → 1.1.1); papra 26.6.2-rootless (Node.js 26.4.0 → 26.12.1); romm 5.4.1 (Node.js 24.18.1 → 24.22.1); seerr 2.7.3 (Node.js 22.18.0 → 22.23.4); sparkyfitness v0.17.3 (Node.js 24.18.0 → 24.22.1); tandoor 2.6.15 (Node.js 24.18.1 → 24.22.1); termix 2.8.0 (Node.js 26.9.0 → 26.12.1); wanderer v0.20.0 (Node.js 22.23.1 → 22.23.4); zipline 4.8.0 (Node.js 24.21.0 → 24.22.1). Plus one BOX program: the file manager `gtstef/filebrowser:1.5.8-stable` carries Alpine `brotli-libs` 1.2.0-r1 (a controller release when upstream rebuilds). None of these has a notice of its own. | OPEN — home-assistant and immich: lane steps walked tonight (drill, the live push waits for the operator); romm: R-1330; calcom: R-1329; paperless: R-1331; the rest: each app's next release whose image carries the fixed runtime, walked as a lane step — the watch lists them every night until then | — | per app: the first upstream image with Node ≥ 22.23.4 / 24.22.1 / 26.12.1 or Brotli ≥ 1.2.1, a lane step; close when the watch's runtime list is empty | CC |
## App updates — 15 rows (P2 3, P3 12)
@@ -293,7 +297,7 @@ stopping line that lies.
| **R-1285** | Box system & updates | P4 | **D43's removal of our SSH drop-in from a BYO host happens one bundle LATE: the bundle run that delivers agent 0.165.0 is planned by the box's OLD (0.164.0) wrapper, which writes the drop-in as before („same"), and only then replaces itself.** Measured 2026-10-25 on the BYO test host (VM 9371 on demo-hp, `Tester4-f967d4`; attended release run): 0.164.0 day-0 → signed `agent_update` + `agent_config_update` 0.165.0 → the planted drop-in still there; a second `agent_config_update` (same bundle) → `BUNDLE REMOVED … (ours, on a BYO host — D43)`, nothing written, `sshd -T` the owner's own `permitrootlogin yes` (`audits/release-2026-10-26/byo/byo-proof.txt`). No box is affected today (the fleet's only BYO host is this test host); a fresh BYO install on ≥ 0.165.0 runs the new wrapper from the start. | **VERIFY** — 2026-10-27: agent v0.167.0 RELEASED; in the NORMAL signed delivery the pass now runs right after the commit on all three boxes („running the bundle policy pass after the self-update commit (R-1285)” → `BUNDLE POLICY DONE install_mode=appliance removed=0`; `audits/release-2026-10-27/agent-0167-tester1-demofelhom.txt`). Not live: a BYO box coming FROM 0.164.0 (none in the fleet). **Earlier:** **VERIFY** — 2026-10-26: agent `b1bc63d` (unreleased) — the new agent runs the bundle-policy pass once right after its self-update commits (`selfupdate.SetOnCommitted`); test in the real order (commit, then one pass), red-proved. **Not live:** only a box coming FROM 0.164.0 discriminates (0.165.0's wrapper already applies D43), which needs a signed downgrade chain on a BYO box — the live proof rides the next agent release. **Earlier:** **OPEN** — 2026-10-26 (agent 0.166.0 released, attended run): the policy pass is wired, but in the normal delivery it does NOT run: the box gets `agent_update` and `agent_config_update` in ONE pass, and the bundle job runs in the OLD process (demo-hp: pid 1966780, agent 0.165.0, bundle 09:03:07–09:03:09 CET; the restart into 0.166.0 at 09:03:10) — no `bundle policy pass` line on any of the three boxes. So a BYO host coming from 0.164.0 still loses our drop-in one release late. The night proof (`audits/night-2026-10-25/rig/r1285-live.txt`) ran a bundle under an agent that was ALREADY 0.166.0-code, so it did not cover this order. No BYO host is in the fleet today. Fix options: the agent runs the pass once at start-up after `update committed`; or the release procedure sends the bundle job only after the commit. **Earlier state:** **VERIFY** — fixed on main 2026-10-25 night (agent `abcc1e6`), waits for the agent release + the vouch: the agent runs the wrapper's `bundle-policy` pass after every install (red-proved, `redproof-r1285.txt`). **Live on the rig in ONE signed run** (BYO host, staged as upgraded from 0.164.0: the new agent by hand, the old 0.164.0 wrapper on disk, our drop-in planted; one `agent_config_update` of a test bundle): the old wrapper installed the bundle and replaced itself, then „BUNDLE POLICY REMOVED … (ours, on a BYO host)", `sshd -T` the owner's own `permitrootlogin yes` (`rig/r1285-live.txt`); the test package deleted from the registry after. **Earlier state:** OPEN | — | either the wrapper plans again after it replaced itself (one agent change + a test that a 0.164.0→0.165.0 bundle on a BYO record removes our copy in ONE run, red first), or the release procedure sends a BYO box's config bundle twice; until then a BYO box upgraded from ≤ 0.164.0 keeps our drop-in until its next bundle | CC |
## Monitoring & notifications — 21 rows (P2 1, P3 13, P4 7)
## Monitoring & notifications — 23 rows (P2 3, P3 13, P4 7)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -318,8 +322,10 @@ stopping line that lies.
| **R-1224** | Monitoring & notifications | P4 | **The operator's 1-hour mail cooldown for `wg_tunnel_stale` is per customer, not per host, and `wg_tunnel_ok` does not end it.** Fresh review 2026-10-23 night (`REPORT-night-2026-10-23.md`, H3): `notify/dispatcher.go:620` keys the cooldown `customerID:eventType`. Two hosts of one household going stale within an hour, or stale → ok → stale, store a second event row but mail once (the suppression is in `notification_log`). | OPEN | — | Key the cooldown by `host_id` from the details for host-scoped operator events, and let the matching ok event end it; a dispatcher test with two hosts; rides a hub deploy | CC |
| **R-1233** | Monitoring & notifications | P4 | **`[ERROR] [stacks] execCommand failed: exit status 1` names no command.** Seen once a night (2026-10-21 00:30:49 and 2026-10-23 02:17:14 UTC, each right after an app stopped for its dump); the command and stderr go only into the returned error, which the caller may drop (`internal/stacks/manager.go:1757`). An ERROR nobody can judge. | OPEN | — | Log the command's first two words (`docker inspect`, never the arguments — `docker exec … psql` can carry a password) and the stderr's first line; find the caller of the nightly one | CC |
| **R-1296** | Monitoring & notifications | P4 | **When ep0's tenancy script refuses, the hub log shows only its one-line error — PBS's own reason (the 403 `missing Datastore.Audit|Datastore.Backup on …`) stays in the script's stderr, which the hub drops on a refusal.** Found 2026-10-26 diagnosing R-1295: the cause was visible only by reading ep0's proxy journal over root SSH. `hub/internal/tenantsync/client.go` `parseResponse` returns the JSON error and ignores stderr; the script keeps secrets off stderr by design (its header), so the tail is safe to log. | OPEN | — | append the last ~3 stderr lines to the refusal error (bounded, never stdout); a test that a secret on stdout never appears | CC |
| **R-1327** | Monitoring & notifications | P2 | **The security watch could not see a bug in the RUNTIME under an app — HTTP2Shell (CVE-2026-111497, the Brotli decoder in Node.js and Python's `brotli`/`brotlicffi`, 2026-10-21) never reaches an app's own notices, so Home Assistant and RomM ran the bad code on the test boxes unseen.** Measured 2026-10-27 from the images (`scripts/image_runtime.py`, registry config + layers, no pull): Home Assistant 2026.9.4 Python Brotli 1.2.0 (2026.10.4: 1.2.1); RomM 5.3.1 and 5.4.1 Node 24.18.1 (fixed 24.22.1); Immich v3.2.4 Node 24.21.0 (v3.3.5: 24.22.1); cal.com v6.2.0 Node 20.20.0; Paperless 2.20.15 Brotli 1.1.0 — each matching a live container where one runs. | **VERIFY** — BUILT 2026-10-27 night (commit only): the watch reads every pin's runtime (`security_watch_runtimes.json`: Node, Python Brotli, brotlicffi, Alpine brotli-libs for our own images), an ADVISORY with source „runtime", cached by image digest in `state.json` (`runtime_cache`) — CI has no zstd, so the cache is filled on DooPlex; a new zstd image in CI is NOT CHECKED with the way to fill it. Tests W10 (W1–W12: 58 checks), red-proved. Not in the table: Debian `libbrotli1` (Debian 13's fixed version not known tonight). Waits for: the first CI night naming Home Assistant and RomM. | — | CI's first nightly run lists HA and RomM as ADVISORY (runtime) and audiobookshelf as not; then close | CC |
| **R-1328** | Monitoring & notifications | P2 | **DooPlex's own workloads were not in the security watch at all** — about ten public apps there carry a bad Brotli/Node (HTTP2Shell), on the host that runs the hub, Gitea, the registry and the backups. | **VERIFY** — BUILT 2026-10-27 night (commit only): `scripts/dooplex_inventory.py` lists every `image:` of homelab-manifests (89 images, `documentation/security-watch/dooplex-workloads.txt`; NOT Helm-chart images — said in the file); the watch reads them as the group „dooplex" (operator mails only, own block, „Next: the homelab session"); an image in the private registry is NOT CHECKED with the reason. Tests W12, red-proved. The homelab session's list: `documentation/audits/http2shell-2026-10-27/HOMELAB-LIST.md`. Felhom changes nothing on DooPlex. | — | the first CI night shows the DooPlex block; the homelab session works its list | CC |
## Hub & operator — 13 rows (P3 4, P4 9)
## Hub & operator — 12 rows (P3 3, P4 9)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -335,7 +341,6 @@ stopping line that lies.
| **R-1056** | Hub & operator | P4 | **Five operator routes fit no token group, so they are session only until the operator places them:** apps reset-telemetry and dismiss-issues, the Configuration asset re-seed, the off-site WireGuard endpoint save, the off-site abandon-cancel (`05` §17.3). Nothing needs them by token today. | WAITING-ON-OPERATOR — filed 2026-10-15 by the token build | operator decision | Place each in a group, or leave session only | operator |
| **R-1164** | Hub & operator | P4 | **Move the website to Hetzner (maybe the JARRs.eu prod server) — an operator idea, no decision asked yet.** If it is done, the website's git-syncs must read the public `https://gitea.dooplex.hu/admin/felhom.eu.git` again: since 2026-10-19 (R-1163) all four read Gitea in-cluster, which works only inside the DooPlex cluster (`01-topology-and-trust.md` §7, `manifests/webpage.yaml`). | OPEN | the operator's idea becoming a decision | When decided: switch the four `--repo` values together (gate 6 keeps them identical); keep `--max-failures=-1` | operator |
| **R-1293** | Hub & operator | P4 | **Once a year the hub's 02:00 database copy never reaches ep0, and the 20:00 copy is pushed twice instead.** Measured 2026-10-26 during R-1183's read (`audits/night-2026-10-25/r1183-measurement-2026-10-26.txt`): on 2026-10-25 (the clocks go back) 02:00–03:00 happens twice. The hub's `nextDailyRun` uses `time.Date`, which picks the SECOND 02:00 (CET, 01:00Z — measured with Go). DooPlex's `felhom-hub-db-backup.timer` fired at the FIRST 02:30 (CEST, 00:31Z) and pushed `hub-20261024T180000Z.db`, 391 min old — under the 7 h limit, so no refusal and no alarm; the 08:30 push took the 08:00 copy. Effect: for one morning a year ep0's newest hub copy is 12 h old, not 6 h; nothing is lost on the hub. The spring night (2027-03-28, 02:00–03:00 does not exist) is not measured. | OPEN | — | pick one: the push refuses (and says so) a newest snapshot older than the slot it serves, or the hub and the timer agree on one 02:00; then read the 2027-03-28 night (DUE-CHECKS) | CC |
| **R-1326** | Hub & operator | P3 | **The hub's start-up eats half its liveness margin: hub 0.174.0 took 36 s from „starting" to „Listening on :8080" (0.174.1: 31 s), and the liveness probe (`initialDelaySeconds` 5, `periodSeconds` 30, `failureThreshold` 3) kills the container after ~65 s of failed probes.** Seen 2026-10-27 at both deploys: two `Liveness probe failed … connection refused` events at start, then ready. The hub's database keeps growing (559 MB on 2026-10-27 04:05); a start-up past ~65 s would restart the hub in a loop at every deploy. No startup probe is set. | OPEN | — | Measure what the start spends its 30 s on (DB open, migrations, checkers); then a `startupProbe` (e.g. 10 s × 18) in `manifests/hub.yaml`, or move the slow work after `Listen` | CC |
## Business & legal — 14 rows (P2 4, P3 2, P4 8)
@@ -364,7 +369,7 @@ stopping line that lies.
| **R-1240** | Process & tooling | P4 | **Scratch guest 9202 holds 34 saved copies of its `controller.yaml`, each carrying the catalog's git token** (`controller.yaml.pre-<session>`, 2026-09-21 → 2026-10-17, root 0600, inside the guest's controller volume). Every drill session that repointed 9202 saved one and none removed it after its byte-identical restore. Found 2026-10-23 (the D39 session removed its own after `cmp` proved it identical). Not read: whether the token in them is still live (R-925 rotated tokens). | **OPEN** | — | read which token the copies carry (by hash, never printed); if the live one, delete the copies; add „remove the save after an identical restore" to `repoint.py restore` | 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-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-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** — 2026-10-27 evidence: HTTP2Shell (CVE-2026-111497) sat in the RUNTIME of five catalog images (Home Assistant, RomM, Immich, cal.com, Paperless) and no notice of theirs said so; the nightly watch now reads each image's runtime (R-1327) — a full scan of every package is still this row. **Earlier:** OPEN | — | Add to the monthly re-test | CC |
| **R-733** | Process & tooling | P3 | **[P3-LOW] The test bench has NO swap and the boxes have 512 MiB — so a box proof can pass on swap where the bench fails, and nobody records whether a customer guest has swap.** MEASURED 2026-09-30 (R-732): immich's first start was OOM-killed 61–104 times on the bench (swap 0) and passed on 9202 by swapping ~108 MB; the bench given 512 MiB swap passed too. demo-hp 9201, 9202 and demo-felhom 9201 all read `swap: 512`; the golden's guest config is not recorded in its bake evidence, so a customer guest's swap is NOT measured. The harness's memory watch judges `anon` against the limit and never reads `memory.swap.current`. **Needs:** the golden's `swap` read and recorded; the box walk and the harness report `memory.swap.peak` beside `anon`; a decision whether proofs run with swap off (the stricter venue, as R-732's fix was proven). | **READY — rank P3-LOW; owner: CC (harness + golden evidence)** **2026-10-06 night: NARROWED** — the harness records each container's `swap_peak` and the venue's swap (catalog branch `night-held-2026-10-06`, `3f4611c`; SwapRecorded red-proved; bench SwapTotal 0 kB, 9202 524288 kB). LEFT: the golden's swap in its bake evidence, the box walk's record, and whether proofs run with swap off. | — | — | CC |
| **R-902** | Process & tooling | P3 | **The contact form's mailer has no source in any repository: the running image cannot be rebuilt or changed.** FOUND 2026-10-08 (website refresh, brief Part C5). `manifests/contact-mailer.yaml` names `contact-mailer:latest` with `imagePullPolicy: Never` (imported into k3s by hand); the binary in the pod is `/app/contact-mailer` dated 2026-02-05, Go module `github.com/felhom/contact-mailer` (devel). Searched: `git log --all` of felhom.eu (only the manifest), every repo under `/mnt/5_hdd/felhom.eu/git`, `find /mnt/5_hdd/felhom.eu ~ -maxdepth 5 -iname '*contact-mailer*'`. READ from the binary (strings, read-only `kubectl exec … cat`): ONE send per form (`sendViaResend`) to `TO_EMAIL`, `reply_to` = the visitor; nothing is sent back to the visitor; the operator's notification is Hungarian (`buildEmailHTML`, „Csatolmány … fájl"). The English form needs no mailer change (its subject labels carry „(EN)"). `audits/website-refresh-2026-10-08/mailer.md` | **OPEN — NARROWED 2026-10-08 (search done, NOT FOUND): owner: operator.** A read-only search found no source on DooPlex (55,548 `*.go`/`go.mod`/`Dockerfile*` files, every disk, media/Longhorn/containerd/PBS excluded by name), in the web editor's storage and file history, in all 10 Gitea repositories' history, or in Docker's build cache (oldest record 2026-09-28). The binary's own clues: one file `/build/main.go`, go1.23.12, no `vcs.*` stamp; image built 2026-02-05 10:40 by BuildKit. The binary is now KEPT in git (`audits/mailer-source-2026-10-08/contact-mailer.bin`), so a DooPlex rebuild no longer loses the program. A replacement plan is written (`audits/mailer-source-2026-10-08/PLAN-replacement.md`); one question left: is the February folder on the operator's Windows workstation? | — | Operator: look on the workstation; if absent, build the replacement from the plan (attended deploy) | operator |
| **R-935** | Process & tooling | P3 | **Something sets `core.bare = true` on the shared `felhom.eu` checkout, and then every git command there fails („this operation must be run in a work tree").** SEEN 2026-10-12 evening twice: `.git/config` changed at 16:29:37 and 16:37:33 CEST, each time with `core.bare=true`; restored by hand both times (`git config core.bare false`, a flag only, no file touched). **Not caused by the gates:** `repo_gates.py --fast` run plainly and with `GIT_DIR=.git` (as a hook gets it) left the flag `false`; a scratch `git init --bare <path>` under `GIT_DIR` does not flip the parent either. The first flip came while this session did not touch the repo; another session pushed to main at 16:44 from elsewhere. | **OPEN** — 2026-10-12 later: across this session's next push the flag stayed `false` (read before and after); not this session's pushes. **OPEN** — NEW 2026-10-12 **Note 2026-10-12 (security review):** at least some of these flips match the security review's pushes from a git worktree of this clone (about 16:29 and 16:37): inside the pre-push hook, `scripts/test_gate_decoys.py`'s poster-facts case runs `git init`/`git config`/`git commit` with the hook's inherited `GIT_DIR`, so it wrote into the real repository — four test commits in the worktree and `user.name d` / `user.email d@d` in the shared `.git/config` (the review removed that `[user]` section at about 16:50; commits 6c2c3dd5 and 186f3aa3 carry that author). The review stopped pushing from the worktree. Fix: the test helper must drop `GIT_DIR`, `GIT_WORK_TREE` and `GIT_INDEX_FILE` from its environment. | finding the writer | Watch `.git/config` (an inotify or audit rule on the file) during a day with the parallel sessions; name the command; fix it there | CC |