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 "context"
8 "crypto/ed25519"
9 "encoding/json"
10 "fmt"
11 "os"
12 "path/filepath"
13 "reflect"
14 "runtime"
15 "strconv"
16 "strings"
17 "time"
18
19 "github.com/carlosframework/platform/internal/hostactors"
20 "github.com/carlosframework/platform/internal/hostfloor"
21 "github.com/carlosframework/platform/internal/release"
22 "github.com/carlosframework/platform/internal/status"
23 "golang.org/x/sys/unix"
24)
25
26func activationConsolePID(r hostactors.Report) (int, string, error) {
27 p := strings.Split(r.ProcessID, ":")
28 if len(p) != 4 || p[0] != r.BootID {
29 return 0, "", errActivation
30 }
31 for _, field := range p[1:] {
32 n, e := strconv.ParseUint(field, 10, 64)
33 if e != nil || n == 0 {
34 return 0, "", errActivation
35 }
36 }
37 pid, err := strconv.Atoi(p[2])
38 if err != nil || pid <= 0 {
39 return 0, "", errActivation
40 }
41 return pid, p[3], nil
42}
43
44// This observation is deliberately on demand. It never refreshes an old box
45// report or asserts that every possible console launch path is guarded.
46func activationConsoleProcess(ctx context.Context, pub ed25519.PublicKey, bucket string, o hostactors.Observed, id string, r hostactors.Report) (status.ConsoleProcess, error) {
47 var zero status.ConsoleProcess
48 c, ok := o.Roster.Consoles[id]
49 if !ok || c.Compute == nil || r.Actor != id || r.Component != "console" || r.Compute == nil || *r.Compute != *c.Compute {
50 return zero, errActivation
51 }
52 localRaw, err := activationRead("/etc/carlos/host-actor.json", false)
53 if err != nil {
54 return zero, err
55 }
56 var local hostfloor.Enrollment
57 if json.Unmarshal(localRaw, &local) != nil || local.Box != c.Compute.Box || local.Enrollment != c.Compute.Enrollment {
58 return zero, errActivation
59 }
60 hostEnv, err := activationRead("/etc/carlos/host.env", false)
61 if err != nil {
62 return zero, err
63 }
64 expected, err := activationConsoleEnvironment(o.Roster, local, hostEnv, id, r.HostClaimEnabled)
65 if err != nil {
66 return zero, err
67 }
68 actual, err := activationRead("/etc/carlos/claim-console.env", true)
69 if err != nil || !bytes.Equal(actual, expected) {
70 return zero, errActivation
71 }
72 d := hostfloor.Disk{Dir: "/opt/carlos/system/guard", Bucket: bucket, PublicKey: pub}
73 policyRaw, err := activationRead(filepath.Join(d.Dir, "policy.json"), false)
74 if err != nil {
75 return zero, err
76 }
77 var policy hostfloor.Policy
78 if json.Unmarshal(policyRaw, &policy) != nil || policy.Mode == "unpublished" {
79 return zero, errActivation
80 }
81 floor, err := policy.Verify(pub, bucket)
82 if err != nil || floor.Digest != r.FloorSHA256 {
83 return zero, errActivation
84 }
85 path, err := activationRead(filepath.Join(d.Dir, "console-ready.json"), false)
86 if err != nil {
87 return zero, err
88 }
89 guard, err := activationRead("/etc/systemd/system/console.service.d/90-host-floor.conf", false)
90 if err != nil || string(guard) != hostfloor.ConsoleGuardDropIn(string(path)) {
91 return zero, errActivation
92 }
93 if !filepath.IsAbs(string(path)) || filepath.Clean(string(path)) != string(path) || strings.ContainsAny(string(path), " \t\r\n;") {
94 return zero, errActivation
95 }
96 snapshot := func() (map[string]string, error) {
97 raw, e := claimSystemctl(ctx, "show", "console.service", "--property=MainPID,ActiveState,SubState,ControlGroup,ExecStart,FragmentPath,DropInPaths")
98 if e != nil {
99 return nil, e
100 }
101 m := map[string]string{}
102 for _, line := range strings.Split(string(raw), "\n") {
103 k, v, ok := strings.Cut(line, "=")
104 if ok {
105 m[k] = v
106 }
107 }
108 return m, nil
109 }
110 unit, err := snapshot()
111 if err != nil {
112 return zero, err
113 }
114 pid, start, err := activationConsolePID(r)
115 if err != nil {
116 return zero, err
117 }
118 wantExec := "{ path=/opt/carlos/system/guard/carlos ; argv[]=/opt/carlos/system/guard/carlos system guard-exec console " + string(path) + " serve ;"
119 if unit["ActiveState"] != "active" || unit["SubState"] != "running" || unit["MainPID"] != strconv.Itoa(pid) || !strings.HasPrefix(unit["ExecStart"], wantExec) || strings.Count(unit["ExecStart"], "argv[]=") != 1 || unit["ControlGroup"] != "/system.slice/console.service" {
120 return zero, errActivation
121 }
122 for _, p := range append([]string{unit["FragmentPath"]}, strings.Fields(unit["DropInPaths"])...) {
123 if _, err := activationRead(p, false); err != nil {
124 return zero, err
125 }
126 }
127 if !strings.Contains(unit["DropInPaths"], "/etc/systemd/system/console.service.d/90-host-floor.conf") || !strings.Contains(unit["DropInPaths"], "/etc/systemd/system/console.service.d/91-claim-runtime.conf") {
128 return zero, errActivation
129 }
130 proc := fmt.Sprintf("/proc/%d", pid)
131 check := func() error {
132 boot, e := os.ReadFile("/proc/sys/kernel/random/boot_id")
133 if e != nil || strings.TrimSpace(string(boot)) != r.BootID {
134 return errActivation
135 }
136 st, e := os.ReadFile(proc + "/stat")
137 if e != nil {
138 return e
139 }
140 end := strings.LastIndexByte(string(st), ')')
141 if end < 0 {
142 return errActivation
143 }
144 fields := strings.Fields(string(st[end+1:]))
145 if len(fields) < 20 || fields[19] != start {
146 return errActivation
147 }
148 var ns unix.Stat_t
149 if unix.Stat(proc+"/ns/pid", &ns) != nil || strconv.FormatUint(ns.Ino, 10) != strings.Split(r.ProcessID, ":")[1] {
150 return errActivation
151 }
152 group, e := os.ReadFile(proc + "/cgroup")
153 if e != nil || string(group) != "0::"+unit["ControlGroup"]+"\n" {
154 return errActivation
155 }
156 return nil
157 }
158 if err = check(); err != nil {
159 return zero, err
160 }
161 command, err := os.ReadFile(proc + "/cmdline")
162 if err != nil || string(command) != string(path)+"\x00serve\x00" {
163 return zero, errActivation
164 }
165 processEnv, err := os.ReadFile(proc + "/environ")
166 if err != nil {
167 return zero, err
168 }
169 wantedEnv, err := activationEnvironment(expected)
170 if err != nil {
171 return zero, err
172 }
173 for _, name := range []string{"CARLOS_CONSOLE_ID", "CONSOLE_HOME_REGION", "CARLOS_HOST_CLAIM"} {
174 count := 0
175 for _, entry := range strings.Split(string(processEnv), "\x00") {
176 key, value, ok := strings.Cut(entry, "=")
177 if ok && key == name {
178 count++
179 if value != wantedEnv[name] {
180 return zero, errActivation
181 }
182 }
183 }
184 if count != 1 {
185 return zero, errActivation
186 }
187 }
188 checker, err := hostfloor.OpenProtected(filepath.Join(d.Dir, "carlos"))
189 if err != nil {
190 return zero, err
191 }
192 _, err = d.VerifyFile(checker, runtime.GOOS+"-"+runtime.GOARCH)
193 checker.Close()
194 if err != nil {
195 return zero, err
196 }
197 candidate, err := hostfloor.OpenProtected(string(path))
198 if err != nil {
199 return zero, err
200 }
201 defer candidate.Close()
202 f, err := os.Open(proc + "/exe")
203 if err != nil {
204 return zero, err
205 }
206 defer f.Close()
207 currentInfo, err := f.Stat()
208 if err != nil {
209 return zero, err
210 }
211 candidateInfo, err := candidate.Stat()
212 if err != nil || !os.SameFile(currentInfo, candidateInfo) {
213 return zero, errActivation
214 }
215 var st unix.Stat_t
216 if unix.Fstat(int(f.Fd()), &st) != nil || st.Uid != 0 || st.Mode&0022 != 0 {
217 return zero, errActivation
218 }
219 hash, err := d.VerifyFile(f, runtime.GOOS+"-"+runtime.GOARCH, "console")
220 if err != nil || hash != r.BinarySHA256 {
221 return zero, errActivation
222 }
223 if err = check(); err != nil {
224 return zero, err
225 }
226 again, err := snapshot()
227 if err != nil || !reflect.DeepEqual(again, unit) {
228 return zero, errActivation
229 }
230 return status.ConsoleProcess{ConsoleID: id, Enrollment: c.Enrollment, ProcessID: r.ProcessID, BinarySHA256: hash, GuardFloorSHA256: floor.Digest}, nil
231}
232
233func publishActivationConsoleReport(ctx context.Context, store release.Store, pub ed25519.PublicKey, bucket, id string, revision uint64, dry bool) (status.ConsoleProcess, error) {
234 var zero status.ConsoleProcess
235 d := hostactors.Directory{Store: store, PublicKey: pub, Bucket: bucket}
236 o, err := d.Read(ctx)
237 if err != nil {
238 return zero, err
239 }
240 if revision == 0 || o.Roster.Revision != revision {
241 return zero, errActivation
242 }
243 c, ok := o.Roster.Consoles[id]
244 if !ok || c.Compute == nil {
245 return zero, errActivation
246 }
247 e := hostactors.ExactEvidence{Store: store, PublicKey: pub, Boxes: d.PrivateBoxReport}
248 r, err := e.ConsoleReport(ctx, id, c)
249 if err != nil {
250 return zero, err
251 }
252 p, err := activationConsoleProcess(ctx, pub, bucket, o, id, r)
253 if err != nil {
254 return zero, err
255 }
256 raw, err := activationRead("/opt/carlos/system/guard/policy.json", false)
257 if err != nil {
258 return zero, err
259 }
260 var policy hostfloor.Policy
261 if json.Unmarshal(raw, &policy) != nil {
262 return zero, errActivation
263 }
264 floor, err := policy.Verify(pub, bucket)
265 if err != nil {
266 return zero, err
267 }
268 if err = hostactors.VerifyReport(ctx, e, pub, floor, r, c.Enrollment, id, time.Now()); err != nil {
269 return zero, err
270 }
271 b := o.Roster.Boxes[c.Compute.Box]
272 key := "status/boxes/" + c.Compute.Box + ".json"
273 body, tag, err := store.Get(ctx, key)
274 if err != nil {
275 return zero, err
276 }
277 br, err := d.PrivateBoxReport(ctx, c.Compute.Box, b)
278 if err != nil {
279 return zero, err
280 }
281 if err = hostactors.VerifyReport(ctx, e, pub, floor, br, b.Enrollment, b.Cloud.InstanceID, time.Now()); err != nil {
282 return zero, err
283 }
284 unchanged, unchangedTag, err := store.Get(ctx, key)
285 if err != nil || unchangedTag != tag || !bytes.Equal(unchanged, body) {
286 return zero, errActivation
287 }
288 if br.BootID != r.BootID {
289 return zero, errActivation
290 }
291 // Keep fields unknown to this binary, including future box metrics.
292 var box map[string]json.RawMessage
293 if json.Unmarshal(body, &box) != nil {
294 return zero, errActivation
295 }
296 var metrics map[string]json.RawMessage
297 if json.Unmarshal(box["metrics"], &metrics) != nil {
298 return zero, errActivation
299 }
300 var processes []status.ConsoleProcess
301 if v := metrics["console_processes"]; len(v) > 0 && json.Unmarshal(v, &processes) != nil {
302 return zero, errActivation
303 }
304 next := []status.ConsoleProcess{}
305 for _, old := range processes {
306 if old.ConsoleID != id {
307 next = append(next, old)
308 }
309 }
310 next = append(next, p)
311 metrics["console_processes"], _ = json.Marshal(next)
312 box["metrics"], _ = json.Marshal(metrics)
313 body, _ = json.Marshal(box)
314 fresh, err := d.Read(ctx)
315 if err != nil || fresh.ETag != o.ETag {
316 return zero, errActivation
317 }
318 if _, err = activationConsoleProcess(ctx, pub, bucket, o, id, r); err != nil {
319 return zero, err
320 }
321 if dry {
322 return p, nil
323 }
324 written, err := store.Put(ctx, key, body, tag)
325 if err != nil {
326 return zero, err
327 }
328 got, readTag, err := store.Get(ctx, key)
329 if err != nil || readTag != written || !bytes.Equal(got, body) {
330 return zero, errActivation
331 }
332 if _, err = activationConsoleProcess(ctx, pub, bucket, o, id, r); err != nil {
333 return zero, err
334 }
335 return p, nil
336}
337