//go:build linux package host import ( "bytes" "log" "os" "path/filepath" "strings" "testing" "golang.org/x/sys/unix" ) func TestOffcloudCredentialsCreationAndRepair(t *testing.T) { if os.Geteuid() != 0 { t.Skip("root required for root:service ownership") } const serviceID = 65534 for _, owner := range []int{0, serviceID} { t.Run(map[int]string{0: "creation", serviceID: "repair"}[owner], func(t *testing.T) { path := filepath.Join(t.TempDir(), "credentials") contents := []byte("[default]\naws_access_key_id=X\naws_secret_access_key=Y\n") if err := os.WriteFile(path, contents, 0o600); err != nil { t.Fatal(err) } if err := os.Chown(path, owner, serviceID); err != nil { t.Fatal(err) } for i := 0; i < 2; i++ { f, err := openOffcloudCredentials(path) if err != nil { t.Fatal(err) } err = secureOffcloudCredentials(f, serviceID) f.Close() if err != nil { t.Fatal(err) } var st unix.Stat_t if err := unix.Lstat(path, &st); err != nil { t.Fatal(err) } if st.Uid != 0 || st.Gid != serviceID || st.Mode&0o7777 != 0o640 { t.Fatalf("got owner %d:%d mode %o", st.Uid, st.Gid, st.Mode&0o7777) } } if got, err := os.ReadFile(path); err != nil || !bytes.Equal(got, contents) { t.Fatalf("credentials changed: %q, %v", got, err) } }) } } func TestOffcloudCredentialsRepairReplacesWritableInode(t *testing.T) { if os.Geteuid() != 0 { t.Skip("root required to replace service-owned credentials with root:service ownership") } const serviceID = 65534 path := filepath.Join(t.TempDir(), "credentials") contents := []byte("[default]\naws_access_key_id=X\naws_secret_access_key=Y\naws_session_token=Z\n") if err := os.WriteFile(path, contents, 0o600); err != nil { t.Fatal(err) } if err := os.Chown(path, serviceID, serviceID); err != nil { t.Fatal(err) } writable, err := os.OpenFile(path, os.O_RDWR, 0) if err != nil { t.Fatal(err) } defer writable.Close() old, err := writable.Stat() if err != nil { t.Fatal(err) } f, err := openOffcloudCredentials(path) if err != nil { t.Fatal(err) } defer f.Close() if err := secureOffcloudCredentials(f, serviceID); err != nil { t.Fatal(err) } fresh, err := os.Stat(path) if err != nil { t.Fatal(err) } if os.SameFile(old, fresh) { t.Fatal("repair kept the service-writable inode") } if _, err := writable.WriteAt([]byte("[default]\ncredential_process=/bin/false\n"), 0); err != nil { t.Fatal(err) } if got, err := os.ReadFile(path); err != nil || !bytes.Equal(got, contents) { t.Fatalf("old writable descriptor affected repaired credentials: %q, %v", got, err) } } func TestOffcloudCredentialsCorrectMetadataUnchanged(t *testing.T) { if os.Geteuid() != 0 { t.Skip("root required to verify root:service ownership is left unchanged") } const serviceID = 65534 path := filepath.Join(t.TempDir(), "credentials") contents := []byte("# preserve this file verbatim\n[default]\naws_access_key_id = X\naws_secret_access_key = Y\n") if err := os.WriteFile(path, contents, 0o640); err != nil { t.Fatal(err) } if err := os.Chown(path, 0, serviceID); err != nil { t.Fatal(err) } if err := os.Chmod(path, 0o640); err != nil { t.Fatal(err) } var before, after unix.Stat_t if err := unix.Stat(path, &before); err != nil { t.Fatal(err) } f, err := openOffcloudCredentials(path) if err != nil { t.Fatal(err) } defer f.Close() if err := secureOffcloudCredentials(f, serviceID); err != nil { t.Fatal(err) } if err := unix.Stat(path, &after); err != nil { t.Fatal(err) } if before.Ino != after.Ino || before.Ctim != after.Ctim || before.Mode != after.Mode || before.Uid != after.Uid || before.Gid != after.Gid { t.Fatal("repair mutated already-correct credential metadata") } if got, err := os.ReadFile(path); err != nil || !bytes.Equal(got, contents) { t.Fatalf("already-correct credentials changed: %q, %v", got, err) } } func TestOffcloudCredentialsInvalidContentsNotRepaired(t *testing.T) { for _, contents := range []string{ "[default]\ncredential_process=/bin/false\n", "[default]\naws_access_key_id=X\naws_secret_access_key=Y\nregion=eu-west-1\n", "[default]\naws_access_key_id=X\n", "[other]\naws_access_key_id=X\naws_secret_access_key=Y\n", } { t.Run(strings.ReplaceAll(contents, "\n", ";"), func(t *testing.T) { path := filepath.Join(t.TempDir(), "credentials") if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { t.Fatal(err) } var before, after unix.Stat_t if err := unix.Stat(path, &before); err != nil { t.Fatal(err) } f, err := openOffcloudCredentials(path) if err != nil { t.Fatal(err) } defer f.Close() var logs bytes.Buffer previous := log.Writer() log.SetOutput(&logs) defer log.SetOutput(previous) if err := secureOffcloudCredentials(f, os.Getegid()); err == nil { t.Fatal("accepted invalid credentials") } if !strings.Contains(logs.String(), "ALERT:") { t.Fatalf("refusal did not alert: %s", logs.String()) } if err := unix.Stat(path, &after); err != nil { t.Fatal(err) } if before.Ino != after.Ino || before.Ctim != after.Ctim || before.Mode != after.Mode || before.Uid != after.Uid || before.Gid != after.Gid { t.Fatal("invalid credentials metadata changed") } if got, err := os.ReadFile(path); err != nil || string(got) != contents { t.Fatalf("invalid credentials changed: %q, %v", got, err) } }) } } func TestOffcloudCredentialsRefuseLinksAndSpecials(t *testing.T) { for _, kind := range []string{"symlink", "dangling", "hardlink", "fifo", "directory", "parent-symlink"} { t.Run(kind, func(t *testing.T) { root := t.TempDir() path := filepath.Join(root, "credentials") victim := filepath.Join(root, "victim") if err := os.WriteFile(victim, []byte("preserve"), 0o600); err != nil { t.Fatal(err) } var err error switch kind { case "symlink": err = os.Symlink(victim, path) case "dangling": err = os.Symlink(filepath.Join(root, "absent"), path) case "hardlink": err = os.Link(victim, path) case "fifo": err = unix.Mkfifo(path, 0o600) case "directory": err = os.Mkdir(path, 0o600) case "parent-symlink": err = os.Symlink(root, filepath.Join(root, "linked-parent")) path = filepath.Join(root, "linked-parent", "victim") } if err != nil { t.Fatal(err) } var logs bytes.Buffer previous := log.Writer() log.SetOutput(&logs) defer log.SetOutput(previous) if err := repairOffcloudCredentials(path); err == nil { t.Fatal("accepted unsafe credentials") } if !strings.Contains(logs.String(), "ALERT:") { t.Fatalf("refusal did not alert: %s", logs.String()) } fi, err := os.Stat(victim) if err != nil || fi.Mode().Perm() != 0o600 { t.Fatalf("victim mode changed: %v, %v", fi, err) } if got, err := os.ReadFile(victim); err != nil || string(got) != "preserve" { t.Fatalf("victim changed: %q, %v", got, err) } }) } } func TestOffcloudCredentialsMissingCloudFile(t *testing.T) { if err := repairOffcloudCredentials(filepath.Join(t.TempDir(), "absent", "credentials")); err != nil { t.Fatal(err) } } func TestBootstrapOffcloudAcceptsRepairedCredentials(t *testing.T) { root := t.TempDir() path := filepath.Join(root, "etc", "carlos", "credentials") if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte("[default]\n"), 0o640); err != nil { t.Fatal(err) } if err := bootstrapOffcloud(root, nil); err != nil { t.Fatal(err) } }