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 "crypto/ed25519"
8 "crypto/rand"
9 "encoding/base64"
10 "encoding/json"
11 "errors"
12 "io"
13 "os"
14 "path/filepath"
15 "strings"
16
17 "github.com/carlosframework/platform/internal/hostactors"
18 "github.com/carlosframework/platform/internal/hostfence"
19 "github.com/carlosframework/platform/internal/release"
20 "github.com/carlosframework/platform/internal/safefile"
21 "golang.org/x/sys/unix"
22)
23
24// Each component is opened relative to the checked parent, before any create.
25func activationDir(path string, create bool) (*os.File, error) {
26 if os.Geteuid() != 0 || !filepath.IsAbs(path) || filepath.Clean(path) != path {
27 return nil, errActivation
28 }
29 fd, err := unix.Open("/", unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
30 if err != nil {
31 return nil, err
32 }
33 for _, name := range strings.Split(strings.TrimPrefix(path, "/"), "/") {
34 var st unix.Stat_t
35 if err = unix.Fstat(fd, &st); err != nil || st.Uid != 0 || st.Mode&0022 != 0 {
36 unix.Close(fd)
37 return nil, errActivation
38 }
39 if name == "" {
40 continue
41 }
42 next, e := unix.Openat(fd, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
43 if e == unix.ENOENT && create {
44 e = unix.Mkdirat(fd, name, 0700)
45 if e == nil || e == unix.EEXIST {
46 if e = unix.Fsync(fd); e == nil {
47 next, e = unix.Openat(fd, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
48 }
49 }
50 }
51 unix.Close(fd)
52 if e != nil {
53 return nil, e
54 }
55 fd = next
56 }
57 var st unix.Stat_t
58 if err = unix.Fstat(fd, &st); err != nil || st.Uid != 0 || st.Mode&0022 != 0 {
59 unix.Close(fd)
60 return nil, errActivation
61 }
62 return os.NewFile(uintptr(fd), path), nil
63}
64
65func activationReadAt(dir *os.File, name string, private bool) ([]byte, error) {
66 f, err := safefile.OpenFileAt(int(dir.Fd()), name, os.O_RDONLY, 0)
67 if err != nil {
68 return nil, err
69 }
70 defer f.Close()
71 var st unix.Stat_t
72 if err = unix.Fstat(int(f.Fd()), &st); err != nil {
73 return nil, err
74 }
75 if st.Uid != 0 || st.Mode&0022 != 0 || private && st.Mode&0777 != 0600 && st.Mode&0777 != 0400 {
76 return nil, errActivation
77 }
78 raw, err := io.ReadAll(io.LimitReader(f, 65537))
79 if err != nil {
80 return nil, err
81 }
82 if len(raw) > 65536 {
83 return nil, errActivation
84 }
85 return raw, nil
86}
87func activationRead(path string, private bool) ([]byte, error) {
88 dir, err := activationDir(filepath.Dir(path), false)
89 if err != nil {
90 return nil, err
91 }
92 defer dir.Close()
93 return activationReadAt(dir, filepath.Base(path), private)
94}
95func activationLock(dir *os.File) (*os.File, error) {
96 f, err := safefile.OpenFileAt(int(dir.Fd()), activationLockName, os.O_CREATE|os.O_RDWR, 0600)
97 if err != nil {
98 return nil, err
99 }
100 var st unix.Stat_t
101 if err = unix.Fstat(int(f.Fd()), &st); err != nil || st.Uid != 0 || st.Mode&0777 != 0600 {
102 f.Close()
103 return nil, errActivation
104 }
105 if err = unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
106 f.Close()
107 return nil, err
108 }
109 return f, nil
110}
111
112type activationKeyProof struct {
113 Binding hostactors.CensusKeyBinding `json:"binding"`
114 Proof []byte `json:"proof"`
115}
116
117func installActivationEvidenceKey(path string, binding hostactors.CensusKeyBinding, dry bool) (activationKeyProof, error) {
118 var result activationKeyProof
119 if binding.Bucket == "" || binding.Box == "" || len(binding.Enrollment) != 32 || len(binding.PublicKey) != 0 && len(binding.PublicKey) != ed25519.PublicKeySize {
120 return result, errActivation
121 }
122 dir, err := activationDir(filepath.Dir(path), false)
123 if err != nil {
124 return result, err
125 }
126 defer dir.Close()
127 if !dry {
128 lock, e := activationLock(dir)
129 if e != nil {
130 return result, e
131 }
132 defer lock.Close()
133 }
134 raw, err := activationReadAt(dir, filepath.Base(path), true)
135 if errors.Is(err, os.ErrNotExist) || errors.Is(err, unix.ENOENT) {
136 if len(binding.PublicKey) != 0 {
137 return result, errActivation
138 }
139 if dry {
140 return result, nil
141 }
142 _, key, e := ed25519.GenerateKey(rand.Reader)
143 if e != nil {
144 return result, e
145 }
146 raw = []byte(base64.StdEncoding.EncodeToString(key.Seed()) + "\n")
147 if err = safefile.WriteAt(int(dir.Fd()), filepath.Base(path), raw, 0600); err != nil {
148 return result, err
149 }
150 verified, e := activationReadAt(dir, filepath.Base(path), true)
151 if e != nil || !bytes.Equal(raw, verified) {
152 return result, errActivation
153 }
154 } else if err != nil {
155 return result, err
156 }
157 key, err := release.ParseSigningKey(strings.TrimSpace(string(raw)), path)
158 if err != nil {
159 return result, errActivation
160 }
161 if len(binding.PublicKey) != 0 && !bytes.Equal(binding.PublicKey, key.Public().(ed25519.PublicKey)) {
162 return result, errActivation
163 }
164 binding.PublicKey = key.Public().(ed25519.PublicKey)
165 wire, err := json.Marshal(binding)
166 if err != nil {
167 return result, err
168 }
169 result = activationKeyProof{Binding: binding, Proof: ed25519.Sign(key, wire)}
170 if !ed25519.Verify(binding.PublicKey, wire, result.Proof) {
171 return activationKeyProof{}, errActivation
172 }
173 return result, nil
174}
175func installActivationFenceCredentials(source, target string, scope activationFenceScope, dry bool) error {
176 raw, err := activationRead(source, true)
177 if err != nil {
178 return err
179 }
180 if err = hostfence.ValidateActivationCredentials(raw, scope.Bucket, scope.Region, scope.Role); err != nil {
181 return errActivation
182 }
183 dir, err := activationDir(filepath.Dir(target), !dry)
184 if dry && (errors.Is(err, os.ErrNotExist) || errors.Is(err, unix.ENOENT)) {
185 return nil
186 }
187 if err != nil {
188 return err
189 }
190 defer dir.Close()
191 if !dry {
192 lock, e := activationLock(dir)
193 if e != nil {
194 return e
195 }
196 defer lock.Close()
197 }
198 existing, err := activationReadAt(dir, filepath.Base(target), true)
199 if err == nil {
200 if !bytes.Equal(existing, raw) {
201 return errActivation
202 }
203 } else if !errors.Is(err, os.ErrNotExist) && !errors.Is(err, unix.ENOENT) {
204 return err
205 }
206 receipt := hostfence.ActivationCredentialReceipt(raw)
207 prior, receiptErr := activationReadAt(dir, "credentials.installation.json", true)
208 if receiptErr == nil && !bytes.Equal(prior, receipt) {
209 return errActivation
210 }
211 if receiptErr != nil && !errors.Is(receiptErr, os.ErrNotExist) {
212 return receiptErr
213 }
214 if dry {
215 return nil
216 }
217 if err != nil {
218 if err = safefile.WriteAt(int(dir.Fd()), filepath.Base(target), raw, 0600); err != nil {
219 return err
220 }
221 }
222 after, err := activationReadAt(dir, filepath.Base(target), true)
223 if err != nil || !bytes.Equal(raw, after) {
224 return errActivation
225 }
226 if receiptErr != nil {
227 if err = safefile.WriteAt(int(dir.Fd()), "credentials.installation.json", receipt, 0600); err != nil {
228 return err
229 }
230 }
231 after, err = activationReadAt(dir, "credentials.installation.json", true)
232 if err != nil || !bytes.Equal(receipt, after) {
233 return errActivation
234 }
235 return nil
236}
237
238func installActivationControl(envPath, unitPath string, dry bool, systemctl func(...string) (string, error)) error {
239 raw, err := activationRead(envPath, false)
240 if err != nil {
241 return err
242 }
243 cfg, err := activationUnitSettings(raw)
244 if err != nil {
245 return err
246 }
247 dir, err := activationDir(unitPath, false)
248 if err != nil {
249 return err
250 }
251 defer dir.Close()
252 // Refuse a masked/symlinked control unit, including an operator's own mask.
253 if _, err = activationReadAt(dir, claimControlUnit, false); err != nil && !errors.Is(err, os.ErrNotExist) && !errors.Is(err, unix.ENOENT) {
254 return err
255 }
256 content, err := unitsFS.ReadFile("units/" + claimControlUnit)
257 if err != nil {
258 return err
259 }
260 if dry {
261 return nil
262 }
263 lock, err := activationLock(dir)
264 if err != nil {
265 return err
266 }
267 defer lock.Close()
268 cfg.rootRetry, err = prepareRootRetryUnitDirectory()
269 if err != nil {
270 return err
271 }
272 content = claimUnitContent(claimControlUnit, content, cfg)
273 if err = safefile.WriteAt(int(dir.Fd()), claimControlUnit, content, 0644); err != nil {
274 return err
275 }
276 after, err := activationReadAt(dir, claimControlUnit, false)
277 if err != nil || !bytes.Equal(after, content) {
278 return errActivation
279 }
280 _, err = systemctl("daemon-reload")
281 return err
282}
283
284func installActivationConsole(envPath, dropPath string, raw []byte, dry bool, systemctl func(...string) (string, error)) error {
285 parent, err := activationDir(filepath.Dir(envPath), false)
286 if err != nil {
287 return err
288 }
289 defer parent.Close()
290 unitParent, err := activationDir(filepath.Dir(dropPath), false)
291 if err != nil {
292 return err
293 }
294 defer unitParent.Close()
295 if !dry {
296 lock, e := activationLock(parent)
297 if e != nil {
298 return e
299 }
300 defer lock.Close()
301 }
302 existing, err := activationReadAt(parent, filepath.Base(envPath), true)
303 off := bytes.Replace(raw, []byte("CARLOS_HOST_CLAIM=1\n"), []byte("CARLOS_HOST_CLAIM=\n"), 1)
304 if err == nil && !bytes.Equal(existing, raw) && !bytes.Equal(existing, off) {
305 return errActivation
306 }
307 if err != nil && !errors.Is(err, os.ErrNotExist) {
308 return err
309 }
310 drop, err := activationDir(dropPath, !dry)
311 if dry && errors.Is(err, os.ErrNotExist) {
312 return nil
313 }
314 if err != nil {
315 return err
316 }
317 defer drop.Close()
318 content := []byte("[Service]\nEnvironmentFile=" + envPath + "\n")
319 old, err := activationReadAt(drop, "91-claim-runtime.conf", false)
320 if err == nil && !bytes.Equal(old, content) {
321 return errActivation
322 }
323 if err != nil && !errors.Is(err, os.ErrNotExist) {
324 return err
325 }
326 if dry {
327 return nil
328 }
329 if err = safefile.WriteAt(int(parent.Fd()), filepath.Base(envPath), raw, 0600); err != nil {
330 return err
331 }
332 if err = safefile.WriteAt(int(drop.Fd()), "91-claim-runtime.conf", content, 0644); err != nil {
333 return err
334 }
335 got, err := activationReadAt(parent, filepath.Base(envPath), true)
336 if err != nil || !bytes.Equal(got, raw) {
337 return errActivation
338 }
339 got, err = activationReadAt(drop, "91-claim-runtime.conf", false)
340 if err != nil || !bytes.Equal(got, content) {
341 return errActivation
342 }
343 _, err = systemctl("daemon-reload")
344 return err
345}
346