diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fae66a..0e4dde6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,40 @@ All notable changes to **felhom-agent** are recorded here. Update on every code change that gets pushed. +## (unreleased) — sudoers completeness audit: close non-root allowlist gaps (no binary change) (2026-06-29) + +A full audit of every privileged command the agent shells via `sudo -n` against +`configs/felhom-agent.sudoers`, closing the read-only/fixed-vector gaps left by the 2026-06-28 +root→non-root cutover. **Sudoers-only change — no Go change, no version bump** (the file is fetched +canonically by the host-install script). Root cause of the multi-drive "attach one, the other drops" +symptom (audit `felhom.eu/documentation/audits/SPIKE-multidrive-mutual-exclusion-2026-06-29.md`): the +allowlist was incomplete, so several `sudo -n` calls were denied under the non-root user. + +- **`lxc-info -n [0-9]* -p -H` → FELHOM_INTERMEDIARY (THE root-cause fix).** `guestInitPID` + (`intermediary.go:256`) shells this to resolve the guest init PID for + `GuestSeesMount`→`bound_under_parent`. It was absent from the allowlist → `sudo -n` denied → empty + PID → **every external drive reported absent** → the controller drive-gate stopped each drive's apps + (flapping). With the grant, `bound_under_parent` reports truthfully and the gate quiesces. +- **`mount --make-private /mnt/felhom-drives` → FELHOM_INTERMEDIARY.** `EnsureSharedParent` + (`intermediary.go:110`) calls it to isolate the shared parent's peer group on first setup; the + allowlist had only `--make-shared`, so the parent stayed in root's peer group and host submounts + "doubled". Guarded by a mountpoint check (never re-churns a live parent). +- **`systemctl restart dnsmasq` → FELHOM_DNSMASQ.** The v0.29.x LAN-DNS fix switched `reload`→`restart` + (`lanresolver.go restartDnsmasq`) but the allowlist still only permitted `reload` → split-horizon + DNS self-heal was silently denied under non-root. Added alongside the retained `reload`. +- **`pct set [0-9]* -onboot 1` → FELHOM_PROVISION.** The provision back-half (`backhalf.go`, F3 + auto-start) sets onboot; only `-mp[0-9]*` was allowed → denied under non-root. +- **`pct reboot [0-9]*` → FELHOM_GUESTHOOK.** `RebootGuest` (`disks.go:448`, the enroll "activate + pending binds" fallback) was unmatched. + +**Surfaced for operator decision (NOT added — would require arbitrary root-in-guest):** `GuestExec`'s +general `pct exec [0-9]* -- <…>` (controller-swap self-update / Phase-2 managed updates) runs variable +vectors incl. `bash -c ""` — granting it = arbitrary execution. **Controller-swap is +currently broken under the non-root agent** until narrow per-vector grants are decided. **Deferred:** +`sensors -j` (defined-but-unwired AND `lm-sensors` not installed on the host — no live caller, path +unverifiable). **Not added (no daemon caller):** `pct create …` (CreateGoldenLXC, maintenance/broad), +`mount UUID=… …` (MountUSBByUUID, legacy/unreferenced). Full audit table in `REPORT.md`. + ## v0.43.0 — canonical systemd unit + binary published to Gitea (BUNDLE slice) (2026-06-28) Day-0 no longer needs a hand-installed agent. The agent binary is now PUBLISHED to Gitea as a generic diff --git a/configs/felhom-agent.sudoers b/configs/felhom-agent.sudoers index 517c994..2d3db64 100644 --- a/configs/felhom-agent.sudoers +++ b/configs/felhom-agent.sudoers @@ -34,7 +34,8 @@ Cmnd_Alias FELHOM_DISK = \ # writes there is the only thing these touch. ':' is escaped per sudoers grammar. Cmnd_Alias FELHOM_PROVISION = \ /usr/bin/chown -R 100000\:100000 /var/lib/felhom-agent/guests/*, \ - /usr/sbin/pct set [0-9]* -mp[0-9]* /var/lib/felhom-agent/guests/* + /usr/sbin/pct set [0-9]* -mp[0-9]* /var/lib/felhom-agent/guests/*, \ + /usr/sbin/pct set [0-9]* -onboot 1 # Disk inspection + format (slice 8C). blkid/lsblk read the device's data-bearing evidence (the # agent decides data-bearing-ness from THIS, never the caller's claim); mkfs.* formats a device the @@ -57,6 +58,7 @@ Cmnd_Alias FELHOM_DNSMASQ = \ /usr/bin/install -m 0644 /tmp/felhom-resolver-*.conf /etc/dnsmasq.d/felhom-*.conf, \ /usr/bin/systemctl enable --now dnsmasq, \ /usr/bin/systemctl reload dnsmasq, \ + /usr/bin/systemctl restart dnsmasq, \ /usr/bin/rm -f /etc/dnsmasq.d/felhom-*.conf, \ /usr/sbin/pct exec [0-9]* -- ip -4 -o addr show dev eth0, \ /usr/sbin/pct exec [0-9]* -- docker exec felhom-controller cat /opt/docker/felhom-controller/controller.yaml @@ -69,13 +71,19 @@ Cmnd_Alias FELHOM_DNSMASQ = \ Cmnd_Alias FELHOM_GUESTHOOK = \ /usr/bin/install -m 0755 -- /tmp/felhom-guest-hook.sh /var/lib/vz/snippets/felhom-guest-hook.sh, \ /usr/sbin/pct set [0-9]* --hookscript local\:snippets/felhom-guest-hook.sh, \ - /usr/sbin/pct set [0-9]* --delete mp[0-9]* + /usr/sbin/pct set [0-9]* --delete mp[0-9]*, \ + /usr/sbin/pct reboot [0-9]* # Intermediary mount model (the drive hot-swap re-architecture). The agent keeps a SHARED host parent # /mnt/felhom-drives (self-bind + make-shared + a boot-persistence systemd unit) and binds/unbinds each # drive's felhom-data namespace UNDERNEATH it so the change propagates into the running guest live (no # pct, no reboot). The agent fine-validates the drive name + confines paths before any exec; the trailing # `*` (matching the comma-laden mp spec) mirrors the existing FELHOM_PROVISION pattern. +# `lxc-info -n -p -H` resolves the guest init PID for the GuestSeesMount / bound_under_parent check +# (a READ — the drive-gate's "is the drive live in the guest?" signal); WITHOUT it the non-root agent gets +# an empty PID and reports every drive absent (multi-drive flapping, audit 2026-06-29). `make-private` +# isolates the parent's peer group on FIRST setup only (EnsureSharedParent guards on mountpoint, so it +# never re-churns a live parent); without it the parent stays in root's group and submounts double. Cmnd_Alias FELHOM_INTERMEDIARY = \ /usr/bin/mkdir -p /mnt/felhom-drives, \ /usr/bin/mkdir -p /mnt/felhom-drives/*, \ @@ -83,11 +91,13 @@ Cmnd_Alias FELHOM_INTERMEDIARY = \ /usr/bin/chown 100000\:100000 /mnt/*/felhom-data, \ /usr/bin/mount --bind /mnt/felhom-drives /mnt/felhom-drives, \ /usr/bin/mount --make-shared /mnt/felhom-drives, \ + /usr/bin/mount --make-private /mnt/felhom-drives, \ /usr/bin/mount --bind /mnt/*/felhom-data /mnt/felhom-drives/*, \ /usr/bin/umount /mnt/felhom-drives/*, \ /usr/bin/install -m 0755 -- /tmp/felhom-shared-parent.sh /usr/local/sbin/felhom-shared-parent.sh, \ /usr/bin/install -m 0644 -- /tmp/felhom-shared-parent.service /etc/systemd/system/felhom-shared-parent.service, \ /usr/bin/systemctl enable felhom-shared-parent.service, \ + /usr/bin/lxc-info -n [0-9]* -p -H, \ /usr/sbin/pct set [0-9]* -mp8 /mnt/felhom-drives* felhom-agent ALL=(root) NOPASSWD: FELHOM_MOUNT, FELHOM_DISK, FELHOM_PROVISION, FELHOM_FORMAT, FELHOM_DNSMASQ, FELHOM_GUESTHOOK, FELHOM_INTERMEDIARY