Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
7.5 KiB
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 sudoersteeline 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 callstee→ 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-applyverifies 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
03sentence 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-imageinconfigs/felhom-priv-apply(stdin ≤ 256 bytes, the regex, a numeric vmid, thenpct exec <vmid> -- teeas root); sudoers: removetee /etc/felhom-controller-image$, add/usr/local/sbin/felhom-priv-apply ^controller-image [0-9]+$;controllerswap.go:150calls the verb. - Red test first:
configs/test_felhom_priv_apply.py—controller-image 9201with stdindocker.io/library/alpine:latestmust exit 3; withgitea.dooplex.hu/admin/felhom-controller:0.301.0exit 0. AndTestSudoersRefusesTheR861Injectionsgainspct exec 9201 -- tee /etc/felhom-controller-imageas a REFUSED line — it fails on today's sudoers. - Live proof on scratch 9202 after a bundle there (not tonight):
sudo -l -U felhom-agentlists notee; a managed controller update still swaps (positive observable: the new controller version indocker psAND the hub's host report — two channels); a hand-fedalpineref is refused (journal tagfelhom-priv-apply).
6. Open questions for the operator
- (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.
- (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.