Files
felhom.eu/documentation/audits/night-burndown-2026-10-06/design-R-861.md
T

7.5 KiB
Raw Blame History

R-861 — the three sudoers leftovers — design proposal (burn-down night 2026-10-06, no code)

Baselines read: felhom-agent 74b5eae, felhom.eu 8e2dc204. Architecture: 03-host-agent.md §3.1 (the group table) and §11, 11-os-updates.md §5.4.2 (the config bundle), 09 §3 decision 122. Read only; nothing ran on a box.

1. The problem

After agent v0.146.1 a compromised agent PROCESS (user felhom-agent) no longer becomes host root: measured 2026-10-05, 29 attack lines refused, sudo -l 93/93 on both demo boxes (audits/hub-safety-2026-10-05/partF/). Three paths were left open on purpose and named in 03 §3.1. The row asks the operator: accept them, or close them before the first paying customer? The question for each is: what does an attacker who already owns the agent process gain through it, beyond what the agent already has?

What the agent already has (the baseline): its Proxmox token holds VM.Backup, VM.Allocate, VM.Config.*, VM.PowerMgmt, Pool.Allocate on /pool/felhom (felhom.eu/scripts/felhom-host-install.sh:309). So it can already back up a customer guest and restore it into a scratch guest (the restore-test does exactly that), stop and start felhom guests, and it writes the guest's bootstrap.json itself. The household's data is already in its reach, offline.

2. What the code does today (read in source)

(a) The controller image ref. FELHOM_CONTROLLERSWAP allows pct exec <vmid> -- tee /etc/felhom-controller-image (felhom-agent/configs/felhom-agent.sudoers:122). The ref goes in on STDIN (internal/localapi/controllerswap.go:150). The regex controllerImageRe (controllerswap.go:36) is the agent's OWN check — a compromised agent skips it, and sudo cannot see stdin. The guest's bootstrap unit then runs docker run … "$IMAGE" with the docker socket, the read-only bootstrap dir and /mnt (configs/build-golden.sh:316,355-362). Correction to 03 §3.1: the table says "a chosen (pinned-registry) image ref". That is not true: only the docker image inspect line is pinned (sudoers:119); the tee content is free, so the bootstrap pulls and runs any image from any registry. Gain over the baseline: a LIVE foothold in the guest with the docker socket (guest root, the household's running apps and its LAN), and it survives agent restarts until the next swap. Not host root.

(b) The felhom-op SSH key. felhom-priv-apply sshd-key installs one plain key line (no command=/from=; configs/felhom-priv-apply:60-61) from a file the agent staged; the key itself comes from the hub, unsigned. A compromised agent can therefore put its own key on felhom-op. felhom-op's sudo (configs/felhom-op.sudoers) is scoped: restart wg/agent/sshd, pct list, and pct start|stop|unlock [0-9]* — on any guest of the host, not only the felhom pool. Gain over the baseline: power control of NON-felhom guests on a BYO host (the household's own other VMs), and an interactive login. Not host root. (Side note: these pct lines still use the * glob, which matches spaces — the R-861 shape 1; no harmful pct start/stop option is known, so this is hygiene, not a hole.)

(c) The escrow ceremony. The root child (FELHOM_ESCROW, sudoers:283-284) returns the recovery code R on the agent's stdout pipe (internal/localapi/escrow_ceremony.go:19-29,93); the agent holds R in memory for one claim (:140-147). The agent's own hub key may read this box's escrow blob (hub/internal/api/handler.go:252,1357-1372, self-scoped). So a compromised agent can learn R, fetch the blob and unwrap this box's PBS encryption key (and the identity bundle). Gain over the baseline: off-box decryption of this box's off-site archives, which lasts after the compromise is cleaned up, until the key is rotated. One box only; not root.

3. Options

(a)

  • A1. A checking wrapper verb. felhom-priv-apply controller-image <vmid>: as root, read stdin, require ^gitea\.dooplex\.hu/admin/felhom-controller:[0-9]+\.[0-9]+\.[0-9]+$, then write the guest file. The sudoers tee line is removed. Same mechanism as decision 122 (b); delivered by the signed config bundle. Cost: ~1–2 h (one verb, its Python tests, the Go call path, TestManifestCoveredBySudoers, the capability probe). What can go wrong: the old agent binary still calls tee → the bundle must follow the agent update (the usual order). Leaves: a chosen OLD controller version from our registry (a downgrade) — still possible.
  • A2. Check in the guest. The bootstrap script refuses a ref outside the pattern. Cost: a golden change, and existing guests keep the old script until re-baked — slow to reach the fleet.
  • A3. Accept. Write the corrected sentence in 03 §3.1.

(b)

  • B1. Sign the key. The operator signs the felhom-op key with the operator key; felhom-os-apply verifies as root. Cost: medium; every key rotation needs the operator's offline signature.
  • B2. Narrow felhom-op's sudo to anchored regexes (^start [0-9]+$ …) — hygiene only; it does not stop the key swap. Cost: ~30 min, rides the bundle.
  • B3. Accept (felhom-op is not root; the gain is power control of guests).

(c)

  • C1. R bypasses the agent. The root child writes R straight into the guest (to the controller), so the agent never sees it. Cost: a ceremony redesign across agent and controller; touches the escrow promise to the household.
  • C2. Accept — the ceremony is designed so the box handles K once; the residual is "a compromised agent can read this one box's backups off-site", which 03 §3.1 already names.

4. The pick — PROPOSAL for the operator, not a decision

  • (a) A1, before the first paying customer. It is the only one of the three that gives a live, persistent foothold next to the household's running apps, and the fix uses a mechanism already built and measured. Fix the 03 sentence in the same commit.
  • (b) B3 now, B2 as hygiene with the next bundle; B1 later if the OOB door is ever opened wider.
  • (c) C2. C1 changes an escrow promise and is a redesign — not before the first customer.

5. First slice and its proof (for A1)

  • Build: verb controller-image in configs/felhom-priv-apply (stdin ≤ 256 bytes, the regex, a numeric vmid, then pct exec <vmid> -- tee as root); sudoers: remove tee /etc/felhom-controller-image$, add /usr/local/sbin/felhom-priv-apply ^controller-image [0-9]+$; controllerswap.go:150 calls the verb.
  • Red test first: configs/test_felhom_priv_apply.py — controller-image 9201 with stdin docker.io/library/alpine:latest must exit 3; with gitea.dooplex.hu/admin/felhom-controller:0.301.0 exit 0. And TestSudoersRefusesTheR861Injections gains pct exec 9201 -- tee /etc/felhom-controller-image as a REFUSED line — it fails on today's sudoers.
  • Live proof on scratch 9202 after a bundle there (not tonight): sudo -l -U felhom-agent lists no tee; a managed controller update still swaps (positive observable: the new controller version in docker ps AND the hub's host report — two channels); a hand-fed alpine ref is refused (journal tag felhom-priv-apply).

6. Open questions for the operator

  1. (a) Close the free image ref before the first paying customer (A1, ~1–2 h, ships with a bundle)? If you do nothing: a compromised agent can run any container next to the household's apps, with the docker socket.
  2. (b)+(c) Accept both for the first customers (felhom-op is not root; the escrow residual is one box's off-site backups)? If you do nothing: they stay open, named in 03 §3.1, as today.