Host power-off for the household (R-998): POST /host/poweroff
gates / gates (push) Successful in 34s

The guest's controller asks; the agent refuses in the first 10 min after boot, a second time in
one boot (persisted, keyed by /proc/stat btime), and while a backup, restore test, OS step,
controller update, drive format, NAS verify, escrow ceremony or vzdump runs (each a named 409
reason). On acceptance: the record first, a host report carrying deliberate_off accepted by the
hub (bounded 60 s), then pct shutdown --timeout 120, then systemctl poweroff.

Sudoers: two exact vectors (pct ^shutdown N --timeout N$, systemctl poweroff) in FELHOM_POWEROFF;
nine injection cases. New TestEveryAliasIsGranted: a defined alias missing from the grant line
passed the coverage check before. Every new test red-proofed.
This commit is contained in:
2026-10-13 19:29:52 +02:00
parent 86adbcaf8a
commit 3403c87c80
15 changed files with 792 additions and 2 deletions
+2
View File
@@ -115,6 +115,8 @@ var manifest = []Capability{
{"guesthook-register", "pre-start hook register", "/usr/sbin/pct", []string{"set", "9201", "--hookscript", "local:snippets/felhom-guest-hook.sh"}, false, ""},
{"guesthook-delete-mp", "dead mountpoint slot delete (C1 net)", "/usr/sbin/pct", []string{"set", "9201", "--delete", "mp0"}, false, ""},
{"guest-reboot", "enroll activate-binds reboot", "/usr/sbin/pct", []string{"reboot", "9201"}, false, ""},
{"guest-shutdown", "household power-off: guest shutdown (R-998)", "/usr/sbin/pct", []string{"shutdown", "9201", "--timeout", "120"}, false, ""},
{"host-poweroff", "household power-off: host power-off (R-998)", "/usr/bin/systemctl", []string{"poweroff"}, false, ""},
// ---- LAN split-horizon resolver (dnsmasq) ----
{"dnsmasq-install", "dnsmasq package install", "/usr/bin/apt-get", []string{"install", "-y", "-q", "dnsmasq"}, false, ""},
+36
View File
@@ -311,3 +311,39 @@ func TestWGCapabilityCriticality(t *testing.T) {
}
}
}
// TestEveryAliasIsGranted closes a gap found while adding FELHOM_POWEROFF (R-998): the coverage check above reads
// every Cmnd_Alias body whether or not the alias appears on the grant line, so a manifest command whose alias was
// never GRANTED still passed — and failed with „a password is required" on the box. Every defined alias must be
// named on the `felhom-agent ALL=(root) NOPASSWD:` line. Red-proof: drop FELHOM_POWEROFF from that line.
func TestEveryAliasIsGranted(t *testing.T) {
data, err := os.ReadFile(sudoersPath)
if err != nil {
t.Fatalf("read sudoers: %v", err)
}
var defined []string
granted := map[string]bool{}
grantLines := 0
for _, ln := range strings.Split(string(data), "\n") {
tr := strings.TrimSpace(ln)
if strings.HasPrefix(tr, "Cmnd_Alias ") {
if f := strings.Fields(tr); len(f) >= 2 {
defined = append(defined, f[1])
}
}
if strings.HasPrefix(tr, "felhom-agent ") && strings.Contains(tr, "NOPASSWD:") {
grantLines++
for _, a := range strings.Split(tr[strings.Index(tr, "NOPASSWD:")+len("NOPASSWD:"):], ",") {
granted[strings.TrimSpace(a)] = true
}
}
}
if grantLines == 0 || len(defined) < 10 {
t.Fatalf("parsed %d grant line(s), %d aliases — parser broke", grantLines, len(defined))
}
for _, a := range defined {
if !granted[a] {
t.Errorf("Cmnd_Alias %s is defined but never granted to felhom-agent", a)
}
}
}
@@ -53,6 +53,16 @@ var r861Injections = []string{
"/usr/sbin/pct exec 9201 -- tee /etc/felhom-controller-image",
"/usr/local/sbin/felhom-priv-apply controller-image 9201 9202",
"/usr/local/sbin/felhom-priv-apply controller-image 9201;id",
// R-998 (host power-off): the two new grants take exactly their fixed shape — no extra option, no other target
"/usr/sbin/pct shutdown 9201 --timeout 120 --forceStop 1",
"/usr/sbin/pct shutdown 9201 --forceStop 1 --timeout 120",
"/usr/sbin/pct shutdown 9201",
"/usr/sbin/pct shutdown 9201 --timeout -1",
"/usr/bin/systemctl poweroff --force",
"/usr/bin/systemctl poweroff -ff",
"/usr/bin/systemctl reboot",
"/usr/bin/systemctl halt",
"/usr/bin/systemctl kexec",
}
func TestSudoersRefusesTheR861Injections(t *testing.T) {