Files
felhom.eu/documentation/architecture/14-host-power-off.md
T
admin 81e64c6fe8
gates / gates (push) Successful in 6m40s
Doc 14: turning the machine off (R-998) — path, guards, the hub's 14-day hold, the threat; F11; R-1000 filed
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.
2026-10-13 20:36:42 +02:00

89 lines
6.8 KiB
Markdown

# 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.