//go:build linux && rootrunner package host import ( "bytes" "context" "crypto/ed25519" "encoding/json" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostfence" ) func activationRootFixture(t *testing.T) string { t.Helper() if os.Geteuid() != 0 { t.Fatal("real root required") } d, err := os.MkdirTemp("/var/lib", "carlos-activation-test-") if err != nil { t.Fatal(err) } t.Cleanup(func() { os.RemoveAll(d) }) return d } func TestRootActivationSecrets(t *testing.T) { d := activationRootFixture(t) binding := hostactors.CensusKeyBinding{Bucket: "bucket", Box: "leo", Enrollment: strings.Repeat("a", 32)} p := filepath.Join(d, "host-evidence.key") result, err := installActivationEvidenceKey(p, binding, false) if err != nil { t.Fatal(err) } key, err := readClaimEvidenceKey(p) if err != nil { t.Fatal(err) } wire, _ := json.Marshal(result.Binding) if !ed25519.Verify(key.Public().(ed25519.PublicKey), wire, result.Proof) { t.Fatal("proof does not bind installed key") } again, err := installActivationEvidenceKey(p, binding, false) if err != nil || !bytes.Equal(again.Binding.PublicKey, result.Binding.PublicKey) { t.Fatal("replay rotated key", err) } st, _ := os.Stat(p) if st.Mode().Perm() != 0600 { t.Fatal(st.Mode()) } before, _ := os.ReadFile(p) _, err = installActivationEvidenceKey(p, binding, true) if err != nil { t.Fatal(err) } after, _ := os.ReadFile(p) if !bytes.Equal(before, after) { t.Fatal("dry run changed key") } dry := filepath.Join(d, "absent.key") if _, err = installActivationEvidenceKey(dry, binding, true); err != nil { t.Fatal(err) } if _, err = os.Lstat(dry); !os.IsNotExist(err) { t.Fatal("dry run created key") } for _, kind := range []string{"symlink", "hardlink", "writable-parent", "mode"} { t.Run(kind, func(t *testing.T) { parent := filepath.Join(d, kind) if err := os.Mkdir(parent, 0700); err != nil { t.Fatal(err) } target := filepath.Join(parent, "key") switch kind { case "symlink": os.Symlink(p, target) case "hardlink": os.Link(p, target) case "writable-parent": os.Chmod(parent, 0777) case "mode": os.WriteFile(target, before, 0644) } if _, err := installActivationEvidenceKey(target, binding, false); err == nil { t.Fatal("unsafe path accepted") } os.Remove(target) os.Chmod(parent, 0700) }) } } func TestRootActivationFenceCredentials(t *testing.T) { d := activationRootFixture(t) source := filepath.Join(d, "source.json") target := filepath.Join(d, "credentials.json") raw := []byte(`{"role":"arn:aws:iam::123456789012:role/fence","region":"eu-west-1","bucket":"bucket","access_key":"fixture","secret_key":"fixture-secret"}`) if err := os.WriteFile(source, raw, 0600); err != nil { t.Fatal(err) } scope := activationFenceScope{Bucket: "bucket", Region: "eu-west-1", Role: "arn:aws:iam::123456789012:role/fence"} if err := installActivationFenceCredentials(source, target, scope, true); err != nil { t.Fatal(err) } if _, err := os.Stat(target); !os.IsNotExist(err) { t.Fatal("dry run wrote credentials") } if err := installActivationFenceCredentials(source, target, scope, false); err != nil { t.Fatal(err) } st, _ := os.Stat(target) if st.Mode().Perm() != 0600 { t.Fatal(st.Mode()) } if err := installActivationFenceCredentials(source, target, scope, false); err != nil { t.Fatal(err) } scope.Bucket = "wrong" if err := installActivationFenceCredentials(source, target, scope, false); err == nil { t.Fatal("scope mismatch accepted") } scope.Bucket = "bucket" os.WriteFile(source, bytes.ReplaceAll(raw, []byte("fixture-secret"), []byte("replacement")), 0600) if err := installActivationFenceCredentials(source, target, scope, false); err == nil { t.Fatal("rotation accepted") } got, _ := os.ReadFile(target) if !bytes.Equal(got, raw) { t.Fatal("existing credentials replaced") } } func TestRootActivationControlInstaller(t *testing.T) { d := activationRootFixture(t) env := filepath.Join(d, "host.env") unitDir := filepath.Join(d, "units") os.Mkdir(unitDir, 0755) os.WriteFile(env, []byte("CARLOS_HOST_CLAIM=1\nCARLOS_HOST_CLAIM_ORIGINS=1\n"), 0600) for _, name := range rootMigrationOfflineUnits { if err := os.Symlink("/dev/null", filepath.Join(unitDir, name)); err != nil { t.Fatal(err) } } calls := []string{} ctl := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")); return "", nil } if err := installActivationControl(env, unitDir, true, ctl); err != nil { t.Fatal(err) } if len(calls) != 0 { t.Fatal("dry run called systemd") } if _, err := os.Stat(filepath.Join(unitDir, claimControlUnit)); !os.IsNotExist(err) { t.Fatal("dry run installed") } if err := installActivationControl(env, unitDir, false, ctl); err != nil { t.Fatal(err) } got, err := os.ReadFile(filepath.Join(unitDir, claimControlUnit)) if err != nil { t.Fatal(err) } for _, want := range []string{"StateDirectory=carlos-claims\n", "MemoryMax=256M\n", claimControlEnvironment} { if !bytes.Contains(got, []byte(want)) { t.Fatal("missing", want) } } if strings.Join(calls, ",") != "daemon-reload" { t.Fatal("unexpected startup", calls) } for _, name := range rootMigrationOfflineUnits { target, err := os.Readlink(filepath.Join(unitDir, name)) if err != nil || target != "/dev/null" { t.Fatal("mask changed", name, err) } } os.WriteFile(env, []byte("CARLOS_HOST_CLAIM=1\nmalformed\n"), 0600) if err := installActivationControl(env, unitDir, false, ctl); err == nil { t.Fatal("malformed config accepted") } after, _ := os.ReadFile(filepath.Join(unitDir, claimControlUnit)) if !bytes.Equal(got, after) { t.Fatal("unit changed after refusal") } } func TestRootActivationConsoleInstaller(t *testing.T) { d := activationRootFixture(t) env := filepath.Join(d, "claim-console.env") drop := filepath.Join(d, "console.service.d") calls := []string{} ctl := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")); return "", nil } raw := []byte("CARLOS_CONSOLE_ID=dev-console\nCONSOLE_HOME_REGION=ie\nCARLOS_HOST_CLAIM=\n") if err := installActivationConsole(env, drop, raw, true, ctl); err != nil { t.Fatal(err) } if _, err := os.Stat(env); !os.IsNotExist(err) { t.Fatal("dry run changed config") } if err := installActivationConsole(env, drop, raw, false, ctl); err != nil { t.Fatal(err) } got, _ := os.ReadFile(env) if !bytes.Equal(got, raw) { t.Fatal("config mismatch") } if strings.Join(calls, ",") != "daemon-reload" { t.Fatal("started console", calls) } os.WriteFile(env, []byte("operator-change\n"), 0600) if err := installActivationConsole(env, drop, raw, false, ctl); err == nil { t.Fatal("overwrote operator config") } } func TestRootActivationControlSystemd(t *testing.T) { activationRootFixture(t) paths := []string{"/etc/carlos", "/etc/systemd/system/" + claimControlUnit, "/etc/systemd/system/.claims-activation.lock"} for _, name := range rootMigrationOfflineUnits { paths = append(paths, "/etc/systemd/system/"+name) } for _, path := range paths { if _, err := os.Lstat(path); !os.IsNotExist(err) { t.Fatal("pre-existing fixture path", path, err) } } t.Cleanup(func() { for _, path := range paths { if err := os.RemoveAll(path); err != nil { t.Error(err) } } if raw, err := claimSystemctl(context.Background(), "daemon-reload"); err != nil { t.Error(string(raw), err) } }) if err := os.Mkdir("/etc/carlos", 0700); err != nil { t.Fatal(err) } for _, name := range rootMigrationOfflineUnits { if err := os.Symlink("/dev/null", "/etc/systemd/system/"+name); err != nil { t.Fatal(err) } } if raw, err := claimSystemctl(t.Context(), "daemon-reload"); err != nil { t.Fatal(string(raw), err) } if err := os.WriteFile("/etc/carlos/host.env", []byte("CARLOS_HOST_CLAIM=1\nCARLOS_HOST_CLAIM_ORIGINS=1\n"), 0600); err != nil { t.Fatal(err) } var result bytes.Buffer if err := runSystemClaims([]string{"install-control"}, &result); err != nil { t.Fatal(err) } raw, err := claimSystemctl(t.Context(), "cat", claimControlUnit) if err != nil || !bytes.Contains(raw, []byte("MemoryMax=256M")) { t.Fatal(string(raw), err) } if err = verifyOfflineRootMigration(t.Context()); err != nil { t.Fatal("migration masks changed", err) } raw, err = claimSystemctl(t.Context(), "show", claimControlUnit, "--property=ActiveState", "--value") if err != nil || strings.TrimSpace(string(raw)) != "inactive" { t.Fatal("control started", string(raw), err) } } func TestRootActivationFenceStoreReceipt(t *testing.T) { if _, err := os.Lstat("/etc/carlos"); !os.IsNotExist(err) { t.Fatal("pre-existing config directory", err) } if err := os.Mkdir("/etc/carlos", 0700); err != nil { t.Fatal(err) } defer os.RemoveAll("/etc/carlos") source := "/etc/carlos/fence-source.json" target := "/etc/carlos/fence-writer/credentials.json" raw := []byte(`{"role":"arn:aws:iam::123456789012:role/fence","region":"eu-west-1","bucket":"bucket","access_key":"fixture","secret_key":"fixture-secret"}`) if err := os.WriteFile(source, raw, 0600); err != nil { t.Fatal(err) } scope := activationFenceScope{Bucket: "bucket", Region: "eu-west-1", Role: "arn:aws:iam::123456789012:role/fence"} if err := installActivationFenceCredentials(source, target, scope, false); err != nil { t.Fatal(err) } if _, err := hostfence.OpenRootFenceStore(t.Context(), "bucket"); err != nil { t.Fatal("installed credentials refused", err) } marker := "/etc/carlos/fence-writer/credentials.installation.json" if err := os.Remove(marker); err != nil { t.Fatal(err) } if _, err := hostfence.OpenRootFenceStore(t.Context(), "bucket"); err == nil { t.Fatal("uninstalled 0600 credentials accepted") } if err := installActivationFenceCredentials(source, target, scope, false); err != nil { t.Fatal("interrupted receipt not resumed", err) } os.Remove(marker) os.Chmod(target, 0400) if _, err := hostfence.OpenRootFenceStore(t.Context(), "bucket"); err != nil { t.Fatal("legacy 0400 credentials changed", err) } } func TestRootActivationEvidenceBoundKey(t *testing.T) { d := activationRootFixture(t) path := filepath.Join(d, "evidence.key") binding := hostactors.CensusKeyBinding{Bucket: "bucket", Box: "leo", Enrollment: strings.Repeat("a", 32)} proof, err := installActivationEvidenceKey(path, binding, false) if err != nil { t.Fatal(err) } bound := proof.Binding if _, err = installActivationEvidenceKey(path, bound, false); err != nil { t.Fatal("bound replay", err) } if _, err = installActivationEvidenceKey(filepath.Join(d, "missing.key"), bound, false); err == nil { t.Fatal("replaced a lost bound key") } if _, err = os.Lstat(filepath.Join(d, "missing.key")); !os.IsNotExist(err) { t.Fatal("created replacement key") } bound.PublicKey[0] ^= 1 if _, err = installActivationEvidenceKey(path, bound, false); err == nil { t.Fatal("accepted another bound key") } }