v0.328.0 (unreleased): the security review's four findings — the entry is bound to its image and start; symlinks out of a root; the missed classes; roots only under the drive namespace or system data
gates / gates (push) Successful in 42s

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-17 16:42:18 +02:00
parent 6d90374f63
commit 24d937d6e4
5 changed files with 202 additions and 12 deletions
@@ -267,3 +267,84 @@ func TestAllowList_CatalogCopyIsIdentical(t *testing.T) {
t.Fatalf("%s differs from internal/privpolicy/allowlist.json — change both in the same session", p)
}
}
// Security review 2026-10-17, finding 1: an allow-list entry holds only for its image and the image's own start. The
// same allowed socket under a swapped image, or with an entrypoint or command added, is refused.
func TestAdmit_TheAllowedSettingIsBoundToItsImageAndStart(t *testing.T) {
for name, compose := range map[string]string{
"another image": strings.Replace(homepage, "ghcr.io/gethomepage/homepage:v1.13.2", "docker.io/evil/homepage:v1.13.2", 1),
"added entrypoint": strings.Replace(homepage, " volumes:", " entrypoint: [\"sh\", \"-c\", \"id\"]\n volumes:", 1),
"added command": strings.Replace(homepage, " volumes:", " command: sh -c id\n volumes:", 1),
} {
t.Run(name, func(t *testing.T) {
if err := admit(t, "homepage", compose, env()); err == nil {
t.Fatal("admitted")
}
})
}
// Positive control: a new TAG or a digest of the same repository is still homepage.
bumped := strings.Replace(homepage, "homepage:v1.13.2", "homepage:v1.14.0@sha256:"+strings.Repeat("a", 64), 1)
if err := admit(t, "homepage", bumped, env()); err != nil {
t.Fatalf("a version bump of the allowed image was refused: %v", err)
}
}
// Finding 2: on the box, a bind source inside a root that is a symlink OUT of it is outside.
func TestAdmit_ASymlinkOutOfARootIsOutside(t *testing.T) {
root := t.TempDir()
ns := filepath.Join(root, "mnt", "felhom-drives", "d", "felhom-data")
if err := os.MkdirAll(filepath.Join(ns, "userdata", "media"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.Symlink("/etc", filepath.Join(ns, "userdata", "escape")); err != nil {
t.Fatal(err)
}
// The roots must be under the drive namespace by NAME; the temp dir stands in via a Resolve that maps the names.
e := map[string]string{"HDD_PATH": "/mnt/felhom-drives/d/felhom-data", "USERDATA_PATH": "/mnt/felhom-drives/d/felhom-data/userdata"}
p := Policy{Resolve: func(s string) (string, error) {
r, err := filepath.EvalSymlinks(filepath.Join(root, s))
if err != nil {
return "", err
}
if rel, rerr := filepath.Rel(root, r); rerr == nil && !strings.HasPrefix(rel, "..") {
return "/" + rel, nil
}
return r, nil
}}
esc := "services:\n app:\n image: x/y:1\n volumes:\n - ${USERDATA_PATH}/escape:/x\n"
if err := Admit("app", []byte(esc), e, p); err == nil {
t.Fatal("a symlink to /etc inside the userdata root was admitted")
}
ok := "services:\n app:\n image: x/y:1\n volumes:\n - ${USERDATA_PATH}/media:/m\n - ${USERDATA_PATH}/not-yet:/n\n"
if err := Admit("app", []byte(ok), e, p); err != nil {
t.Fatalf("a real directory (and one not created yet) inside the root was refused: %v", err)
}
}
// Finding 3: the classes the first version missed.
func TestAdmit_TheLaterClasses(t *testing.T) {
for name, add := range map[string]string{
"cgroup_parent": " cgroup_parent: /\n",
"runtime": " runtime: runc-unsafe\n",
"gpus": " gpus: all\n",
"device request": " deploy:\n resources:\n reservations:\n devices:\n - capabilities: [gpu]\n",
"provider": " provider:\n type: model\n",
"post_start": " post_start:\n - command: id\n privileged: true\n",
} {
t.Run(name, func(t *testing.T) {
compose := strings.Replace(base, " image: example/app:1.0\n", " image: example/app:1.0\n"+add, 1)
if err := admit(t, "app", compose, env()); err == nil {
t.Fatal("admitted")
}
})
}
}
// Finding 4: a data root lives under the drive namespace or the system data path, not anywhere under /mnt.
func TestAdmit_ARootElsewhereUnderMntIsNoRoot(t *testing.T) {
e := env()
e["HDD_PATH"] = "/mnt/sys_other/x"
if err := admit(t, "app", base, e); err == nil {
t.Fatal("a root outside /mnt/felhom-drives/ and the system data path was accepted")
}
}