fix(v0.92.0): the guestnet probes need their own sudoers grant (found live)

The first sweep on felhom-pve logged 'dhclient liveness probe failed: sudo: a password is
required' and reported state=unknown — the watchdog fail-safed correctly, but it was blind.
TASK-D assumed no sudoers change; the existing grant only covered lanresolver's address read.

FELHOM_GUESTNET adds four FIXED vectors (route, interfaces, pgrep, and the incident's heal
invocation). Every argument after the numeric vmid is a literal, so nothing the guest or the
hub says can widen the grant. Plus four guestnet-* capability rows, non-critical, so a host
that has not taken the new sudoers is visibly degraded without paging an operator per box.
This commit is contained in:
2026-07-21 12:32:27 +02:00
parent c0966d753d
commit 0e8fd814ba
3 changed files with 34 additions and 1 deletions
+10
View File
@@ -58,6 +58,16 @@ block named against the convention.
every unit test green. Red-proof: un-wiring both lines fails the test with both reasons named.
- Red-proof for the detection itself: reverting `classify` to IP-presence-only makes the July-20
fixture report **"healthy"** and records **zero** heals — the 80-minute silent window, reproduced.
- **Ships a sudoers change** (`configs/felhom-agent.sudoers` MUST be deployed with the binary).
TASK-D assumed none was needed; live verification on felhom-pve proved otherwise — the first sweep
logged `dhclient liveness probe failed: sudo: a password is required` and correctly reported
`state=unknown` rather than acting blind. The existing grant covered only lanresolver's address
read. `FELHOM_GUESTNET` adds four fixed vectors (route, interfaces, pgrep, and the heal), every
argument after the numeric vmid a literal, so no hub or guest input can widen it. The address read
is not duplicated — it stays FELHOM_DNSMASQ's.
- Four `guestnet-*` capability rows so a host missing that sudoers file is VISIBLE as degraded
rather than silently watchdog-less. Non-critical on purpose: a missing grant must not page an
operator for every box on rollout day (the R-50b amber-fleet lesson).
- `var version` in main.go was stale at `0.89.0` (three releases behind); builds set it via ldflags,
but `go run` and any forgotten `-X` reported a version that had not existed for days.
+14 -1
View File
@@ -261,4 +261,17 @@ Cmnd_Alias FELHOM_ESCROW = \
Cmnd_Alias FELHOM_SELFHEAL = \
/usr/bin/systemctl start networking.service
felhom-agent ALL=(root) NOPASSWD: FELHOM_MOUNT, FELHOM_DISK, FELHOM_PROVISION, FELHOM_FORMAT, FELHOM_DNSMASQ, FELHOM_GUESTHOOK, FELHOM_INTERMEDIARY, FELHOM_CONTROLLERSWAP, FELHOM_STALELOCK, FELHOM_NETMOUNT, FELHOM_WG, FELHOM_SELFUPDATE, FELHOM_SSHD, FELHOM_OOB, FELHOM_PBSDR, FELHOM_SELFHEAL, FELHOM_ESCROW
# Guest-network watchdog (internal/guestnet, R-54). The guest's DHCP client is unsupervised — when it
# died on 2026-07-20 the box lost its address ~80 minutes later and went off the internet for 1h15m
# (INCIDENT-guest-dhclient-killed-2026-07-20). Four FIXED read vectors plus ONE fixed heal vector; the
# heal is the incident's own restored invocation, byte for byte. This is NOT a general `pct exec`: every
# argument after the numeric vmid is a literal, so the grant cannot be widened by anything the guest or
# the hub says. The address read is deliberately NOT duplicated here — it is already FELHOM_DNSMASQ's,
# and the same command must not be granted twice under two names.
Cmnd_Alias FELHOM_GUESTNET = \
/usr/sbin/pct exec [0-9]* -- ip route show default, \
/usr/sbin/pct exec [0-9]* -- cat /etc/network/interfaces, \
/usr/sbin/pct exec [0-9]* -- pgrep -x dhclient, \
/usr/sbin/pct exec [0-9]* -- dhclient -pf /run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases eth0
felhom-agent ALL=(root) NOPASSWD: FELHOM_MOUNT, FELHOM_DISK, FELHOM_PROVISION, FELHOM_FORMAT, FELHOM_DNSMASQ, FELHOM_GUESTHOOK, FELHOM_INTERMEDIARY, FELHOM_CONTROLLERSWAP, FELHOM_STALELOCK, FELHOM_NETMOUNT, FELHOM_WG, FELHOM_SELFUPDATE, FELHOM_SSHD, FELHOM_OOB, FELHOM_PBSDR, FELHOM_SELFHEAL, FELHOM_ESCROW, FELHOM_GUESTNET
+10
View File
@@ -129,6 +129,16 @@ var manifest = []Capability{
{"dnsmasq-guest-ip", "guest LAN IP discovery", "/usr/sbin/pct", []string{"exec", "9201", "--", "ip", "-4", "-o", "addr", "show", "dev", "eth0"}, false, ""},
{"dnsmasq-guest-domain", "guest domain discovery", "/usr/sbin/pct", []string{"exec", "9201", "--", "docker", "exec", "felhom-controller", "cat", "/opt/docker/felhom-controller/controller.yaml"}, false, ""},
// ---- Guest-network watchdog (FELHOM_GUESTNET, R-54, v0.92.0) ----
// Deliberately NOT Critical: a host that has not yet taken the new sudoers file should show
// degraded (visible, diagnosable) without paging an operator for every box on rollout day —
// the R-50b lesson that an amber fleet is background noise. The address read is covered by
// dnsmasq-guest-ip above and is not duplicated here (one command, one capability row).
{"guestnet-route", "guest default-route probe", "/usr/sbin/pct", []string{"exec", "9201", "--", "ip", "route", "show", "default"}, false, ""},
{"guestnet-ifaces", "guest interface-mode read", "/usr/sbin/pct", []string{"exec", "9201", "--", "cat", "/etc/network/interfaces"}, false, ""},
{"guestnet-dhclient-probe", "guest DHCP-client liveness probe", "/usr/sbin/pct", []string{"exec", "9201", "--", "pgrep", "-x", "dhclient"}, false, ""},
{"guestnet-heal", "guest DHCP-client restart (the 2026-07-20 heal)", "/usr/sbin/pct", []string{"exec", "9201", "--", "dhclient", "-pf", "/run/dhclient.eth0.pid", "-lf", "/var/lib/dhcp/dhclient.eth0.leases", "eth0"}, false, ""},
// ---- Controller-swap / managed auto-update (FELHOM_CONTROLLERSWAP, v0.45.0; Critical: a
// silently-broken fleet auto-update is operator-alert-worthy) ----
{"controllerswap-read", "controller-swap / managed auto-update", "/usr/sbin/pct", []string{"exec", "9201", "--", "cat", "/etc/felhom-controller-image"}, true, ""},