D44/D45 = all picks applied (R-1265, R-1266 closed; R-1280–R-1283 opened); R-232 dated check done; release-2026-10-26 evidence: fix A/B live on rc.6, D47 set-aside alarm on rc.7
gates / gates (push) Successful in 13m3s

This commit is contained in:
admin committed 2026-10-25 17:57:11 +01:00
1 parent a86ab64f6c
commit ade357a14e
38 files changed
+4907 -58

No files matched your search

@@ -23,8 +23,10 @@ records *Felhom's decisions*, not Proxmox behaviour.
## 1. Model at a glance
Three components. **Control is always box-initiated** — the hub never connects *into* a
customer box.
Three components. **Control is always box-initiated** — the hub never ~~connects~~ sends a command *into* a
customer box. **[DESIGN — corrected 2026-10-25, D45 = all picks, R-1266]** The hub may POKE a box: through ep0, one empty UDP datagram to the box's WireGuard
address (port 51822; `hub/internal/poke/client.go`, agent `internal/poke/poke.go`). A poke carries no command; it only
makes the box pull its desired state at once. The box still pulls.
```
operator side customer box (per Proxmox host)
@@ -98,8 +100,12 @@ credentials.
- CGNAT does **not** force this: the Cloudflare Tunnel already makes a box reachable through
Cloudflare's edge. We *choose* box-initiated control for the smallest attack surface — the
box exposes no control endpoint at all.
- The agent and the controller **poll** the hub; the hub never initiates inbound.
box exposes no control endpoint ~~at all~~: no route on the box takes a command from the hub. **[DESIGN — corrected 2026-10-25, D45 = all picks, R-1266]**
Two listeners sit behind the WireGuard tunnel: the agent's poke listener (a poke carries no command; the box still
pulls) and the operator door, `felhom-sshd` on the host, reachable only from the operator's `/32` peer — the
documented break-glass (`03` §5, `06` §4.5).
- The agent and the controller **poll** the hub; the hub never initiates ~~inbound~~ a command inbound — a poke only
makes them poll at once (corrected 2026-10-25, D45, R-1266).
- Operator actions are delivered as **signed jobs**: the agent verifies an operator signature
before executing, so a compromised hub database alone cannot forge commands.
- **[FACT, 2026-10-16, R-970]** The last contradiction is gone: until controller v0.322.0 the box's own hub API key,
@@ -245,7 +251,8 @@ app. Controller down → the gated app answers an error, never the app. Measured
2026-10-08, R-900). Replacing it with our own relay is deferred until after the first customers (R-904, `09` §3
decision 184).
- **Outbound only** for control/report/backup (poll to hub, push to PBS). No inbound control
endpoint exists in the chosen model.
endpoint exists in the chosen model. **[DESIGN — corrected 2026-10-25, D45 = all picks, R-1266]** A poke carries no command; the box still pulls. The operator
door (`felhom-sshd` over the WireGuard tunnel) is the documented break-glass (§4, `03` §5).
- **Every customer has their OWN domain — never a name under `felhom.eu`** (operator ruling
2026-09-14). The free Cloudflare tier's certificate covers one level below a zone, so nested names
such as `felhom.<customer>.felhom.eu` are not covered; the customer's own domain is a few thousand
@@ -330,7 +337,12 @@ app. Controller down → the gated app answers an error, never the app. Measured
- **Key custody (zero-knowledge default):** three tiers the customer chooses —
*customer-only* / *zero-knowledge escrow (default)* / *operator-managed*. Default escrows
the **PBS passphrase-protected keyfile** in the hub, wrapped under a **customer recovery
code** the operator can't open; DR needs the customer's code. Access-notification is an
code**; DR needs the customer's code. ~~the operator can't open~~ **[DESIGN — corrected 2026-10-25, D45 = all picks, R-1266]** The hub keeps only the
locked key package, and every older one it retained; without the customer's code the operator cannot open any of
them, and from the off-site storage and the hub nobody can read the backup. Two exact limits, as the website says
since R-985: while the box runs, the operator has root access for support and with it could reach the data and the
backup key on the box; and the operator can open an OLDER off-site store from a retained package, by hand, when the
household types its old code on its own box (`runbooks/RUNBOOK-open-retained-package.md`, R-304). Access-notification is an
audit signal, never the primary guard. (Don't build bespoke crypto — use PBS's native
keyfile passphrase.)
@@ -355,8 +367,11 @@ app. Controller down → the gated app answers an error, never the app. Measured
## 10. How this embodies the product values
- **Zero-knowledge offsite** — the operator holds the offsite backup but cannot read it.
- **Box-initiated control + signed jobs** — no standing operator backdoor; a hub compromise
- **Zero-knowledge offsite** — the operator holds the offsite backup but cannot read it without the customer's code
(the two exact limits are in §8 — corrected 2026-10-25, D45, R-1266).
- **Box-initiated control + signed jobs** — ~~no standing operator backdoor~~ no hidden operator door: the one
operator door is `felhom-sshd` over the WireGuard tunnel, the documented break-glass (`03` §5), and the website tells
the household about the operator's root access (R-985) — corrected 2026-10-25, D45, R-1266; a hub compromise
alone can't forge commands.
- **Customer-visible audit log** — every operator action is visible to the customer.
- **Never hold data hostage** — subscriptions cover ongoing labour (monitoring, offsite,
+47 -9
View File
@@ -64,7 +64,8 @@ Explicitly does **not**:
- Serve application traffic or sit in the data path. **Control plane, not data plane**: if the agent dies, apps keep serving (Docker + LXC run without it); only *management* degrades — no new backups, no provisioning, hub loses the heartbeat.
- Hold or proxy customer application data.
- Run inside a guest. It is the thing that recovers guests and the host; it cannot be one of them.
- Manage **geo-restriction / the Cloudflare API**. Geo is hub-owned: the customer sets it in the controller UI, the controller reports the geo desired-state to the hub, and the **hub** (holding the CF API token) reconciles the WAF (S4). The agent manages only the *tunnel* service (`cloudflared`, §3/§5), never WAF rules.
- Manage **geo-restriction / the Cloudflare API**. Geo is hub-owned: the customer sets it in the controller UI, the controller reports the geo desired-state to the hub, and the **hub** (holding the CF API token) reconciles the WAF (S4). ~~The agent manages only the *tunnel* service (`cloudflared`, §3/§5), never WAF rules.~~ The agent manages no
Cloudflare piece: it only reads the guest's `cloudflared` container (§3) — corrected 2026-10-25, D45, R-1266.
## 3. Process model & host integration
@@ -240,10 +241,16 @@ alerts the operator when an agent misses its expected check-in window. This is t
failure mode for a managed service, so it gets first-class treatment hub-side.
**Break-glass.** Standing inbound control is off. But when the poll loop *itself* is wedged
(agent hung, host sick) you cannot fix it through the poll loop. So there is an explicit,
**off-by-default, customer-consented, fully-audited** emergency path: SSH to the host via
(agent hung, host sick) you cannot fix it through the poll loop. So there is an explicit emergency path.
~~**off-by-default, customer-consented, fully-audited** emergency path: SSH to the host via
the Cloudflare Tunnel behind Cloudflare Access (or on-site). Enabling it is itself a signed,
logged operation; it auto-expires.
logged operation; it auto-expires.~~ **[DESIGN — corrected 2026-10-25, D45 = all picks, R-1266]** The path is the operator door: the operator's WireGuard
peer reaches a dedicated `felhom-sshd` on the host (a non-22 port, reachable only from the operator's `/32` over
`wg-felhom`, key-only login; `06` §4.5, TASK H1). It does not use the Cloudflare Tunnel. It is installed by default on
an appliance install, and a BYO install refuses it (the owner's call; `scripts/felhom-host-install.sh`). It stays on;
it is not opened per incident. When SSH itself is broken, the hub-vaulted `root@pam` password opens the Proxmox web
console (`runbooks/break-glass.md`). The website tells the household that the operator has root access for support
(R-985).
## 6. Agent ↔ controller local API
@@ -315,11 +322,11 @@ A controller can only `POST /rollback` (or snapshot/backup) **its own** guest
token → guest and authorizes per guest, so a compromised controller's blast radius is
**self-scoped and bounded** to its own guest.
**[DESIGN — undocumented, found 2026-10-24, review round 2 — confirmed through R-1265]:** the surface listed above is
the slice-8 design; on `main` the local API registers **41** routes (`felhom-agent/internal/localapi/server.go`; 24 of
them are not listed above — the backup tiers and target, disk candidates, decommission, format status, guest attach,
the network storage routes, the controller swap, the escrow stage/preflight/ceremony routes, the debug logs, guest
memory). Among them the controller can start a HOST action: `POST /host/line-test/run` (about 55 s; bounded by the due
**[DESIGN — confirmed by the operator 2026-10-25 (D44 = all picks, R-1265); every route listed below since
2026-10-25]:** the surface listed above is the slice-8 design; on `main` the local API registers **41** routes
(`felhom-agent/internal/localapi/server.go:562-634`). The list above names 15 of them; the other 26 are in the table
below (recounted 2026-10-25 — the review counted 24). Every route is behind the same per-guest token (`withGuest`).
Among them the controller can start a HOST action: `POST /host/line-test/run` (about 55 s; bounded by the due
date, the window, 6 tries a day and the heavy-operation gate — R-1250, unreleased). The local API also listens on the
R-50 island bridge (host `169.254.253.1`, guest `net1` `169.254.253.2/30`, `internal/config/config.go:260-269`). The
weekly line test keeps its state in the OOB state dir (`line-test.json`) and has an off switch, `line_test.disable`.
@@ -327,6 +334,37 @@ The served WireGuard block carries `oob_peer_ip` and `oob_operator_ssh_key` (`in
runs only inside the tunnel watchdog, and its 15-minute count lives in memory — an agent restart starts it again (the
once-an-hour bound survives in `wg/last-rekey`).
The routes the list above does not name (read from `server.go:562-634` and each handler's own comment, 2026-10-25):
| Route | What it does |
|---|---|
| `GET /backup/tiers` | this guest's backup tiers, each with its cadence and whether its Proxmox storage exists now (`present` / `absent` / `unknown`) |
| `POST /backup/target` | moves the primary backup tier to a new storage: create → grant → config; answers `restart_required`, and the caller restarts the agent after its own in-flight check |
| `GET /host/line-test` | HOST: the weekly upload line test's last measurement; `present:false` when never measured (R-1247) |
| `POST /host/line-test/run` | HOST: starts a line test now — about 55 s, bounded as above (R-1250) |
| `GET /disks/candidates` | the host disks free for Felhom to enroll (`initialize`, and `attach` for those with a mountable file system) |
| `POST /disks/same-disk` | whether two of the guest's paths sit on the same physical disk, from the agent's own read (`16` §4) |
| `POST /disks/decommission` | takes the household's own drive out for good: a permanent intent, the guest bind pruned, unmounted; never formats; a system or backup mount is refused (403) |
| `GET /disks/format/status` | the newest or running format job, for a caller whose request timed out |
| `POST /disks/guest-attach` | binds an enrolled drive's `felhom-data` folder into this guest (read-write); the rest of the drive stays hidden |
| `GET /guest/memory` | this guest's RAM and its bounds |
| `POST /guest/memory` | a bounded RAM resize, applied live (R-24) |
| `POST /netstorage/add` | mounts a NAS share on the host and starts a verify job; a failed verify rolls it back |
| `GET /netstorage` | the NAS shares |
| `GET /netstorage/verify-status` | the verify job's state |
| `POST /netstorage/remove` | removes a NAS share |
| `POST /controller/swap` | swaps the controller image (202, then async); the agent verifies and rolls back |
| `GET /controller/swap/status` | the swap's state |
| `POST /escrow/stage-secret` | stages the off-site repository password for the escrow ceremony (0600, never logged) |
| `DELETE /escrow/stage-secret` | wipes the staged password |
| `POST /escrow/recover-offsite-password` | with the recovery code in the body, returns only the off-site repository password (§8a) |
| `GET /escrow/preflight` | the ceremony's checklist |
| `POST /escrow/ceremony` | starts the root ceremony job |
| `GET /escrow/ceremony/status` | the ceremony's state |
| `POST /escrow/ceremony/claim` | the one-shot hand-off of the new recovery code; a later claim is 410 |
| `POST /escrow/ceremony/confirm` | the household's two words; only then does the package reach the hub (R-1151) |
| `GET /debug/logs` | the agent's debug log ring, for the controller's Debug page |
### 6a. Implementation (slice 8A — implemented)
**Status: implemented** (agent v0.10.0 `internal/localapi`; controller v0.35.0 `internal/bootstrap`
@@ -77,10 +77,10 @@ nonce (DoS-safe). Each layer is mandatory and was proven to reject independently
A cannot be replayed at box B);
- **time window** — `now ∈ [issued_at, expires_at]`; as built, `issued_at` may be up to **2 minutes** in the future
(clock skew), `expires_at` is never extended — the agent (`felhom-agent/internal/authz/verifier.go:19-22,148-155`) and
the root wrapper (`configs/felhom-os-apply`) alike (corrected 2026-10-24). **[DESIGN — undocumented, found
2026-10-24]:** neither caps the window's LENGTH (`expires_at − issued_at`); `felhom-opsign` defaults to 30 min
(`cmd/felhom-opsign/main.go:57`), and the nonce store keeps a record until its expiry. A register row asks the
operator to confirm or cap it;
the root wrapper (`configs/felhom-os-apply`) alike (corrected 2026-10-24). **[DESIGN — the operator picked a code change
2026-10-25 (D44); row R-1280]:** neither caps the window's LENGTH (`expires_at − issued_at`); `felhom-opsign` defaults to 30 min
(`cmd/felhom-opsign/main.go:57`), and the nonce store keeps a record until its expiry. The pick: the agent and the
root wrapper refuse a window longer than 24 h. Not built yet;
- **nonce** — unseen within the window (the nonce store **must be persistent across agent restarts**
and expiry-pruned; a non-persistent store reopens the replay window after a restart).
@@ -330,12 +330,16 @@ traverse `/root`) — create datastores under a world-traversable parent (`/srv`
## 6. Trust-model conformance (01 §3/§4 preserved)
- The box **only ever dials out** (UDP to the endpoint; the same posture as hub-poll and
PBS-push, 01 §4/§7). No inbound listener is added to any customer box; the hub still never
initiates into a box.
PBS-push, 01 §4/§7). ~~No inbound listener is added to any customer box; the hub still never
initiates into a box.~~ **[DESIGN — corrected 2026-10-25, D45 = all picks, R-1266]** Two listeners sit on the box's tunnel side, reachable only through
the tunnel: the agent's poke listener (UDP 51822 on the box's WireGuard address — a poke carries no command, the box
still pulls) and, on an appliance install, the operator door `felhom-sshd` (only from the operator's `/32`, §4.5) —
the documented break-glass (`03` §5). The hub still never sends a command into a box.
- The endpoint VM cannot reach into boxes either: it terminates tunnels whose per-peer routes
(§4.5) admit only box→PBS traffic; it holds no box credentials.
- **Out of scope, explicitly:** (a) operator-inbound access to boxes — a *separate* future
problem with its own trust decision; nothing here creates or presumes it; (b) game-server
- **Out of scope, explicitly:** (a) ~~operator-inbound access to boxes — a *separate* future
problem with its own trust decision; nothing here creates or presumes it;~~ operator-inbound access to boxes was
added on 2026-07-05 (TASK H1) and rides this tunnel — §4.5 (corrected 2026-10-25, D45, R-1266); (b) game-server
ingress — public players are not tunnel peers; that is a different ingress problem (public
reachability), not a backup-transport one.
@@ -387,7 +391,7 @@ venue; it gates nothing. Runbook:
**MVP that puts offsite backup in a real customer's hands = S1 → S2 → S3; then S4.** S5/S6
complete the story. The cloudflared service (`internal/hub/cloudflared.go:9-12` — then a
read-only prober that named itself "the seam for the tunnel-management slice") adopts the
S3-piloted agent-managed-service pattern in a later, separate slice. *(Dated note 2026-10-24: cloudflared is a GUEST
container the agent only reads — four states, `cloudflared.go:12-23`; `03` §2 says the agent will not manage it.
Whether this plan is withdrawn is the operator's — a row of review round 2.)*
read-only prober that named itself "the seam for the tunnel-management slice") ~~adopts the
S3-piloted agent-managed-service pattern in a later, separate slice.~~ **[DESIGN — corrected 2026-10-25, D45 = all picks, R-1266]** This plan is withdrawn:
cloudflared is a GUEST container that the controller renders and keeps up; the agent only reads it (four states,
`cloudflared.go:12-23`) and will not manage it — `03` §2/§3 wins.
@@ -1561,7 +1561,8 @@ crosses the line — **R-158**.
> design, **row 14** is proven and merely never stopwatched, **row 11b** is a note not a row, and
> **row 15 was an open DEFECT (R-104) that this line did NOT cover** — *R-104 was CLOSED 2026-10-05 (controller
v0.297.0: a lock that survives the self-heal gets the class `locked`, its own note and a retry, `offbox.go:156-162`;
CLOSED-ITEMS.md; corrected 2026-10-24).*
CLOSED-ITEMS.md; corrected 2026-10-24).* **Row 16** (added 2026-10-25, D44 #6, R-1265 — after this banner) has a blank RTO
too: a registry rebuild from the ep0 release copy was never timed.
>
> **NO STATUS MOVED ON THE DAY THIS WAS WRITTEN, because nothing was proven that day.** A stopping
> line that promotes a row is a stopping line that lies. Full text, and the conditions that reopen
@@ -1601,6 +1602,7 @@ CLOSED-ITEMS.md; corrected 2026-10-24).*
| 13 | **Customer loses R** | every byte, every tier, the Recipe | **on-premises recovery is unaffected** — Tier-1/2/3 and the whole-guest tiers all work while the guest and host live. What is lost: the ability to re-establish **host identity** and to open the escrowed offsite key material after a host loss | — | | | **NONE for host-loss** | R exists in **zero** system copies by design (INV Part C, row 24). Whether the operator should be able to recover is **open** → §11-A, §11-B. **D5 narrows this further:** Tier-1/2 app recovery needs only the drive, so losing R now costs the offsite route and host identity, never local app recovery (§7.4) |
| 14 | **Host SSH management plane dies** | everything | 3-layer break-glass: tmpfiles → 60 s watchdog → hub-vaulted `root@pam` on the PVE console | operator | | | **PROVEN** | `runbooks/break-glass.md`; the original incident and its fix |
| 15 | **Interrupted offsite run leaves a stale lock** | everything | manual `restic unlock --remove-all` | **operator** (SSH) | | | **FIXED 2026-10-05 (R-104, v0.297.0; was DEFECT — corrected 2026-10-24)** | the self-heal exists (`offbox.go:634-648`) but the probe fails first and `classifyResticProbe` has no lock case → fail-fast, operator told *„ismeretlen okból"* → **R-104** |
| 16 | **The container registry is lost with DooPlex** (the released images and agent packages) | the release copy on ep0 (D33): the newest 3 release tags of `felhom-controller`, `felhom-hub` and `felhom-samba`, and the newest 5 agent packages (binary + config bundle), in a plain folder outside PBS (`/srv/felhom-release-copy`) | push the copy back into a registry with the same name — `runbooks/total-loss-of-dooplex.md` „Restore the release copy” (same digests, no rebuild; `scripts/release-copy/`) | **operator** (ep0 root) | | 24 h (nightly 01:40) | **PARTIAL** | `audits/day-2026-10-23/08-release-copy-D33.txt`: the copy was pulled back, every sha256 and digest matched, and a controller image from it ran on a scratch box. Not yet walked: the push into a real new registry and the agent-package upload. Added 2026-10-25 (D44 = all picks, R-1265) |
### 8.2 The ONE exception to row 5's derived-copy rule (R-403, controller v0.230.0)
@@ -1740,7 +1742,7 @@ put-back leaves (§8 D20/D22), so no new retention rule. A watcher looks 30 more
turns to success („…elindult: a visszaállítás kész.") and the copy is removed, as after any successful restore; if it
never runs, the copy stays for support.
### 8.2c Decisions in code that no document recorded [DESIGN — undocumented, found 2026-10-24, review round 2 — the operator confirms them through one register row]
### 8.2c Decisions in code that no document recorded [DESIGN — confirmed by the operator 2026-10-25 (D44 = all picks, R-1265)]
Read at `file:line` on `main` (controller `321df1f`, hub `44d19042`) by the main session:
@@ -1756,8 +1758,9 @@ Read at `file:line` on `main` (controller `321df1f`, hub `44d19042`) by the main
a box that declares it waits for the household's recovery code (`awaiting_recovery_key`) is skipped — no event, no
heal (R-1230, `:103-110`).
- **The release copy (D33)** — nightly 01:40, the newest 3 image tags and 5 agent packages to ep0 under its own
limited user, capped by the sender (`felhom.eu/scripts/release-copy/`) — has no row in this matrix; its homes are
the poster facts and `runbooks/total-loss-of-dooplex.md`.
limited user, capped by the sender (`felhom.eu/scripts/release-copy/`). ~~has no row in this matrix~~ It is row
**16** of the matrix since 2026-10-25 (D44 = all picks, R-1265); its other homes are the poster facts and
`runbooks/total-loss-of-dooplex.md`.
### 8.1 The blank cells, listed explicitly
@@ -1776,6 +1779,7 @@ Per the rule that a blank is a finding, here they are:
| 13 | RTO, RPO | not a timed recovery; a capability loss |
| 14 | RTO | the break-glass path is proven but was never timed |
| 15 | RTO | the manual unlock was performed but not timed |
| 16 | RTO | a registry rebuild from the ep0 release copy has never been timed (row added 2026-10-25, D44 #6, R-1265) |
**Also unmeasured, and not representable as a row** (INV Part F.3): any restore larger than 155.5 MB
from restic; any restore of a customer data drive (no tier holds one whole); the weekly PBS
@@ -286,8 +286,11 @@ builds them (`audits/update-rulings-2026-09-23/`).
13. **The test decides, not the tag** (Q3). **REPLACES decision 3's second half, "never across a
major".** The box applies by itself every step the catalog holds, because **the catalog holds only
tested steps**; the size of the version jump does not matter, the test does. The box does not
parse tags to decide. The tag rule (`stacks.CompareImageRefs`) moves to the **catalog gate** as a
push-time safety net: an image move with no test record is refused there. Two exceptions, both
parse tags to decide. ~~The tag rule (`stacks.CompareImageRefs`) moves to the **catalog gate** as a
push-time safety net:~~ **[DESIGN — corrected 2026-10-25, D45 = all picks, R-1266]** The tag rule (`stacks.CompareImageRefs`) did not move: it
stays in the controller (`internal/stacks/updateorder.go`), where it only tells whether an installed app is
AHEAD of the catalog; it never decides an update. The catalog gate is the push-time safety net, and it asks for a
proven test instead (§6.4 part 4): an image move with no test record is refused there. Two exceptions, both
**marks the test sets on the step**: *files may change* → automatic only when a fresh copy holds
the files, else a person (this is Q2's refinement); *needs a person* → the tester writes why.
**Why:** decision 3 was a proxy. "Within a major" was standing in for "known to work", and the
@@ -299,7 +302,11 @@ builds them (`audits/update-rulings-2026-09-23/`).
unchanged for every other move: a step a box takes was still tested, on at least the box venue.
14. **The update ladder** (Q3). A box more than one step behind **climbs one tested step at a time, in
order, and never jumps**. Each step is the full guarded update of §6.1. **Why:** a step was tested
order, and ~~never jumps~~ the night leg never jumps**. Each step is the full guarded update of §6.1.
**[DESIGN — corrected 2026-10-25, D45 = all picks, R-1266]** An app older than the ladder (its installed version matches no entry's `from`) is never
pressed by the night leg (`internal/stacks/unattended.go`). A PERSON's press on such an app takes the catalog's
current definition, logged by name (`internal/stacks/ladder.go`, `TestLadder_UnknownInstalledJumpsAndSaysSo`); the
guarded update and its undo (decision 15) still run around it. **Why:** a step was tested
from the version before it, not from three versions before it; R-40's multi-major jump is what
this prevents. This is the "stepping" §6.1 already assigned to slice 6.
@@ -988,8 +995,11 @@ its length, and both fixes cost something the household would notice — operato
installs the vouched golden and updates itself to the floor (not yet proven end to end — R-1052). **The golden
stays weekly:** a CHANGED golden selection is still refused when it is older than the fleet's newest controller
(`TestVouchGoldenChangedToOlderIsStillRefused`).
5. The push gate judges the golden by its AGE (older than 7 days while the floor is above it), not by the newest
CHANGELOG heading (`scripts/golden_currency_gate.py`).
5. The push gate judges the golden by its AGE, not by the newest CHANGELOG heading
(`scripts/golden_currency_gate.py`). **[DESIGN — corrected 2026-10-25, D45 = all picks, R-1266]** ~~(older than 7 days while the floor is above
it)~~ The gate runs offline and cannot read the floor, so „a release is ahead of the newest bake” stands in for
„the floor is above it”: it convicts when a release is ahead AND the newest bake is more than 7 days old; ahead
and at most 7 days old is an advisory. The floor follows a release within hours, so the gate is the stricter one.
251. **Scripts use scoped hub tokens, not the operator password; CC's floor step uses its own.** *Operator ruling
2026-10-15 11:38.* Tokens with groups (read/write per group), managed on the Configuration tab, shown once, stored
hashed, every use logged by name (`05` §17, hub v0.152.0). Never by any token: a secret shown or minted, a delete or
@@ -2570,7 +2580,7 @@ and E are the ones that unblock a decision; leg A is the one that takes the time
harness on DooPlex for the image-side ones.
### 6.4.x Decisions in code no section records [DESIGN — undocumented, found 2026-10-24, review round 2 — the operator confirms them through one register row]
### 6.4.x Decisions in code no section records [DESIGN — confirmed by the operator 2026-10-25 (D44 = all picks, R-1265)]
Read at `file:line` on controller `main` (`321df1f`) by the main session:
@@ -78,12 +78,13 @@ OS packages (exact versions, apt-signed), the installer script (by tag, R-980).
| **added:** catalog ↔ every box | **design gap** | a catalog push reaches every box with no check on the box (R-971). **Re-check (2026-10-24, review round 2):** the box checks every rendered compose against its allow-list before any write since controller v0.328.0 (`internal/privpolicy/privpolicy.go`) |
| **added 2026-10-23 (R-1241):** a hacked app pod on DooPlex ↔ Felhom's secrets | **does not hold**; not used in the Plex2Shell window | two no-login doors: the Longhorn NFS export (the hub's unencrypted nightly copies with the unsealed tokens of R-975; by a published file-handle escape the whole sda1 disk, the operator signing keys included — not exercised) and the Longhorn API (backup a volume to any target). Plex2Shell, 2026-10-17 → 10-23: **NOT REACHED** — 0 new NFS connections in the window, no off-schedule backup or volume; the hub, the k8s API and the kubelet refuse a caller with no credential (measured). The export was narrowed to the pod network on 2026-10-23; the pod side stays open (`audits/plex2shell-felhom-impact-2026-10-23/`). **Felhom half, 2026-10-23 night:** the last plain tokens sealed in hub 0.168.0 (deployed 2026-10-24 — corrected 2026-10-24; `audits/hub-token-seal-2026-10-23/`); the two plain Longhorn copies of the hub volume already in the folder are R-1243's decision. A household box's version of this attacker (a hacked app sending or mining) is what the upload watch (R-1244, released 2026-10-24 in controller v0.342.0 + hub v0.169.0 — corrected 2026-10-24) detects |
**[DESIGN — undocumented in `01` §5, found 2026-10-24, review round 2 — confirmed through R-1265]:** the file manager's one
**[DESIGN — the operator picked a code change 2026-10-25 (D44); row R-1281]:** the file manager's one
sign-in (R-1100) — FileBrowser trusts its proxy header blindly, and every app container can reach `filebrowser:80`
directly; the only guard is that the header NAME is a per-box secret, which traefik's forwardAuth strips from every
client request and sets only for a browser holding the dashboard's files pass
(`felhom-controller/controller/internal/infra/filebrowser_sso.go:15-22`, `internal/web/files_sso.go`). The name file's
creation race was fixed by review round 2 (controller `b01bb4b`).
creation race was fixed by review round 2 (controller `b01bb4b`). The pick: FileBrowser joins only the network traefik uses, so no app
container reaches it directly. Not built yet.
## 6. What we accept (do not re-report)
@@ -89,7 +89,7 @@ alternative (an out-of-band confirmation) needs a second channel the box does no
- ~~The website FAQ line („to turn it off: Beállítások → Rendszer → A gép kikapcsolása") — a later website task (R-1000).~~ Published — `website/gyik.html:128,603` (corrected 2026-10-24).
- The BIOS „after power loss" value of the N100 was not read (no monitor); the household text promises only the button.
## Decisions in code no section records [DESIGN — undocumented, found 2026-10-24, review round 2 — confirmed through one register row]
## Decisions in code no section records [DESIGN — confirmed by the operator 2026-10-25 (D44 = all picks, R-1265)]
- **A re-enroll while the household power-off hold is active counts as „gone"** (a reinstall over the old host), not
„live" (R-1154; `hub/internal/api/reenroll.go:67-68`).
+11 -4
View File
@@ -166,7 +166,11 @@ directory) with that tag. Facts the link code needs:
no capture set (`stacks/classify_binds.go` `classifyRoot`). Measured over the live templates: Transmission's unlisted
`watch` bind goes off-site; the download folders are `optional`, so a second drive gets a nightly copy of every
download (R-1019). *(Fixed since: catalog `c50adfc` marks the download and watch folders `excluded` —
`app-catalog-felhom.eu/templates/transmission/.felhom.yml:46-51`; noted 2026-10-24.)* **[DESIGN]** The per-drive media bind is declared `excluded` for every media app — the family's
`app-catalog-felhom.eu/templates/transmission/.felhom.yml:46-51`; noted 2026-10-24.)* **[DESIGN — the operator picked a code change 2026-10-25 (D44); row R-1283]:** the same commit also
marks the download folders of NZBGet, SABnzbd and MeTube `excluded` (`templates/{nzbget,sabnzbd,metube}/.felhom.yml`).
NZBGet and SABnzbd stay excluded (their files can be fetched again). The pick for MeTube: `optional`, so the
second-drive copy carries it and the off-site copy does not — a MeTube video can be gone from its source site. Not
built yet. **[DESIGN]** The per-drive media bind is declared `excluded` for every media app — the family's
media is no single app's data; whether the library gets its own household-level backup is a separate decision.
## 8. Removal and export assume one drive per app
@@ -268,7 +272,10 @@ returns** (G7f).
**[DESIGN — undocumented, found 2026-10-24, review round 2 — confirmed through one register row]:** the „Később"
**[DESIGN — the operator picked a code change 2026-10-25 (D44); row R-1282]:** the „Később"
03:30 media restart is an IN-MEMORY timer — a controller restart forgets it and only the notice stays
(`internal/web/media_events.go` `handleMediaRestart`, `time.AfterFunc`); the Sonarr/Radarr API key is READ from `/config/config.xml`, the „set at
install" option §6 calls cleaner was not built (`internal/mediawire/mediawire.go:336-345`).
(`internal/web/media_events.go` `handleMediaRestart`, `time.AfterFunc`). The pick: the controller stores the due
time and runs the restart after a restart of its own. Not built yet.
**[DESIGN — confirmed by the operator 2026-10-25 (D44 = all picks, R-1265)]:** the Sonarr/Radarr API key is READ from
`/config/config.xml`; the „set at install" option §6 calls cleaner was not built (`internal/mediawire/mediawire.go:336-345`).
@@ -240,7 +240,7 @@ Also in build B: R-1084 (the whole-box backup offer asks the same-disk question)
offered to attach, by device). A bundle that adds a path needs a step bundle on boxes with an older wrapper (R16, R-880):
0.158.1 was delivered through `0.158.1-step1`.
## Decisions in code no section records [DESIGN — undocumented, found 2026-10-24, review round 2 — confirmed through one register row]
## Decisions in code no section records [DESIGN — confirmed by the operator 2026-10-25 (D44 = all picks, R-1265)]
Read at `file:line` on `main` by the main session: