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.
6.8 KiB
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 in01: [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.