Doc 14: turning the machine off (R-998) — path, guards, the hub's 14-day hold, the threat; F11; R-1000 filed
gates / gates (push) Successful in 6m40s
gates / gates (push) Successful in 6m40s
03 §6 lists /guest/reboot and /host/poweroff; 13 counts 38 local-API routes and names the new power of attacker (e). Hub CHANGELOG corrected: os_ring0_stalled is NOT held (a fleet rollout alarm, on purpose). R-983: the GET reboot fixed (six remain). R-1000: the website FAQ line.
This commit is contained in:
@@ -217,6 +217,7 @@
|
||||
| <a id="f7"></a>[F7](capability-map-detail/F.md#f7) | Always-on debug rings + on-demand log-bundle pulls with TTL/custody | controller v0.116, agent v0.83, hub v0.46 | **PROVEN-LIVE** | — | `audits/CAMPAIGN-3-2026-07-11.md` (2026-07-11) |
|
||||
| <a id="f8"></a>[F8](capability-map-detail/F.md#f8) | Operator alerting (Healthchecks → monitoring@felhom.eu) | k3s, Resend | **IMPLEMENTED** | no corpus validation doc | operator infra; no evidence document |
|
||||
| <a id="f9"></a>[F9](capability-map-detail/F.md#f9) | Backup-deadline alerting (`expected_backup_missed`) is ANCHORED — absence of signal is UNKNOWN, not failure | hub v0.75.0 | **IMPLEMENTED** | — | `audits/DIAG-backup-missed-2026-07-26.md` (2026-07-26); R-82 |
|
||||
| <a id="f11"></a>[F11](capability-map-detail/F.md#f11) | The household turns the whole machine off („A gép kikapcsolása"); the hub holds every silence alarm for 14 days and shows KIKAPCSOLVA | agent v0.155.0, hub v0.149.0, controller v0.310.0 | **PARTIAL** | built and tested; live proof on the N100 pending | `architecture/14-host-power-off.md`; `audits/poweroff-2026-10-13/`; R-998 |
|
||||
| <a id="f10"></a>[F10](capability-map-detail/F.md#f10) | A box that is not always on (off at its backup time) makes the missed night up once when it comes back; the household sees a banner; the operator gets the missed-backup alarm, down or not | controller v0.295.0, hub v0.134.0, agent v0.145.0 | **PARTIAL** | catch-up and banner live; R-872 alarm by tests; suspend not measured | `audits/catchup-2026-10-05/partA/` (2026-10-05); R-872 |
|
||||
|
||||
## G. Fleet & operator (hub)
|
||||
|
||||
@@ -264,9 +264,14 @@ destructive → the §4 gate → refused **`pending_signature`** (the operator-s
|
||||
drive — the 8C analog of self-scoping. **Status: implemented** (agent v0.12.0 `internal/localapi` +
|
||||
`internal/storage`; controller v0.37.0 `internal/web/agent_disk_handlers.go`).
|
||||
|
||||
- **Guest restart:** `POST /guest/reboot` — `pct reboot` of *this* guest (the dashboard's „A Felhom újraindítása").
|
||||
- **Host power-off (R-998, agent v0.155.0):** `POST /host/poweroff` — the household's „A gép kikapcsolása": this guest
|
||||
shut down, then the host powered off. Guarded (10 min after boot, one per boot, no other guest or VM running, no
|
||||
backup/restore/update/drive job) — the whole design, its hub hold and its threat: `14-host-power-off.md`.
|
||||
|
||||
Note what is *absent*: nothing here lets a controller touch **another guest**, the **host** beyond
|
||||
this narrow disk surface, or **restore-overwrite**; and within the disk surface, **data-destructive**
|
||||
power stays operator-signed (§4). Destructive/cross-guest power stays operator-signed.
|
||||
this narrow disk surface **and the guarded power-off above**, or **restore-overwrite**; and within the disk surface,
|
||||
**data-destructive** power stays operator-signed (§4). Destructive/cross-guest power stays operator-signed.
|
||||
|
||||
A controller can only `POST /rollback` (or snapshot/backup) **its own** guest — the agent maps
|
||||
token → guest and authorizes per guest, so a compromised controller's blast radius is
|
||||
|
||||
@@ -47,7 +47,7 @@ scripts are held with the review's working notes.
|
||||
|---|---|---|---|
|
||||
| controller | 231 (+8 in setup mode) | 21, of which 7 change state, each behind its own password, code or token | port 8080 on the guest's Docker networks (not published); mail relay 2525/2465/2526 |
|
||||
| hub | 119 (1 health, 53 API, 65 web) | appliance register (rate-limited, by design), re-enroll (only while the operator has armed recovery mode), login, health | one ingress, path `/`, geo filter (HU, IE, LAN) |
|
||||
| agent | 37 local-API routes | 0 | local API on the island bridge; WireGuard; poke (tunnel only); felhom-sshd (tunnel only, measured) |
|
||||
| agent | 38 local-API routes (`POST /host/poweroff` added 2026-10-13, R-998) | 0 | local API on the island bridge; WireGuard; poke (tunnel only); felhom-sshd (tunnel only, measured) |
|
||||
|
||||
**Outbound fetches that bring code or config onto a box:** the catalog (git, `main`), app images, the four infra
|
||||
images, the controller image (by tag), `controller.yaml` from the hub, the agent binary and config bundle (signed),
|
||||
@@ -59,7 +59,7 @@ OS packages (exact versions, apt-signed), the installer script (by tag, R-980).
|
||||
|---|---|---|
|
||||
| end-user ↔ apps | holds, with gaps | traefik routing and header stripping hold; homepage has the Docker socket and no gate (R-981) |
|
||||
| customer ↔ controller UI | partly | bcrypt and session hold; small gaps (R-983); family sessions survive a reset (R-976) |
|
||||
| controller ↔ agent | partly | every route bound to the caller's guest; a compromised controller can still flush restore points (R-973) and void the recovery code (R-978) |
|
||||
| controller ↔ agent | partly | every route bound to the caller's guest; a compromised controller can still flush restore points (R-973), void the recovery code (R-978), and — since agent v0.155.0 — switch the whole machine off once per power-on, holding the hub's silence alarms for up to 14 days (R-998; mitigations and residual risk: `14-host-power-off.md` §5) |
|
||||
| agent ↔ hub | holds for destructive ops | signed jobs verified in the documented order; unsigned spec has no size bounds (R-984) |
|
||||
| controller ↔ hub | **did not hold until 2026-10-12** | the global key was public (R-950, rotated) and three routes needed no key (R-951, fixed); per-box keys are bound to their box (R-955) |
|
||||
| box ↔ PBS | holds as written | ciphertext only; the zero-knowledge wording is stronger than a live box allows (R-985) |
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# 14 — Turning the machine off: „A gép kikapcsolása" (R-998)
|
||||
|
||||
> **What this is:** the household's way to switch the whole machine off — the path, the guards, what the hub does, and
|
||||
> the threat it adds. Written 2026-10-13 with the build (agent v0.155.0, hub v0.149.0, controller v0.310.0).
|
||||
> Spike: `documentation/audits/poweroff-2026-10-13/`. Marks as in `01`: **[DESIGN]** a decision; **[FACT]** read in a
|
||||
> named file or measured.
|
||||
|
||||
## 1. Why
|
||||
|
||||
**[FACT]** Before this, nothing could turn a Felhom machine off: „Kiszolgáló újraindítása" restarted only the guest
|
||||
(controller `HandleServerReboot` → agent `POST /guest/reboot` → `pct reboot`). A household moving house could only pull
|
||||
the plug. **[FACT]** Measured on the N100 (spike Part A): a clean guest shutdown (12 s) and `systemctl poweroff` work;
|
||||
the plug then reads 0.0 W; the hub stayed silent and „ONLINE" for 18 minutes, and by code alarms at 45 min
|
||||
(`host_stale`), 90 min (`host_down`) and every 05:00 (`expected_*_missed`); the N100 did **not** start when the plug
|
||||
came back (BIOS „last state", inferred), its power button did (dashboard in 42 s).
|
||||
|
||||
## 2. The path
|
||||
|
||||
| Step | Where | What |
|
||||
|---|---|---|
|
||||
| 1 | controller `settings_system.html` | card „A gép kikapcsolása"; the household types KIKAPCSOLÁS / TURN OFF |
|
||||
| 2 | controller `POST /api/server/poweroff` (`internal/web/poweroff_handlers.go`) | dashboard login (`RequireAuth` + CSRF), POST only, the word again, the controller's own backup/restore/app-update check, the `host_poweroff` feature gate (MinAgent 0.155.0) |
|
||||
| 3 | agent `POST /host/poweroff` (`internal/localapi/hostpoweroff.go`) | `withGuest` token scope; the guards (§3); the record (`internal/poweroff`) |
|
||||
| 4 | agent → hub | a host report carrying `deliberate_off.requested_at`, waited for until the hub ACCEPTED it (≤ 60 s, `Loop.SetReportedHook`) |
|
||||
| 5 | agent | `pct shutdown <vmid> --timeout 120`, then `systemctl poweroff` (two exact sudoers vectors, `FELHOM_POWEROFF`) |
|
||||
| 6 | hub (`store/deliberateoff.go`) | `hosts.deliberate_off_at`; silence alarms held 14 days (§4) |
|
||||
| 7 | the household | presses the machine's power button; the first report of the new boot has no `deliberate_off` → the hub clears it |
|
||||
|
||||
## 3. The guards (agent) — each refuses with a stable `reason`, each fails closed
|
||||
|
||||
| Guard | Reason | Test (`hostpoweroff_test.go`) |
|
||||
|---|---|---|
|
||||
| first 10 min after boot | `too_soon_after_boot` | `TooSoonAfterBoot` |
|
||||
| one accepted request per boot (file keyed by `/proc/stat` btime) | `already_requested_this_boot` | `SecondRequestSameBootRefused`, `poweroff/state_test.go` |
|
||||
| any other guest or VM running (`/proc`: `lxc-start -n N`, `kvm -id N`) | `other_guests_running` | `RefusedWhileOtherGuestsRun`, `RunningGuestsFromProc` |
|
||||
| heavy-operation gate held (backup, restore test, OS step, sweep, trim) | `backup_running` / `restore_test_running` / `os_update_running` / `host_busy` | `RefusedDuringBackup` |
|
||||
| backup job, controller swap, drive format, NAS verify, escrow ceremony, Proxmox vzdump | `backup_running` / `controller_update_running` / `drive_operation_running` / `escrow_ceremony_running` | `RefusedDuringVzdump`, `FailsClosedWithoutTaskReader` |
|
||||
| after acceptance: the heavy gate is TAKEN and kept; before each step the jobs and other guests are read again — a job waits ≤ 15 min, another guest cancels at once, a job still running cancels | (log) | `JobStartedAfterAcceptance`, `OtherGuestStartedAfterAcceptance` |
|
||||
|
||||
**[DESIGN]** A cancelled power-off leaves the box on (gate released) — better a box that did not switch off than a
|
||||
backup, update or format cut in the middle. **[DESIGN]** A failed guest shutdown does not stop the power-off: Proxmox's
|
||||
own shutdown stops a still-running guest before the host goes down.
|
||||
|
||||
## 4. The hub's hold — **[DESIGN]** 14 days (operator default, `store.DeliberateOffHold`)
|
||||
|
||||
Held while the box is silent AND inside the hold — every alarm a switched-off box would raise, named from the code:
|
||||
|
||||
| Alarm | Where | How it is held |
|
||||
|---|---|---|
|
||||
| `host_stale`, `host_down` | `monitor/host_staleness.go` | state `off` |
|
||||
| `node_stale`, `node_down` | `monitor/staleness.go` | state `deliberately_off` |
|
||||
| `expected_backup_missed`, `expected_dbdump_missed` | `monitor/deadline.go` | skipped (state, and the host row) |
|
||||
| `offsite_stale`, `offsite_escrow_pending` | `monitor/offsite.go` | customer skipped |
|
||||
| `restore_test_stale` | `monitor/restoretest.go` | customer skipped |
|
||||
| `os_update_stale`, `agent_behind`, `os_config_bundle_behind`, `os_reboot_needed`, `os_not_covered` | `osupdates/service.go` | host filtered |
|
||||
| the kernel „tonight your box restarts" mail | `osupdates/kernel.go` | host filtered |
|
||||
|
||||
**Not held, on purpose:** `os_ring0_stalled` (`osupdates/service.go`, `ring0Hosts`) — it is about the FLEET's update
|
||||
rollout, not about this box: a ring-0 box switched off stalls the rollout, and the operator should know.
|
||||
|
||||
After the hold, still silent: `host_down` + `node_down` once, with a text naming the household power-off. Back on inside
|
||||
the hold: no `*_recovered` mail. Pages: KIKAPCSOLVA and „Kikapcsolva a háztartás által, T". A customer is held only
|
||||
when EVERY host of it is held; a future `requested_at` (the box's clock) is cut to the hub's now. Tests:
|
||||
`monitor/deliberate_off_test.go` (each with a control that alarms), `api/deliberate_off_test.go`,
|
||||
`web/deliberate_off_view_test.go`.
|
||||
|
||||
## 5. The threat it adds — attacker (e), a compromised controller (`13-threat-model.md`)
|
||||
|
||||
**[FACT]** `/host/poweroff` is the first local-API route whose effect reaches beyond the caller's own guest: a
|
||||
compromised controller can now switch the whole machine off, and **nobody can switch it on remotely**. And it can
|
||||
choose to: its power-off holds the hub's silence alarms for 14 days.
|
||||
|
||||
| Mitigation | Bound |
|
||||
|---|---|
|
||||
| one request per boot, none in the first 10 min after boot | no power-off loop; one outage per physical power-on |
|
||||
| refused while another guest or VM runs | cannot take down a neighbour's guest (demo-hp, multi-guest hosts) |
|
||||
| refused during backup/restore/update/drive jobs; re-checked before each step | cannot cut a job in the middle |
|
||||
| the hold ends at 14 days; one machine of two does not hold the customer | the silence is bounded |
|
||||
|
||||
**Residual risk, named:** a compromised controller can switch a single-guest household box off once per power-on and
|
||||
silence the hub for up to 14 days. The household sees a dark machine; the operator sees „Kikapcsolva a háztartás által"
|
||||
— not an alarm. Accepted for a household product whose controller is already trusted with the household's data; the
|
||||
alternative (an out-of-band confirmation) needs a second channel the box does not have.
|
||||
|
||||
## 6. Not done here
|
||||
|
||||
- The website FAQ line („to turn it off: Beállítások → Rendszer → A gép kikapcsolása") — a later website task (R-1000).
|
||||
- The BIOS „after power loss" value of the N100 was not read (no monitor); the household text promises only the button.
|
||||
@@ -177,3 +177,22 @@
|
||||
**Gap / roadmap (as written):** R-872 live; a suspend never measured
|
||||
|
||||
---
|
||||
|
||||
## F11 — The household turns the whole machine off („A gép kikapcsolása"); the hub holds every silence alarm for 14 days
|
||||
|
||||
*Map row: [F11](../00-capability-map.md#f11).*
|
||||
|
||||
**Scenario:** a household moving house or going away presses „A gép kikapcsolása" (Beállítások → Rendszer), types
|
||||
KIKAPCSOLÁS; the guest shuts down, then the host; the hub shows KIKAPCSOLVA and raises no outage alarm for 14 days;
|
||||
the machine's power button brings it back, and the hub clears the state.
|
||||
|
||||
**Components:** agent v0.155.0 (`POST /host/poweroff`), hub v0.149.0 (`hosts.deliberate_off_at`, the hold), controller
|
||||
v0.310.0 (the card, `POST /api/server/poweroff`).
|
||||
|
||||
**Status:** PARTIAL — built and tested (each guard red-proofed); live proof on the N100 pending.
|
||||
|
||||
**Evidence:** design `14-host-power-off.md`; spike `audits/poweroff-2026-10-13/`.
|
||||
|
||||
**Gap / roadmap:** the website FAQ line (R-1000).
|
||||
|
||||
---
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -10,7 +10,7 @@ Needs **agent ≥ 0.155.0** to see anything: the agent's new `deliberate_off.req
|
||||
`host_stale`/`host_down` (state `off`), `node_stale`/`node_down` (state `deliberately_off`, also read by the 05:00
|
||||
check), `expected_backup_missed`/`expected_dbdump_missed` (also skipped from the host row, so a box switched off at
|
||||
04:50 is covered), `offsite_stale` and `offsite_escrow_pending`, `restore_test_stale`, the OS alarms
|
||||
(`os_update_stale`, `agent_behind`, `os_config_bundle_behind`, `os_reboot_needed`, `os_not_covered`, `os_ring0_stalled`)
|
||||
(`os_update_stale`, `agent_behind`, `os_config_bundle_behind`, `os_reboot_needed`, `os_not_covered`; NOT `os_ring0_stalled`, a fleet rollout alarm, on purpose)
|
||||
and the household's kernel notice mail. **After the hold**, still silent: the usual `host_down` + `node_down`, once,
|
||||
with a text naming the household power-off. Back on inside the hold: no `*_recovered` mail.
|
||||
- **Security review (same day):** the time is the BOX's clock, so a future `requested_at` is cut to the hub's now (it
|
||||
|
||||
Reference in New Issue
Block a user