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 "flag"
11 "io"
12
13 "github.com/carlosframework/platform/internal/claimactivation"
14 "github.com/carlosframework/platform/internal/hostactors"
15 "github.com/carlosframework/platform/internal/hostfloor"
16 "github.com/carlosframework/platform/internal/publicrelease"
17 "github.com/carlosframework/platform/internal/release"
18 "github.com/carlosframework/platform/internal/version"
19 "runtime"
20)
21
22func runSystemClaims(args []string, out io.Writer) error {
23 if len(args) == 1 && (args[0] == "help" || args[0] == "--help" || args[0] == "-h") {
24 _, err := io.WriteString(out, activationClaimsUsage)
25 return err
26 }
27 if len(args) == 0 {
28 return errActivation
29 }
30 fs := flag.NewFlagSet("system claims "+args[0], flag.ContinueOnError)
31 fs.SetOutput(io.Discard)
32 consoleID := fs.String("console-id", "", "")
33 enableRuntime := fs.Bool("enable-console-runtime", false, "")
34 dry := fs.Bool("dry-run", false, "")
35 planPath := fs.String("plan", "", "")
36 keyPath := fs.String("signing-key", "", "")
37 pubValue := fs.String("public-key", "", "")
38 region := fs.String("aws-region", "", "")
39 rosterPath := fs.String("roster", "", "")
40 revision := fs.Uint64("expect-revision", 0, "")
41 box := fs.String("box", "", "")
42 enrollment := fs.String("enrollment", "", "")
43 source := fs.String("source", "", "")
44 bucket := fs.String("bucket", "", "")
45 role := fs.String("role", "", "")
46 outPath := fs.String("out", "", "")
47 identityPath := fs.String("identity", "", "")
48 upgrade := fs.Bool("upgrade", false, "")
49 from := fs.String("from", "", "")
50 if err := fs.Parse(args[1:]); err != nil || len(fs.Args()) != 0 {
51 return errActivation
52 }
53 if upgradeFlagsRefused(args[0], fs, *upgrade, *from) {
54 return errActivation
55 }
56 ctx := context.Background()
57 encode := func(value any) error { e := json.NewEncoder(out); e.SetIndent("", " "); return e.Encode(value) }
58 switch args[0] {
59 case "inspect":
60 if *bucket == "" || *region == "" {
61 return errActivation
62 }
63 pub, err := release.ParsePublicKey(*pubValue, "public-key")
64 if err != nil {
65 return errActivation
66 }
67 store, err := release.NewDeploymentS3StoreIn(ctx, *bucket, *region)
68 if err != nil {
69 return err
70 }
71 roster, err := (hostactors.Directory{Store: store, PublicKey: pub, Bucket: *bucket}).Read(ctx)
72 if err != nil {
73 return err
74 }
75 wire, tag, err := store.Get(ctx, hostactors.Key)
76 if err != nil || tag != roster.ETag {
77 return errActivation
78 }
79 return encode(struct {
80 Roster hostactors.Roster `json:"roster"`
81 ETag string `json:"etag"`
82 SignedRoster json.RawMessage `json:"signed_roster"`
83 }{roster.Roster, roster.ETag, wire})
84 case "console-report":
85 if *consoleID == "" || *bucket == "" || *region == "" || *revision == 0 {
86 return errActivation
87 }
88 pub, err := release.ParsePublicKey(*pubValue, "public-key")
89 if err != nil {
90 return errActivation
91 }
92 store, err := release.NewDeploymentS3StoreIn(ctx, *bucket, *region)
93 if err != nil {
94 return err
95 }
96 report, err := publishActivationConsoleReport(ctx, store, pub, *bucket, *consoleID, *revision, *dry)
97 if err != nil {
98 return err
99 }
100 localRaw, err := activationRead("/etc/carlos/host-actor.json", false)
101 if err != nil {
102 return err
103 }
104 var local hostfloor.Enrollment
105 if json.Unmarshal(localRaw, &local) != nil {
106 return errActivation
107 }
108 return encode(map[string]any{"dry_run": *dry, "bucket": *bucket, "key": "status/boxes/" + local.Box + ".json", "expected_roster_revision": *revision, "console_process": report})
109 case "console-config":
110 if *consoleID == "" || *bucket == "" || *revision == 0 || *rosterPath == "" {
111 return errActivation
112 }
113 pub, err := release.ParsePublicKey(*pubValue, "public-key")
114 if err != nil {
115 return errActivation
116 }
117 raw, err := activationRead(*rosterPath, false)
118 if err != nil {
119 return err
120 }
121 var roster hostactors.Roster
122 if release.VerifyRootEnvelope(pub, raw, &roster) != nil || roster.Validate(*bucket) != nil || roster.Revision != *revision {
123 return errActivation
124 }
125 localRaw, err := activationRead("/etc/carlos/host-actor.json", false)
126 if err != nil {
127 return err
128 }
129 var local hostfloor.Enrollment
130 if json.Unmarshal(localRaw, &local) != nil {
131 return errActivation
132 }
133 env, err := activationRead("/etc/carlos/host.env", false)
134 if err != nil {
135 return err
136 }
137 content, err := activationConsoleEnvironment(roster, local, env, *consoleID, *enableRuntime)
138 if err != nil {
139 return err
140 }
141 policyRaw, err := activationRead("/opt/carlos/system/guard/policy.json", false)
142 if err != nil {
143 return err
144 }
145 var policy hostfloor.Policy
146 if json.Unmarshal(policyRaw, &policy) != nil || policy.Mode == "unpublished" {
147 return errActivation
148 }
149 floor, err := policy.Verify(pub, *bucket)
150 if err != nil {
151 return err
152 }
153 if *enableRuntime && floor.Digest != roster.FloorSHA256 {
154 return errActivation
155 }
156 consolePath, err := activationRead("/opt/carlos/system/guard/console-ready.json", false)
157 if err != nil {
158 return err
159 }
160 guard, err := activationRead("/etc/systemd/system/console.service.d/90-host-floor.conf", false)
161 if err != nil {
162 return err
163 }
164 if string(guard) != hostfloor.ConsoleGuardDropIn(string(consolePath)) {
165 return errActivation
166 }
167 if err = installActivationConsole("/etc/carlos/claim-console.env", "/etc/systemd/system/console.service.d", content, *dry, productionSysOps().systemctl); err != nil {
168 return err
169 }
170 return encode(map[string]any{"dry_run": *dry, "console_id": *consoleID, "environment": string(content), "path": "/etc/carlos/claim-console.env", "drop_in": "/etc/systemd/system/console.service.d/91-claim-runtime.conf", "reload": true, "start": false})
171 case "run":
172 if *planPath == "" || *keyPath == "" || *pubValue == "" || *region == "" {
173 return errActivation
174 }
175 raw, err := activationRead(*planPath, false)
176 if err != nil {
177 return err
178 }
179 var plan claimactivation.Plan
180 dec := json.NewDecoder(bytes.NewReader(raw))
181 dec.DisallowUnknownFields()
182 if dec.Decode(&plan) != nil || dec.Decode(new(any)) != io.EOF {
183 return errActivation
184 }
185 keyRaw, err := activationRead(*keyPath, true)
186 if err != nil {
187 return err
188 }
189 key, err := release.ParseSigningKey(string(keyRaw), *keyPath)
190 if err != nil {
191 return errActivation
192 }
193 pub, err := release.ParsePublicKey(*pubValue, "public-key")
194 if err != nil {
195 return errActivation
196 }
197 if !bytes.Equal(key.Public().(ed25519.PublicKey), pub) {
198 return errActivation
199 }
200 // sign-policy and publish-identity produce a file, written to --out
201 // byte for byte; every other phase takes no --out.
202 produces := plan.Phase == "sign-policy" || plan.Phase == "publish-identity"
203 if produces != (*outPath != "") {
204 return errActivation
205 }
206 store, err := release.NewDeploymentS3StoreIn(ctx, plan.Bucket, *region)
207 if err != nil {
208 return err
209 }
210 runner := activationRunner(store, pub, plan.Bucket, key)
211 var result claimactivation.Result
212 if produces {
213 // Reserved before the phase can write to the bucket (F4).
214 out, e := reserveActivationOutput(*outPath, *dry)
215 if e != nil {
216 return e
217 }
218 result, err = runProducingPhase(ctx, runner, &putAttempts{Store: store}, plan, out, *dry)
219 } else {
220 result, err = runner.Run(ctx, plan, *dry)
221 }
222 if err != nil {
223 return err
224 }
225 return encode(struct {
226 claimactivation.Result
227 Out string `json:"out,omitempty"`
228 }{result, *outPath})
229 case "evidence-key":
230 if *rosterPath == "" || *revision == 0 || *bucket == "" || *box == "" || *enrollment == "" {
231 return errActivation
232 }
233 pub, err := release.ParsePublicKey(*pubValue, "public-key")
234 if err != nil {
235 return errActivation
236 }
237 raw, err := activationRead(*rosterPath, false)
238 if err != nil {
239 return err
240 }
241 var roster hostactors.Roster
242 if release.VerifyRootEnvelope(pub, raw, &roster) != nil || roster.Validate(*bucket) != nil || roster.Revision != *revision {
243 return errActivation
244 }
245 actor, ok := roster.Boxes[*box]
246 if !ok || actor.Enrollment != *enrollment || actor.State != "active" {
247 return errActivation
248 }
249 localRaw, err := activationRead("/etc/carlos/host-actor.json", false)
250 if err != nil {
251 return err
252 }
253 var local hostfloor.Enrollment
254 if json.Unmarshal(localRaw, &local) != nil || local.Box != *box || local.Enrollment != *enrollment || local.InstanceID != actor.Cloud.InstanceID {
255 return errActivation
256 }
257 proof, err := installActivationEvidenceKey("/etc/carlos/host-evidence.key", hostactors.CensusKeyBinding{Bucket: *bucket, Box: *box, Enrollment: *enrollment, PublicKey: actor.CensusPublicKey}, *dry)
258 if err != nil {
259 return err
260 }
261 return encode(struct {
262 DryRun bool `json:"dry_run"`
263 Path string `json:"path"`
264 Proof activationKeyProof `json:"key_proof"`
265 }{*dry, "/etc/carlos/host-evidence.key", proof})
266 case "fence-credentials":
267 if *source == "" || *bucket == "" || *region == "" || *role == "" {
268 return errActivation
269 }
270 target := "/etc/carlos/fence-writer/credentials.json"
271 if err := installActivationFenceCredentials(*source, target, activationFenceScope{Bucket: *bucket, Region: *region, Role: *role}, *dry); err != nil {
272 return err
273 }
274 return encode(map[string]any{"dry_run": *dry, "source": *source, "target": target, "mode": "0600", "bucket": *bucket, "aws_region": *region, "role": *role})
275 case "install-console":
276 if *identityPath == "" || *source == "" || *bucket == "" || *region == "" {
277 return errActivation
278 }
279 pub, err := release.ParsePublicKey(*pubValue, "public-key")
280 if err != nil {
281 return errActivation
282 }
283 store, err := release.NewDeploymentS3StoreIn(ctx, *bucket, *region)
284 if err != nil {
285 return err
286 }
287 tag, sha, got, err := runInstallConsole(ctx, store, pub, *identityPath, *source, consoleInstallDir, version.Build, runtime.GOOS+"-"+runtime.GOARCH, *from, *dry)
288 if err != nil {
289 return err
290 }
291 result := map[string]any{"dry_run": *dry, "release_tag": tag, "sha256": sha, "source": *source, "path": got.Path, "previous": got.Previous, "already_installed": got.Already, "mode": "0755"}
292 if *upgrade {
293 result["upgraded"], result["from"], result["previous_sha"] = true, got.From, got.PreviousSHA
294 }
295 return encode(result)
296 case "install-control":
297 if err := verifyOfflineRootMigration(ctx); err != nil {
298 return err
299 }
300 if err := installActivationControl("/etc/carlos/host.env", "/etc/systemd/system", *dry, productionSysOps().systemctl); err != nil {
301 return err
302 }
303 return encode(map[string]any{"dry_run": *dry, "source": "/etc/carlos/host.env", "target": "/etc/systemd/system/" + claimControlUnit, "reload": true, "start": false})
304 default:
305 return errActivation
306 }
307}
308
309// upgradeFlagsRefused reports whether parsed flags misuse --upgrade and
310// --from: they belong to install-console and come together, so anything
311// else is refused as an unknown flag was before them.
312func upgradeFlagsRefused(command string, fs *flag.FlagSet, upgrade bool, from string) bool {
313 set := false
314 fs.Visit(func(f *flag.Flag) { set = set || f.Name == "upgrade" || f.Name == "from" })
315 return set && (command != "install-console" || !upgrade || from == "")
316}
317
318// activationRunner is claims run's runner. Its sums key is the vendor's
319// release key that the carlos binary embeds (publicrelease.Default, the same
320// key update.go's cliUpdatePubKey holds), so publish-identity accepts only a
321// checksum list the vendor signed.
322func activationRunner(store release.Store, pub ed25519.PublicKey, bucket string, key ed25519.PrivateKey) claimactivation.Runner {
323 return claimactivation.Runner{Directory: hostactors.Directory{Store: store, PublicKey: pub, Bucket: bucket}, Signer: release.RootSigner(key), SumsKey: publicrelease.Default().PubKey}
324}
325