ac90169a5d
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
139 lines
6.5 KiB
Go
139 lines
6.5 KiB
Go
package capability
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
// R-861 (agent v0.146.0): the attacks the old globs let through, each as the exact argv a compromised agent would
|
|
// send. NONE may match any entry of the new sudoers. The same list ran against the REAL sudo 1.9.16 (a throwaway
|
|
// container, and `sudo -l -U felhom-agent` on both demo boxes after the bundle): audits/hub-safety-2026-10-05/partF/.
|
|
//
|
|
// RED-PROOF: run this list against the v0.145.0 sudoers (git show v0.145.0:configs/felhom-agent.sudoers) → most of
|
|
// these MATCH (recorded in the same evidence folder).
|
|
var r861Injections = []string{
|
|
// a raw host disk for a guest (pct options smuggled through a vmid glob)
|
|
"/usr/sbin/pct set 9201 --dev0 /dev/sda -onboot 1",
|
|
"/usr/sbin/pct set 9201 --dev0 /dev/sda -mp8 /mnt/felhom-drives",
|
|
"/usr/sbin/pct set 9201 --delete mp0 --dev0 /dev/sda",
|
|
"/usr/sbin/pct set 9201 -mp0 /var/lib/felhom-agent/guests/9201/bootstrap,mp=/x --dev0 /dev/sda",
|
|
// a bind mount over /etc through traversal
|
|
"/usr/bin/mount --bind /mnt/../var/lib/felhom-agent/x/felhom-data /mnt/felhom-drives/x",
|
|
"/usr/bin/mount --bind /mnt/a/felhom-data /mnt/felhom-drives/../../etc/sudoers.d",
|
|
"/usr/bin/umount /mnt/felhom-drives/x /",
|
|
"/usr/bin/chown 100000:100000 /mnt/a/felhom-data /etc/shadow",
|
|
"/usr/bin/mkdir -p /mnt/felhom-drives/x /etc/systemd/system/evil.mount",
|
|
// root-read files the agent writes: gone as `install` lines
|
|
"/usr/bin/install -o root -g root -m 0644 -- /var/lib/felhom-agent/units/x.mount /etc/systemd/system/etc-sudoers.d.mount",
|
|
"/usr/bin/install -m 0755 -- /tmp/felhom-guest-hook-1.sh /var/lib/vz/snippets/felhom-guest-hook.sh",
|
|
"/usr/bin/install -m 0755 -- /tmp/felhom-shared-parent-1.sh /usr/local/sbin/felhom-shared-parent.sh",
|
|
"/usr/bin/install -m 0644 /tmp/felhom-resolver-1.conf /etc/dnsmasq.d/felhom-x.conf",
|
|
"/usr/bin/install -o root -g root -m 0600 -- /var/lib/felhom-agent/wg/wg-felhom.conf /etc/wireguard/wg-felhom.conf",
|
|
"/usr/bin/install -o root -g root -m 0644 -- /var/lib/felhom-agent/felhom-sshd/sshd_config /etc/felhom-sshd/sshd_config",
|
|
// the unsigned binary flip
|
|
"/usr/local/sbin/felhom-selfupdate-guarded apply /var/lib/felhom-agent/selfupdate/felhom-agent-9.9.9 0000000000000000000000000000000000000000000000000000000000000000",
|
|
// enabling or removing anything that is not ours
|
|
"/usr/bin/systemctl enable --now -- mnt-hdd_1.mount evil.service",
|
|
"/usr/bin/systemctl enable --now -- etc-sudoers.d.mount",
|
|
"/usr/bin/rm -f /etc/systemd/system/mnt-felhomx /etc/passwd",
|
|
"/usr/bin/rm -f /etc/dnsmasq.d/felhom-x.conf /etc/shadow",
|
|
"/usr/bin/rmdir /mnt/felhom-drives/x /etc",
|
|
// nftables commands chained after a set element
|
|
"/usr/sbin/nft add element inet felhom_oob operator_ips { 10.77.0.250 } ; flush ruleset",
|
|
// extra options to read-only tools
|
|
"/usr/sbin/smartctl -a -j /dev/sda -s off",
|
|
"/usr/sbin/lvs --reportformat json --units b -o lv_name,data_percent,metadata_percent -- pve/data --config x",
|
|
"/usr/sbin/pct exec 9201 --keep-env -- docker inspect -f x felhom-controller",
|
|
"/usr/sbin/pct unlock 9201 --whatever",
|
|
// the checker with a path it must never take
|
|
"/usr/local/sbin/felhom-priv-apply unit ../../etc/x.mount",
|
|
"/usr/local/sbin/felhom-priv-apply dnsmasq /etc/shadow felhom-x.conf",
|
|
"/usr/local/sbin/felhom-priv-apply wg /etc/shadow",
|
|
// R-861 (a) A1 (decision 165): the agent wrote ANY image ref into the guest by `tee` — now only the root verb may
|
|
"/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",
|
|
}
|
|
|
|
func TestSudoersRefusesTheR861Injections(t *testing.T) {
|
|
data, err := os.ReadFile(sudoersPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries := parseSudoersEntries(t, string(data))
|
|
for _, c := range r861Injections {
|
|
if matchesAny(c, entries) {
|
|
t.Errorf("the sudoers still allows: %s", c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// R-444: the weekly trim's grant is ONE exact shape — `pct fstrim <vmid>` — and nothing smuggled after it.
|
|
// The manifest entry (guest-fstrim) proves the real call is still allowed (TestManifestCoveredBySudoers); this
|
|
// pins the other direction. RED-PROOF: write the rule as the glob `/usr/sbin/pct fstrim [0-9]*` → every decoy
|
|
// below with a trailing argument matches (the glob's `*` eats spaces).
|
|
func TestSudoersFstrimRuleIsExact(t *testing.T) {
|
|
data, err := os.ReadFile(sudoersPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries := parseSudoersEntries(t, string(data))
|
|
if !matchesAny("/usr/sbin/pct fstrim 9201", entries) {
|
|
t.Fatal("the sudoers does not allow `pct fstrim 9201` — the weekly trim cannot run")
|
|
}
|
|
for _, c := range []string{
|
|
"/usr/sbin/pct fstrim 9201 --ignore-mountpoints",
|
|
"/usr/sbin/pct fstrim 9201 --ignore-mountpoints 1",
|
|
"/usr/sbin/pct fstrim 9201; x",
|
|
"/usr/sbin/pct fstrim 9201 9202",
|
|
"/usr/sbin/pct fstrim 92a1",
|
|
"/usr/sbin/pct fstrim ",
|
|
"/usr/sbin/pct fstrim -- 9201",
|
|
"/usr/sbin/pct destroy 9201",
|
|
"/usr/sbin/pct destroy 9201 --purge",
|
|
} {
|
|
if matchesAny(c, entries) {
|
|
t.Errorf("the sudoers allows a command the trim rule must not: %q", c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// R-861 (a) A1: the managed controller update still has its route — the root verb, one numeric vmid.
|
|
func TestSudoersAllowsTheControllerImageVerb(t *testing.T) {
|
|
data, err := os.ReadFile(sudoersPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !matchesAny("/usr/local/sbin/felhom-priv-apply controller-image 9201", parseSudoersEntries(t, string(data))) {
|
|
t.Fatal("the sudoers does not allow `felhom-priv-apply controller-image 9201` — a managed controller update cannot write its image")
|
|
}
|
|
}
|
|
|
|
// R-861 (b) B2 (decision 165, hygiene): felhom-op's `pct start|stop|unlock` grants are ONE numeric vmid each. The old
|
|
// glob `[0-9]*` eats spaces, so `pct stop 9201 --skiplock 1` and two vmids matched.
|
|
// RED-PROOF: on the pre-B2 felhom-op.sudoers (`/usr/sbin/pct stop [0-9]*`) the decoys match.
|
|
func TestFelhomOpSudoersPctIsExact(t *testing.T) {
|
|
data, err := os.ReadFile("../../configs/felhom-op.sudoers")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries := parseSudoersEntries(t, string(data))
|
|
for _, ok := range []string{"/usr/sbin/pct start 9201", "/usr/sbin/pct stop 9201", "/usr/sbin/pct unlock 9201", "/usr/sbin/pct list"} {
|
|
if !matchesAny(ok, entries) {
|
|
t.Errorf("felhom-op lost a repair verb: %s", ok)
|
|
}
|
|
}
|
|
for _, bad := range []string{
|
|
"/usr/sbin/pct stop 9201 --skiplock 1",
|
|
"/usr/sbin/pct start 9201 9202",
|
|
"/usr/sbin/pct unlock 9201 --whatever",
|
|
"/usr/sbin/pct start 92a1",
|
|
"/usr/sbin/pct stop ",
|
|
"/usr/sbin/pct destroy 9201",
|
|
} {
|
|
if matchesAny(bad, entries) {
|
|
t.Errorf("felhom-op's sudoers allows %q", bad)
|
|
}
|
|
}
|
|
}
|