daemon: split owner daemon from root helper

Move the supported systemd path to two services: an owner-user bangerd for
orchestration and a narrow root helper for bridge/tap, NAT/resolver, dm/loop,
and Firecracker ownership. This removes repeated sudo from daily vm and image
flows without leaving the general daemon running as root.

Add install metadata, system install/status/restart/uninstall commands, and a
system-owned runtime layout. Keep user SSH/config material in the owner home,
lock file_sync to the owner home, and move daemon known_hosts handling out of
the old root-owned control path.

Route privileged lifecycle steps through typed privilegedOps calls, harden the
two systemd units, and rewrite smoke plus docs around the supported service
model.

Verified with make build, make test, make lint, and make smoke on the
supported systemd host path.
This commit is contained in:
Thales Maciel 2026-04-26 12:43:17 -03:00
parent 3edd7c6de7
commit 59e48e830b
No known key found for this signature in database
GPG key ID: 33112E6833C34679
53 changed files with 3239 additions and 726 deletions

View file

@ -427,8 +427,8 @@ func TestHealthVMReturnsHealthyForRunningGuest(t *testing.T) {
runner := &scriptedRunner{
t: t,
steps: []runnerStep{
sudoStep("", nil, "chown", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), vsockSock),
sudoStep("", nil, "chmod", "600", vsockSock),
sudoStep("", nil, "chown", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), vsockSock),
},
}
d := &Daemon{store: db, runner: runner}
@ -491,8 +491,8 @@ func TestPingVMAliasReturnsAliveForHealthyVM(t *testing.T) {
runner := &scriptedRunner{
t: t,
steps: []runnerStep{
sudoStep("", nil, "chown", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), vsockSock),
sudoStep("", nil, "chmod", "600", vsockSock),
sudoStep("", nil, "chown", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), vsockSock),
},
}
d := &Daemon{store: db, runner: runner}
@ -691,8 +691,8 @@ func TestPortsVMReturnsEnrichedPortsAndWebSchemes(t *testing.T) {
runner := &scriptedRunner{
t: t,
steps: []runnerStep{
sudoStep("", nil, "chown", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), vsockSock),
sudoStep("", nil, "chmod", "600", vsockSock),
sudoStep("", nil, "chown", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), vsockSock),
},
}
d := &Daemon{store: db, runner: runner}
@ -1148,13 +1148,92 @@ func TestRunFileSyncCopiesDirectoryRecursively(t *testing.T) {
}
}
func TestRunFileSyncAllowsTopLevelSymlinkWithinHome(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
targetDir := filepath.Join(homeDir, ".config", "gh")
if err := os.MkdirAll(targetDir, 0o755); err != nil {
t.Fatal(err)
}
targetPath := filepath.Join(targetDir, "hosts.yml")
if err := os.WriteFile(targetPath, []byte("github.com"), 0o600); err != nil {
t.Fatal(err)
}
linkPath := filepath.Join(homeDir, "gh-hosts.yml")
if err := os.Symlink(targetPath, linkPath); err != nil {
t.Skipf("symlink unsupported on this filesystem: %v", err)
}
workDisk := t.TempDir()
d := &Daemon{
runner: &filesystemRunner{t: t},
config: model.DaemonConfig{
HostHomeDir: homeDir,
FileSync: []model.FileSyncEntry{
{Host: "~/gh-hosts.yml", Guest: "~/.config/gh/hosts.yml"},
},
},
}
wireServices(d)
vm := testVM("sync-top-level-symlink-ok", "image", "172.16.0.77")
vm.Runtime.WorkDiskPath = workDisk
if err := d.ws.runFileSync(context.Background(), &vm); err != nil {
t.Fatalf("runFileSync: %v", err)
}
got, err := os.ReadFile(filepath.Join(workDisk, ".config", "gh", "hosts.yml"))
if err != nil {
t.Fatal(err)
}
if string(got) != "github.com" {
t.Fatalf("guest file = %q, want github.com", got)
}
}
func TestRunFileSyncRejectsTopLevelSymlinkOutsideHome(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
outsideDir := t.TempDir()
targetPath := filepath.Join(outsideDir, "secret.txt")
if err := os.WriteFile(targetPath, []byte("must-stay-outside"), 0o600); err != nil {
t.Fatal(err)
}
linkPath := filepath.Join(homeDir, "secret-link")
if err := os.Symlink(targetPath, linkPath); err != nil {
t.Skipf("symlink unsupported on this filesystem: %v", err)
}
workDisk := t.TempDir()
d := &Daemon{
runner: &filesystemRunner{t: t},
config: model.DaemonConfig{
HostHomeDir: homeDir,
FileSync: []model.FileSyncEntry{
{Host: "~/secret-link", Guest: "~/secret.txt"},
},
},
}
wireServices(d)
vm := testVM("sync-top-level-symlink-reject", "image", "172.16.0.78")
vm.Runtime.WorkDiskPath = workDisk
err := d.ws.runFileSync(context.Background(), &vm)
if err == nil || !strings.Contains(err.Error(), "owner home") {
t.Fatalf("runFileSync error = %v, want owner-home rejection", err)
}
if _, statErr := os.Stat(filepath.Join(workDisk, "secret.txt")); !os.IsNotExist(statErr) {
t.Fatalf("guest file exists after rejected sync (stat err = %v)", statErr)
}
}
// TestRunFileSyncSkipsNestedSymlinks pins the anti-sprawl contract:
// a symlink INSIDE a synced directory is not followed, even if the
// target holds real files. Without this, a user syncing ~/.aws with
// a ~/.aws/session -> ~/other-creds symlink would copy the unrelated
// creds into the guest. Top-level entries (the path the user
// literally named) still follow, because they explicitly asked for
// that path.
// creds into the guest. Top-level entries are resolved separately:
// they may still follow, but only when the real target stays under
// the configured owner home.
func TestRunFileSyncSkipsNestedSymlinks(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
@ -1543,8 +1622,8 @@ func TestStopVMFallsBackToForcedCleanupAfterGracefulTimeout(t *testing.T) {
scriptedRunner: &scriptedRunner{
t: t,
steps: []runnerStep{
sudoStep("", nil, "chown", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), apiSock),
sudoStep("", nil, "chmod", "600", apiSock),
sudoStep("", nil, "chown", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), apiSock),
{call: runnerCall{name: "pgrep", args: []string{"-n", "-f", apiSock}}, out: []byte(strconv.Itoa(fake.Process.Pid) + "\n")},
sudoStep("", nil, "kill", "-KILL", strconv.Itoa(fake.Process.Pid)),
},