Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EPZ4GJ8L5Jqf8UiPwbn1kt
5.1 KiB
REPORT — felhom-agent v0.45.0: controller-swap under non-root (stdin tee + narrow grants)
Date: 2026-06-29 · Class: Risky/supervised · Baseline: main @ 8a4ccab was v0.44.0 →
v0.45.0 @ 8a4ccab (this change). Build sha a393c8bc…. No felhom-controller change.
What & why
Restores fleet controller-swap / managed auto-update under the non-root agent — the capability the
2026-06-29 sudoers audit deliberately left broken (its only write vector, bash -c "printf … >",
needed arbitrary in-guest execution). Option A, spike-proven GO
(felhom.eu/documentation/audits/SPIKE-controllerswap-narrow-grants-2026-06-29.md).
The writeImage diff (the only orchestration change)
- cmd := fmt.Sprintf("printf '%%s\\n' '%s' > %s", image, controllerImageFile)
- _, err := c.exec.GuestExec(ctx, vmid, "bash", "-c", cmd)
+ _, err := c.exec.GuestExecStdin(ctx, vmid, strings.NewReader(image+"\n"), "tee", controllerImageFile)
The image ref is piped on stdin into an in-guest tee — no shell, no interpolation. image+"\n"
is byte-identical to the golden's printf '%s\n'; the bootstrap reads IMAGE=$(cat …)
(newline-stripping), so the write is consumed identically. ValidControllerImage still gates upstream
in Swap. Swap orchestration / rollback / state-file logic are unchanged.
New stdin seam (routes through the SAME sudo -n fenced runner — no hand-rolled exec):
proxmox.Runner.RunStdin + ExecRunner.RunStdin (Run with cmd.Stdin), GuestBinder.GuestExecStdin,
GuestExecutor.GuestExecStdin. The 5 proxmox.Runner test mocks gained a delegating RunStdin.
Sudoers — FELHOM_CONTROLLERSWAP (5 narrow grants)
/usr/sbin/pct exec [0-9]* -- cat /etc/felhom-controller-image # read (fixed)
/usr/sbin/pct exec [0-9]* -- docker image inspect * # read (target present?)
/usr/sbin/pct exec [0-9]* -- docker inspect -f * # read (running/health/image)
/usr/sbin/pct exec [0-9]* -- systemctl restart felhom-controller-bootstrap.service # restart (fixed unit)
/usr/sbin/pct exec [0-9]* -- tee /etc/felhom-controller-image # write (FIXED path, stdin-fed)
No general pct exec, no bash -c. Added to the NOPASSWD line. Capability manifest gains the 5
(Critical: a silently-broken fleet auto-update is operator-alert-worthy) — so the v0.44.0 self-probe
watches them and the build-test asserts grant↔code coverage.
Tests — all green (go build/vet/test ./..., 0 failures)
- New
TestControllerSwap_WriteViaStdinTee_NoShell: the write is stdin-teewith exactimage\n, theteetarget is the fixed path, and no shell vector (bash/-c/printf) appears — would FAIL on the pre-change impl. - Existing swap tests (happy / rollback-on-unhealthy / image-absent / no-healthcheck / bad-image /
single-flight) pass over the new write path (fake
GuestExecutornow modelsteevia stdin). RunStdinforwards stdin; the 5 mock runners implement it.- Build-time coverage:
TestManifestCoveredBySudoerscovers the 5 new vectors. Two red-proofs:TestRedProof_DroppedGrantFailsCheck(lxc-info) + newTestRedProof_DroppedControllerSwapTeeFailsCheck. Demonstrated red→green on the REAL file: dropping theteegrant →capability "controllerswap-write" … NOT covered by any sudoers grant→ FAIL; restored → PASS.
Live validation (felhom-pve, guest 9201) — deployed + verified
- Deploy: built
0.45.0on 180 (shaa393c8bc…), shipped 180→local→felhom-pve (sha verified), backed up prior (felhom-agent.bak-0.44.0),install+ restart →is-active= active. Sudoers redeployed via the install mechanism (visudo -cfstaged →install -m 0440 -o root -g root→ re-validate/etc/sudoersOK). (Staged file needed a\rstrip — Windows checkout CRLF — before visudo; the installed drop-in is LF-clean.) - Self-probe (live):
capabilities self-check ok=45 total=45 degraded=0— the 5 swap vectors are granted AND watched. - Grant matches (
sudo -n -l, as felhom-agent): all 5 → exit 0 (cat/docker image inspect <ref>/docker inspect -f '<real template>' felhom-controller/systemctl restart <unit>/tee <image file>). - Negative controls → denied (exit 1):
bash -c 'echo pwned',tee /etc/passwd,docker rm -f felhom-controller,rm -rf /etc. The grants are scoped, not a blanketpct exec.
NOT yet run — live swap (Scenario D) — awaiting supervised
The full end-to-end live swap (controller pre-pulls → swap endpoint → tee write → restart → healthy
on target; then a bad image → rollback) restarts the live controller and per §13.4 is
operator-supervised. It was NOT run (this session is unattended). When supervised, trigger it via the
controller's real update flow (not a hand-set state) and confirm the tee-write → restart → healthy and
the bad-image rollback over the new write path.
Forward note
No code→manifest drift auto-check yet (a future static check of runner.Run/GuestExec* call sites
vs the manifest). The controller↔agent channel self-health check remains a separate slice.