diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d168e1..36d035c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/configs/felhom-agent.sudoers b/configs/felhom-agent.sudoers index 04e2fae..29dcf11 100644 --- a/configs/felhom-agent.sudoers +++ b/configs/felhom-agent.sudoers @@ -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 diff --git a/internal/capability/manifest.go b/internal/capability/manifest.go index e5ab686..67b7acd 100644 --- a/internal/capability/manifest.go +++ b/internal/capability/manifest.go @@ -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, ""},