carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "bytes"
7 "log"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12
13 "golang.org/x/sys/unix"
14)
15
16func TestOffcloudCredentialsCreationAndRepair(t *testing.T) {
17 if os.Geteuid() != 0 {
18 t.Skip("root required for root:service ownership")
19 }
20 const serviceID = 65534
21 for _, owner := range []int{0, serviceID} {
22 t.Run(map[int]string{0: "creation", serviceID: "repair"}[owner], func(t *testing.T) {
23 path := filepath.Join(t.TempDir(), "credentials")
24 contents := []byte("[default]\naws_access_key_id=X\naws_secret_access_key=Y\n")
25 if err := os.WriteFile(path, contents, 0o600); err != nil {
26 t.Fatal(err)
27 }
28 if err := os.Chown(path, owner, serviceID); err != nil {
29 t.Fatal(err)
30 }
31 for i := 0; i < 2; i++ {
32 f, err := openOffcloudCredentials(path)
33 if err != nil {
34 t.Fatal(err)
35 }
36 err = secureOffcloudCredentials(f, serviceID)
37 f.Close()
38 if err != nil {
39 t.Fatal(err)
40 }
41 var st unix.Stat_t
42 if err := unix.Lstat(path, &st); err != nil {
43 t.Fatal(err)
44 }
45 if st.Uid != 0 || st.Gid != serviceID || st.Mode&0o7777 != 0o640 {
46 t.Fatalf("got owner %d:%d mode %o", st.Uid, st.Gid, st.Mode&0o7777)
47 }
48 }
49 if got, err := os.ReadFile(path); err != nil || !bytes.Equal(got, contents) {
50 t.Fatalf("credentials changed: %q, %v", got, err)
51 }
52 })
53 }
54}
55
56func TestOffcloudCredentialsRepairReplacesWritableInode(t *testing.T) {
57 if os.Geteuid() != 0 {
58 t.Skip("root required to replace service-owned credentials with root:service ownership")
59 }
60 const serviceID = 65534
61 path := filepath.Join(t.TempDir(), "credentials")
62 contents := []byte("[default]\naws_access_key_id=X\naws_secret_access_key=Y\naws_session_token=Z\n")
63 if err := os.WriteFile(path, contents, 0o600); err != nil {
64 t.Fatal(err)
65 }
66 if err := os.Chown(path, serviceID, serviceID); err != nil {
67 t.Fatal(err)
68 }
69 writable, err := os.OpenFile(path, os.O_RDWR, 0)
70 if err != nil {
71 t.Fatal(err)
72 }
73 defer writable.Close()
74 old, err := writable.Stat()
75 if err != nil {
76 t.Fatal(err)
77 }
78 f, err := openOffcloudCredentials(path)
79 if err != nil {
80 t.Fatal(err)
81 }
82 defer f.Close()
83 if err := secureOffcloudCredentials(f, serviceID); err != nil {
84 t.Fatal(err)
85 }
86 fresh, err := os.Stat(path)
87 if err != nil {
88 t.Fatal(err)
89 }
90 if os.SameFile(old, fresh) {
91 t.Fatal("repair kept the service-writable inode")
92 }
93 if _, err := writable.WriteAt([]byte("[default]\ncredential_process=/bin/false\n"), 0); err != nil {
94 t.Fatal(err)
95 }
96 if got, err := os.ReadFile(path); err != nil || !bytes.Equal(got, contents) {
97 t.Fatalf("old writable descriptor affected repaired credentials: %q, %v", got, err)
98 }
99}
100
101func TestOffcloudCredentialsCorrectMetadataUnchanged(t *testing.T) {
102 if os.Geteuid() != 0 {
103 t.Skip("root required to verify root:service ownership is left unchanged")
104 }
105 const serviceID = 65534
106 path := filepath.Join(t.TempDir(), "credentials")
107 contents := []byte("# preserve this file verbatim\n[default]\naws_access_key_id = X\naws_secret_access_key = Y\n")
108 if err := os.WriteFile(path, contents, 0o640); err != nil {
109 t.Fatal(err)
110 }
111 if err := os.Chown(path, 0, serviceID); err != nil {
112 t.Fatal(err)
113 }
114 if err := os.Chmod(path, 0o640); err != nil {
115 t.Fatal(err)
116 }
117 var before, after unix.Stat_t
118 if err := unix.Stat(path, &before); err != nil {
119 t.Fatal(err)
120 }
121 f, err := openOffcloudCredentials(path)
122 if err != nil {
123 t.Fatal(err)
124 }
125 defer f.Close()
126 if err := secureOffcloudCredentials(f, serviceID); err != nil {
127 t.Fatal(err)
128 }
129 if err := unix.Stat(path, &after); err != nil {
130 t.Fatal(err)
131 }
132 if before.Ino != after.Ino || before.Ctim != after.Ctim || before.Mode != after.Mode || before.Uid != after.Uid || before.Gid != after.Gid {
133 t.Fatal("repair mutated already-correct credential metadata")
134 }
135 if got, err := os.ReadFile(path); err != nil || !bytes.Equal(got, contents) {
136 t.Fatalf("already-correct credentials changed: %q, %v", got, err)
137 }
138}
139
140func TestOffcloudCredentialsInvalidContentsNotRepaired(t *testing.T) {
141 for _, contents := range []string{
142 "[default]\ncredential_process=/bin/false\n",
143 "[default]\naws_access_key_id=X\naws_secret_access_key=Y\nregion=eu-west-1\n",
144 "[default]\naws_access_key_id=X\n",
145 "[other]\naws_access_key_id=X\naws_secret_access_key=Y\n",
146 } {
147 t.Run(strings.ReplaceAll(contents, "\n", ";"), func(t *testing.T) {
148 path := filepath.Join(t.TempDir(), "credentials")
149 if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
150 t.Fatal(err)
151 }
152 var before, after unix.Stat_t
153 if err := unix.Stat(path, &before); err != nil {
154 t.Fatal(err)
155 }
156 f, err := openOffcloudCredentials(path)
157 if err != nil {
158 t.Fatal(err)
159 }
160 defer f.Close()
161 var logs bytes.Buffer
162 previous := log.Writer()
163 log.SetOutput(&logs)
164 defer log.SetOutput(previous)
165 if err := secureOffcloudCredentials(f, os.Getegid()); err == nil {
166 t.Fatal("accepted invalid credentials")
167 }
168 if !strings.Contains(logs.String(), "ALERT:") {
169 t.Fatalf("refusal did not alert: %s", logs.String())
170 }
171 if err := unix.Stat(path, &after); err != nil {
172 t.Fatal(err)
173 }
174 if before.Ino != after.Ino || before.Ctim != after.Ctim || before.Mode != after.Mode || before.Uid != after.Uid || before.Gid != after.Gid {
175 t.Fatal("invalid credentials metadata changed")
176 }
177 if got, err := os.ReadFile(path); err != nil || string(got) != contents {
178 t.Fatalf("invalid credentials changed: %q, %v", got, err)
179 }
180 })
181 }
182}
183
184func TestOffcloudCredentialsRefuseLinksAndSpecials(t *testing.T) {
185 for _, kind := range []string{"symlink", "dangling", "hardlink", "fifo", "directory", "parent-symlink"} {
186 t.Run(kind, func(t *testing.T) {
187 root := t.TempDir()
188 path := filepath.Join(root, "credentials")
189 victim := filepath.Join(root, "victim")
190 if err := os.WriteFile(victim, []byte("preserve"), 0o600); err != nil {
191 t.Fatal(err)
192 }
193 var err error
194 switch kind {
195 case "symlink":
196 err = os.Symlink(victim, path)
197 case "dangling":
198 err = os.Symlink(filepath.Join(root, "absent"), path)
199 case "hardlink":
200 err = os.Link(victim, path)
201 case "fifo":
202 err = unix.Mkfifo(path, 0o600)
203 case "directory":
204 err = os.Mkdir(path, 0o600)
205 case "parent-symlink":
206 err = os.Symlink(root, filepath.Join(root, "linked-parent"))
207 path = filepath.Join(root, "linked-parent", "victim")
208 }
209 if err != nil {
210 t.Fatal(err)
211 }
212 var logs bytes.Buffer
213 previous := log.Writer()
214 log.SetOutput(&logs)
215 defer log.SetOutput(previous)
216 if err := repairOffcloudCredentials(path); err == nil {
217 t.Fatal("accepted unsafe credentials")
218 }
219 if !strings.Contains(logs.String(), "ALERT:") {
220 t.Fatalf("refusal did not alert: %s", logs.String())
221 }
222 fi, err := os.Stat(victim)
223 if err != nil || fi.Mode().Perm() != 0o600 {
224 t.Fatalf("victim mode changed: %v, %v", fi, err)
225 }
226 if got, err := os.ReadFile(victim); err != nil || string(got) != "preserve" {
227 t.Fatalf("victim changed: %q, %v", got, err)
228 }
229 })
230 }
231}
232
233func TestOffcloudCredentialsMissingCloudFile(t *testing.T) {
234 if err := repairOffcloudCredentials(filepath.Join(t.TempDir(), "absent", "credentials")); err != nil {
235 t.Fatal(err)
236 }
237}
238
239func TestBootstrapOffcloudAcceptsRepairedCredentials(t *testing.T) {
240 root := t.TempDir()
241 path := filepath.Join(root, "etc", "carlos", "credentials")
242 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
243 t.Fatal(err)
244 }
245 if err := os.WriteFile(path, []byte("[default]\n"), 0o640); err != nil {
246 t.Fatal(err)
247 }
248 if err := bootstrapOffcloud(root, nil); err != nil {
249 t.Fatal(err)
250 }
251}
252