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

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