91 lines
7.3 KiB
Markdown
91 lines
7.3 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` |
|
||
|
||
**„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/<pid>/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.
|