# 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 --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` | **„A Proxmox vzdump runs” (agent v0.156.1, R-1008):** a vzdump task for the guest in Proxmox's active list whose worker process still lives — Proxmox's own rule (the UPID's pid + pstart against `/proc//stat`). After a power cut the dead task stays listed for ~30–60 s; it no longer counts. Doubt (an unparsable task, an unreadable `/proc`) still refuses. The same answer serves the start-up stale-lock pass, the guest-power watchdog and the controller supervisor (`stalelock_inflight_test.go`). | 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.