| 1 | package rootaws |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "log" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | |
| 12 | "golang.org/x/sys/unix" |
| 13 | ) |
| 14 | |
| 15 | func TestRootCredentialsRejectUnsafeFiles(t *testing.T) { |
| 16 | for _, kind := range []string{"symlink", "dangling", "hardlink", "fifo", "directory", "service-owner", "group-write", "other-write", "process"} { |
| 17 | t.Run(kind, func(t *testing.T) { |
| 18 | isolatedAWSEnv(t) |
| 19 | dir := t.TempDir() |
| 20 | path := filepath.Join(dir, "credentials") |
| 21 | target := filepath.Join(dir, "target") |
| 22 | body := []byte("[default]\naws_access_key_id=ACCESS\naws_secret_access_key=SECRET\n") |
| 23 | if err := os.WriteFile(target, body, 0600); err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | var err error |
| 27 | switch kind { |
| 28 | case "symlink": |
| 29 | err = os.Symlink(target, path) |
| 30 | case "dangling": |
| 31 | err = os.Symlink(filepath.Join(dir, "missing"), path) |
| 32 | case "hardlink": |
| 33 | err = os.Link(target, path) |
| 34 | case "fifo": |
| 35 | err = unix.Mkfifo(path, 0600) |
| 36 | case "directory": |
| 37 | err = os.Mkdir(path, 0700) |
| 38 | default: |
| 39 | if os.Geteuid() != 0 && kind != "service-owner" { |
| 40 | t.Skip("requires root-owned fixture to isolate mode and content checks") |
| 41 | } |
| 42 | if kind == "process" { |
| 43 | body = append(body, []byte("credential_process=bad\n")...) |
| 44 | } |
| 45 | err = os.WriteFile(path, body, 0600) |
| 46 | if err == nil { |
| 47 | switch kind { |
| 48 | case "service-owner": |
| 49 | if os.Geteuid() == 0 { |
| 50 | err = os.Chown(path, 65534, 65534) |
| 51 | } |
| 52 | case "group-write": |
| 53 | err = os.Chmod(path, 0660) |
| 54 | case "other-write": |
| 55 | err = os.Chmod(path, 0602) |
| 56 | } |
| 57 | } |
| 58 | } |
| 59 | if err != nil { |
| 60 | t.Fatal(err) |
| 61 | } |
| 62 | var logs bytes.Buffer |
| 63 | old := log.Writer() |
| 64 | log.SetOutput(&logs) |
| 65 | defer log.SetOutput(old) |
| 66 | cfg, err := loadRootConfig(context.Background(), path) |
| 67 | if err != nil { |
| 68 | t.Fatalf("unused unsafe credentials made config loading fatal: %v", err) |
| 69 | } |
| 70 | if _, err := cfg.Credentials.Retrieve(context.Background()); err == nil { |
| 71 | t.Fatal("unsafe file accepted") |
| 72 | } |
| 73 | if !strings.Contains(logs.String(), "ALERT:") { |
| 74 | t.Fatal("unsafe file refusal did not alert") |
| 75 | } |
| 76 | }) |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | func TestRootCredentialsEnvironmentKeysIgnoreServiceOwnedFile(t *testing.T) { |
| 81 | isolatedAWSEnv(t) |
| 82 | t.Setenv("AWS_ACCESS_KEY_ID", "ENV_ACCESS") |
| 83 | t.Setenv("AWS_SECRET_ACCESS_KEY", "ENV_SECRET") |
| 84 | path := filepath.Join(t.TempDir(), "credentials") |
| 85 | if err := os.WriteFile(path, []byte("[default]\naws_access_key_id=FILE\naws_secret_access_key=SECRET\n"), 0600); err != nil { |
| 86 | t.Fatal(err) |
| 87 | } |
| 88 | if os.Geteuid() == 0 { |
| 89 | if err := os.Chown(path, 65534, 65534); err != nil { |
| 90 | t.Fatal(err) |
| 91 | } |
| 92 | } |
| 93 | // Without environment keys the same file is refused, so the file is unsafe. |
| 94 | t.Setenv("AWS_SECRET_ACCESS_KEY", "") |
| 95 | unsafeConfig, err := loadRootConfig(context.Background(), path) |
| 96 | if err != nil { |
| 97 | t.Fatal(err) |
| 98 | } |
| 99 | if _, err := unsafeConfig.Credentials.Retrieve(context.Background()); err == nil { |
| 100 | t.Fatal("service-owned fixture was not unsafe") |
| 101 | } |
| 102 | t.Setenv("AWS_SECRET_ACCESS_KEY", "ENV_SECRET") |
| 103 | cfg, err := loadRootConfig(context.Background(), path) |
| 104 | if err != nil { |
| 105 | t.Fatalf("service-owned unused file made config fatal: %v", err) |
| 106 | } |
| 107 | got, err := cfg.Credentials.Retrieve(context.Background()) |
| 108 | if err != nil || got.AccessKeyID != "ENV_ACCESS" || got.SecretAccessKey != "ENV_SECRET" { |
| 109 | t.Fatalf("environment credentials failed: %v", err) |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | func TestRootCredentialsStaticProvider(t *testing.T) { |
| 114 | if os.Geteuid() != 0 { |
| 115 | t.Skip("requires root-owned credentials fixture") |
| 116 | } |
| 117 | isolatedAWSEnv(t) |
| 118 | path := filepath.Join(t.TempDir(), "credentials") |
| 119 | if err := os.WriteFile(path, []byte("[default]\naws_access_key_id=STATIC\naws_secret_access_key=SECRET\naws_session_token=TOKEN\n"), 0640); err != nil { |
| 120 | t.Fatal(err) |
| 121 | } |
| 122 | fileCreds, err := offcloudCredentials(path) |
| 123 | if err != nil || fileCreds.AccessKeyID != "STATIC" || fileCreds.SecretAccessKey != "SECRET" || fileCreds.SessionToken != "TOKEN" { |
| 124 | t.Fatalf("off-cloud file credentials failed: %v", err) |
| 125 | } |
| 126 | cfg, err := loadRootConfig(context.Background(), path) |
| 127 | if err != nil { |
| 128 | t.Fatal(err) |
| 129 | } |
| 130 | // The provider holds the parsed snapshot and never asks the SDK to reread it. |
| 131 | if err := os.WriteFile(path, []byte("[default]\ncredential_process=bad\n"), 0640); err != nil { |
| 132 | t.Fatal(err) |
| 133 | } |
| 134 | got, err := cfg.Credentials.Retrieve(context.Background()) |
| 135 | if err != nil || got.AccessKeyID != "STATIC" || got.SessionToken != "TOKEN" { |
| 136 | t.Fatalf("static snapshot failed: %v", err) |
| 137 | } |
| 138 | } |
| 139 | |