R-861: narrow the agent's root grants — exact sudo patterns, felhom-priv-apply content checker, fixed hook/parent files in the bundle, signed self-update verified as root, escrow root reads pinned
gates / gates (push) Successful in 20s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-05 12:00:43 +02:00
parent 61345790ed
commit 6ab1e7c56c
39 changed files with 1657 additions and 400 deletions
+10 -3
View File
@@ -101,6 +101,10 @@ type MountSpec struct {
type Binaries struct {
Systemctl string
Install string
// PrivApply is the root content checker (R-861, agent v0.146.0): a unit file reaches /etc/systemd/system only
// through `felhom-priv-apply unit <name>`, which reads the staged copy from /var/lib/felhom-agent/units/ and
// refuses anything the renderers here never produce (a bind over /etc, a Where outside /mnt, …).
PrivApply string
Smartctl string
Lvs string
Blkid string // device signature probe (8C data-bearing detection)
@@ -119,6 +123,9 @@ func (b Binaries) withDefaults() Binaries {
if b.Install == "" {
b.Install = "/usr/bin/install"
}
if b.PrivApply == "" {
b.PrivApply = "/usr/local/sbin/felhom-priv-apply"
}
if b.Smartctl == "" {
b.Smartctl = "/usr/sbin/smartctl"
}
@@ -246,9 +253,9 @@ func (h *SudoHostOps) EnsureMount(ctx context.Context, spec MountSpec) error {
return fmt.Errorf("storage: staging unit: %w", err)
}
dest := filepath.Join(h.unitDir, unitName)
// install as root (atomic copy with fixed mode/owner) — fixed arg vector.
if err := h.run(ctx, h.bins.Install, "-o", "root", "-g", "root", "-m", "0644", "--", stagePath, dest); err != nil {
// R-861: the root checker installs it (fixed source dir, fixed destination dir, content checked) — never a
// plain `install` of a file the agent wrote.
if err := h.run(ctx, h.bins.PrivApply, "unit", unitName); err != nil {
return fmt.Errorf("storage: installing unit %s: %w", unitName, err)
}
if err := h.run(ctx, h.bins.Systemctl, "daemon-reload"); err != nil {
+3 -2
View File
@@ -55,8 +55,9 @@ func TestHostOps_MountLifecycle(t *testing.T) {
if len(rr.calls) != 3 {
t.Fatalf("expected 3 commands, got %d: %v", len(rr.calls), rr.calls)
}
if rr.calls[0][0] != "/usr/bin/install" || !contains(rr.calls[0], "0644") {
t.Errorf("call[0] not the install: %v", rr.calls[0])
// R-861: the unit is installed by the root content checker, named — never a plain `install` of a staged path.
if rr.calls[0][0] != "/usr/local/sbin/felhom-priv-apply" || len(rr.calls[0]) != 3 || rr.calls[0][1] != "unit" {
t.Errorf("call[0] not the checker's unit install: %v", rr.calls[0])
}
if !contains(rr.calls[1], "daemon-reload") {
t.Errorf("call[1] not daemon-reload: %v", rr.calls[1])
+1 -1
View File
@@ -100,7 +100,7 @@ func TestMigrateNetworkUnits_RewritesDriftedOnceIdempotent(t *testing.T) {
if strings.Contains(joined, "daemon-reload") {
reloads++
}
if strings.Contains(joined, "install") {
if strings.Contains(joined, "felhom-priv-apply unit") {
installs++
}
}
+7 -3
View File
@@ -236,9 +236,13 @@ func (s NetworkMountSpec) mountOptions() string {
"file_mode=0664",
"dir_mode=0775",
"_netdev",
"nosuid",
"nodev",
}, ",")
}
return "vers=4.1,soft,timeo=50,retrans=2,noatime,_netdev,retry=0"
// R-861 (v0.146.0): nosuid,nodev — a set-uid file or a device node on a server outside the box must never act on
// the host. felhom-priv-apply refuses a network unit without them.
return "vers=4.1,soft,timeo=50,retrans=2,noatime,_netdev,retry=0,nosuid,nodev"
}
// renderNetworkMountUnit builds the .mount unit (triggered by the .automount; deliberately NO [Install]
@@ -409,8 +413,8 @@ func (h *SudoHostOps) installUnit(ctx context.Context, unitName, content string)
if err := os.WriteFile(stagePath, []byte(content), 0o644); err != nil {
return fmt.Errorf("netmount: staging unit %s: %w", unitName, err)
}
dest := filepath.Join(h.unitDir, unitName)
if err := h.run(ctx, h.bins.Install, "-o", "root", "-g", "root", "-m", "0644", "--", stagePath, dest); err != nil {
// R-861: through the root checker (felhom-priv-apply unit), never a plain install of an agent-written file.
if err := h.run(ctx, h.bins.PrivApply, "unit", unitName); err != nil {
return fmt.Errorf("netmount: installing unit %s: %w", unitName, err)
}
return nil
+1 -1
View File
@@ -264,7 +264,7 @@ func TestEnsureNetworkMount_Commands(t *testing.T) {
switch {
case strings.Contains(joined, "mkdir") && strings.Contains(joined, "/mnt/felhom-drives/media"):
sawMkdir = true
case strings.Contains(joined, "install"):
case strings.Contains(joined, "felhom-priv-apply unit"):
installs++
case strings.Contains(joined, "daemon-reload"):
sawReload = true
@@ -0,0 +1,43 @@
package storage
import (
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-agent/internal/privapplytest"
)
// R-861: every unit the agent renders is one the root checker installs — the name it computes, the Where, the
// options. RED-PROOF: drop "nosuid","nodev" from mountOptions → the network cases are REFUSED [U5].
func TestPrivApply_AcceptsTheRenderedUnits(t *testing.T) {
local := MountSpec{Name: "x", UUID: "91d2dc2d-2d28-4929-9bdd-3e11fa2f41ae", Where: "/mnt/hdd_1", FSType: "ext4"}
name, _ := UnitNameForMount(local.Where)
if got := privapplytest.Check(t, "unit", name, renderMountUnit(local)); got != "OK" {
t.Errorf("local unit %s: %s", name, got)
}
local.FSType = ""
if got := privapplytest.Check(t, "unit", name, renderMountUnit(local)); got != "OK" {
t.Errorf("local unit without Type: %s", got)
}
for _, spec := range []NetworkMountSpec{
{Name: "media", Protocol: ProtocolNFS, Server: "10.0.0.5", Export: "/srv/media", MappedUID: 1000, MappedGID: 1000},
{Name: "photos", Protocol: ProtocolSMB, Server: "nas.lan", Export: "photos", CredsRef: "/etc/felhom/netmount/photos.cred", MappedUID: 1000, MappedGID: 1000},
} {
mu, err := UnitNameForMount(spec.Where())
if err != nil {
t.Fatal(err)
}
if got := privapplytest.Check(t, "unit", mu, renderNetworkMountUnit(spec)); got != "OK" {
t.Errorf("%s .mount: %s", spec.Name, got)
}
au := strings.TrimSuffix(mu, ".mount") + ".automount"
if got := privapplytest.Check(t, "unit", au, renderNetworkAutomountUnit(spec)); got != "OK" {
t.Errorf("%s .automount: %s", spec.Name, got)
}
}
// control: the checker is really looking — a unit over /etc is refused
evil := strings.Replace(renderMountUnit(MountSpec{UUID: local.UUID, Where: "/mnt/hdd_1"}), "Where=/mnt/hdd_1", "Where=/etc/sudoers.d", 1)
if got := privapplytest.Check(t, "unit", name, evil); !strings.HasPrefix(got, "REFUSED") {
t.Fatalf("control: a unit over /etc/sudoers.d was not refused: %s", got)
}
}