@forgezero/agent 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -138,393 +138,6 @@ function shapeEgressUnitDirectives(tap, guaranteedMbps, burstMbps) {
138
138
  }
139
139
  var tapFor = (guestIndex) => `tap${guestIndex}`;
140
140
 
141
- // src/provision.ts
142
- function atLeast(version, floor) {
143
- const parse = (value) => (value.trim().replace(/^v/, "").match(/\d+/g) ?? []).slice(0, 3).map(Number);
144
- const got = parse(version);
145
- const want = parse(floor);
146
- if (got.length === 0)
147
- return false;
148
- for (let index = 0;index < want.length; index += 1) {
149
- const a = got[index] ?? 0;
150
- const b = want[index] ?? 0;
151
- if (a > b)
152
- return true;
153
- if (a < b)
154
- return false;
155
- }
156
- return true;
157
- }
158
- var CAPABILITY_CHECKS = {
159
- snpGuest: {
160
- command: "test -e /dev/sev-guest && echo yes || echo no",
161
- satisfied: (stdout) => stdout.trim() === "yes",
162
- remedy: "Not a confidential guest. The agent will run in `enrolled` mode, which is still stronger than an API key in the application."
163
- },
164
- systemd: {
165
- command: "test -d /run/systemd/system && echo yes || echo no",
166
- satisfied: (stdout) => stdout.trim() === "yes",
167
- remedy: "systemd is what supervises the agent. On a non-systemd host, run `fz-agent` under whatever supervises services there."
168
- },
169
- bun: {
170
- command: "bun --version 2>/dev/null || echo missing",
171
- satisfied: (stdout) => atLeast(stdout, "1.1.0"),
172
- remedy: "Install bun: curl -fsSL https://bun.sh/install | bash"
173
- },
174
- python: {
175
- command: "python3 --version 2>/dev/null || echo missing",
176
- satisfied: (stdout, exitCode) => exitCode === 0 && /^Python 3\./.test(stdout.trim()),
177
- remedy: "Install Python 3. It supplies the standard-library ioctl boundary for SNP reports."
178
- }
179
- };
180
- var modeFor = (capabilities) => capabilities.snpGuest ? "attested" : "enrolled";
181
- var reasonFor = (mode) => mode === "attested" ? "SEV-SNP guest device present, so the agent can prove what it is running and the platform can refuse it if the measurement is wrong." : "No SEV-SNP guest device. The agent authenticates with its enrolment token and hybrid Ed25519 + ML-DSA signature — weaker than attestation, stronger than an API key in the application.";
182
- var DEPLOYMENT_RUNNER_USER = "forgezero-runner";
183
- var DEPLOYMENT_GROUP = "forgezero-deploy";
184
- var VAULT_GROUP = "forgezero-vault";
185
- var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
186
- var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
187
- var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
188
- function agentEnrolmentUnit(options) {
189
- if (!options.apiUrl || !options.enrolTokenCredentialPath || !options.enrolStatePath) {
190
- throw new Error("direct enrolment needs API, credential and state paths");
191
- }
192
- const bin = options.binPath ?? "fz-agent";
193
- const user = options.user ?? "forgezero";
194
- const seedCredentialPath = options.seedCredentialPath ?? "/etc/forgezero/creds/agent-seed.cred";
195
- const label = options.nodeLabel ? `Environment=FZ_NODE_LABEL=${options.nodeLabel}
196
- ` : "";
197
- const gitPublicKey = options.gitPublicKeyPath ? `Environment=FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}
198
- ` : "";
199
- const stateDir = options.enrolStatePath.replace(/\/[^/]+$/, "");
200
- return `[Unit]
201
- Description=Bind this machine to its ForgeZero compute
202
- After=network-online.target
203
- Wants=network-online.target
204
- Before=forgezero-agent.service
205
- ConditionPathExists=!${options.enrolStatePath}
206
-
207
- [Service]
208
- Type=oneshot
209
- User=${user}
210
- Group=${user}
211
- LoadCredentialEncrypted=agent-seed:${seedCredentialPath}
212
- LoadCredentialEncrypted=enrol-token:${options.enrolTokenCredentialPath}
213
- Environment=FZ_SEED_CREDENTIAL=agent-seed
214
- Environment=FZ_ENROL_TOKEN_CREDENTIAL=enrol-token
215
- Environment=FZ_ENROL_STATE_FILE=${options.enrolStatePath}
216
- Environment=FZ_API=${options.apiUrl}
217
- ${label}${gitPublicKey}ExecStart=${bin} enrol
218
- # A '+' fixed command runs as root solely to remove the host-bound one-time
219
- # ciphertext. Tenant code and the agent never receive a privilege boundary.
220
- ExecStartPost=+/usr/bin/rm -f ${options.enrolTokenCredentialPath}
221
- NoNewPrivileges=true
222
- PrivateTmp=true
223
- ProtectSystem=strict
224
- ProtectHome=true
225
- ReadWritePaths=${stateDir}
226
- LimitCORE=0
227
-
228
- [Install]
229
- WantedBy=multi-user.target
230
- `;
231
- }
232
- function deploymentRunnerUnit(options) {
233
- const bin = options.binPath ?? "fz-agent";
234
- const root = options.deployRoot ?? "/opt/forgezero";
235
- const agentUser = options.user ?? "forgezero-agent";
236
- return `[Unit]
237
- Description=ForgeZero credential-free project command runner
238
- Documentation=https://www.forgezero.net/docs/agent
239
-
240
- [Service]
241
- Type=notify
242
- NotifyAccess=all
243
- User=${DEPLOYMENT_RUNNER_USER}
244
- Group=${DEPLOYMENT_GROUP}
245
- Environment=FZ_DEPLOY_RUNNER_SOCKET=${DEPLOYMENT_RUNNER_SOCKET}
246
- RuntimeDirectory=forgezero-deploy
247
- RuntimeDirectoryMode=0710
248
- ExecStartPre=+/usr/bin/install -d -o ${DEPLOYMENT_RUNNER_USER} -g ${DEPLOYMENT_GROUP} -m 0710 /run/forgezero-deploy
249
- ExecStartPre=+/usr/bin/rm -f ${DEPLOYMENT_RUNNER_SOCKET}
250
- ExecStart=${bin} deploy-runner --root=${root} --home=${root}/runner-home
251
- ExecStartPost=+/usr/bin/chown ${agentUser}:${agentUser} ${DEPLOYMENT_RUNNER_SOCKET}
252
- ExecStartPost=+/usr/bin/chmod 0600 ${DEPLOYMENT_RUNNER_SOCKET}
253
- ExecStartPost=+/usr/bin/chown root:${VAULT_GROUP} /run/forgezero-deploy
254
- ExecStopPost=+/usr/bin/rm -f ${DEPLOYMENT_RUNNER_SOCKET}
255
- Restart=always
256
- RestartSec=2
257
- UMask=0007
258
- LimitCORE=0
259
- NoNewPrivileges=false
260
- PrivateTmp=true
261
- ProtectSystem=strict
262
- ProtectHome=true
263
- ProtectKernelTunables=true
264
- ProtectKernelModules=true
265
- ProtectControlGroups=true
266
- RestrictRealtime=true
267
- MemoryDenyWriteExecute=true
268
- LockPersonality=true
269
- ReadWritePaths=${root}/releases ${root}/runner-home
270
-
271
- [Install]
272
- WantedBy=multi-user.target
273
- `;
274
- }
275
- function agentUnit(options) {
276
- const bin = options.binPath ?? "fz-agent";
277
- const user = options.user ?? "forgezero";
278
- const seedCredentialPath = options.seedCredentialPath ?? "/etc/forgezero/creds/agent-seed.cred";
279
- const controlSocketPath = options.controlSocketPath ?? "/run/forgezero/control.sock";
280
- const deployRoot = options.deployRoot ?? "/opt/forgezero";
281
- const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
282
- const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath && options.enrolStatePath);
283
- const deploymentEnvironment = options.deploymentEnvironment ?? {};
284
- const deploymentCredentials = options.deploymentCredentials ?? {};
285
- for (const [name, value] of Object.entries(deploymentEnvironment)) {
286
- if (!/^[A-Z_][A-Z0-9_]*$/.test(name) || !/^[A-Za-z0-9._:\/@+-]+$/.test(value)) {
287
- throw new Error(`invalid deployment environment entry: ${name}`);
288
- }
289
- }
290
- for (const [name, path] of Object.entries(deploymentCredentials)) {
291
- if (!/^[A-Z_][A-Z0-9_]*$/.test(name) || !path.startsWith("/") || /[\r\n:]/.test(path)) {
292
- throw new Error(`invalid deployment credential entry: ${name}`);
293
- }
294
- }
295
- const environment = [
296
- `FZ_SOCKET_PATH=${options.socketPath}`,
297
- `FZ_CONTROL_SOCKET=${controlSocketPath}`,
298
- `FZ_SEED_CREDENTIAL=agent-seed`,
299
- `FZ_AGENT_MODE=${options.mode}`,
300
- options.apiUrl ? `FZ_API=${options.apiUrl}` : null,
301
- options.project ? `FZ_PROJECT=${options.project}` : null,
302
- options.environment ? `FZ_ENVIRONMENT=${options.environment}` : null,
303
- options.enrolStatePath ? `FZ_ENROL_STATE_FILE=${options.enrolStatePath}` : null,
304
- options.nodeLabel ? `FZ_NODE_LABEL=${options.nodeLabel}` : null,
305
- options.gitPublicKeyPath ? `FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}` : null,
306
- options.repository ? `FZ_DEPLOY_REPO=${options.repository}` : null,
307
- options.branch ? `FZ_DEPLOY_BRANCH=${options.branch}` : null,
308
- options.role ? `FZ_DEPLOY_ROLE=${options.role}` : null,
309
- options.repository && options.branch ? `FZ_DEPLOY_KEY=${options.project ?? "platform"}:${options.environment ?? "production"}` : null,
310
- deploymentEnabled ? `FZ_DEPLOY_ROOT=${deployRoot}` : null,
311
- deploymentEnabled ? `FZ_DEPLOY_RUNNER_SOCKET=${DEPLOYMENT_RUNNER_SOCKET}` : null,
312
- Object.keys(deploymentCredentials).length > 0 ? `FZ_DEPLOY_SYSTEMD_SECRETS=${Object.keys(deploymentCredentials).join(",")}` : null,
313
- Object.keys(deploymentEnvironment).length > 0 ? `FZ_DEPLOY_ENV_NAMES=${Object.keys(deploymentEnvironment).join(",")}` : null,
314
- ...Object.entries(deploymentEnvironment).map(([name, value]) => `${name}=${value}`),
315
- options.publicApiUrl ? `FZ_PUBLIC_API_URL=${options.publicApiUrl}` : null,
316
- options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null
317
- ].filter((line) => line !== null);
318
- if (deploymentEnabled) {
319
- environment.push(`HOME=${deployRoot}/agent-home`, `XDG_CACHE_HOME=${deployRoot}/cache`);
320
- }
321
- const gitCredential = options.gitCredentialPath ? `LoadCredentialEncrypted=git-deploy-key:${options.gitCredentialPath}
322
- ` : "";
323
- const projectCredentials = Object.entries(deploymentCredentials).map(([name, path]) => `LoadCredentialEncrypted=${name}:${path}`).join(`
324
- `);
325
- const deploymentWrites = deploymentEnabled ? `ReadWritePaths=${deployRoot}/releases ${deployRoot}/agent-home ${deployRoot}/cache` : "";
326
- const deploymentGroup = deploymentEnabled ? `SupplementaryGroups=${DEPLOYMENT_GROUP}` : "";
327
- const after = [
328
- "network-online.target",
329
- deploymentEnabled ? "forgezero-deploy-runner.service" : null,
330
- enrolmentEnabled ? "forgezero-agent-enrol.service" : null
331
- ].filter((value) => value !== null);
332
- const requires = [
333
- deploymentEnabled ? "forgezero-deploy-runner.service" : null,
334
- enrolmentEnabled ? "forgezero-agent-enrol.service" : null
335
- ].filter((value) => value !== null);
336
- const deploymentDependency = [
337
- `After=${after.join(" ")}`,
338
- "Wants=network-online.target",
339
- requires.length > 0 ? `Requires=${requires.join(" ")}` : null
340
- ].filter((value) => value !== null).join(`
341
- `);
342
- const snpDevice = options.mode === "attested" ? `DevicePolicy=closed
343
- DeviceAllow=/dev/sev-guest rw` : "";
344
- const snpPrepare = options.mode === "attested" ? `ExecStartPre=+/bin/chgrp ${VAULT_GROUP} /dev/sev-guest
345
- ExecStartPre=+/bin/chmod 0640 /dev/sev-guest
346
- ` : "";
347
- return `[Unit]
348
- Description=ForgeZero node agent (${options.mode})
349
- Documentation=https://www.forgezero.net/docs/agent
350
- ${deploymentDependency}
351
-
352
- [Service]
353
- Type=simple
354
- User=${user}
355
- Group=${VAULT_GROUP}
356
- ${deploymentGroup}
357
- LoadCredentialEncrypted=agent-seed:${seedCredentialPath}
358
- ${gitCredential}${projectCredentials}${projectCredentials ? `
359
- ` : ""}${snpPrepare}ExecStart=${bin}
360
- Restart=always
361
- RestartSec=2
362
-
363
- ${environment.map((line) => `Environment=${line}`).join(`
364
- `)}
365
-
366
- # The node seed and the vault replica live in this process's memory. A core dump
367
- # writes both to disk, which is the one artefact this design exists to remove.
368
- LimitCORE=0
369
-
370
- # The socket is the entire interface: anything that can read it can read the
371
- # scope. So it lives in a directory systemd creates with a known owner rather
372
- # than wherever the process happened to have write access.
373
- RuntimeDirectory=forgezero
374
- RuntimeDirectoryMode=0750
375
- UMask=0007
376
-
377
- # Tenant-controlled commands execute in forgezero-deploy-runner.service. This
378
- # credential-bearing process never needs to cross a privilege boundary.
379
- NoNewPrivileges=true
380
- PrivateTmp=true
381
- ProtectSystem=strict
382
- ProtectHome=true
383
- ProtectKernelTunables=true
384
- ProtectKernelModules=true
385
- ProtectControlGroups=true
386
- RestrictSUIDSGID=true
387
- RestrictRealtime=true
388
- MemoryDenyWriteExecute=true
389
- LockPersonality=true
390
- ${snpDevice}
391
- ${deploymentWrites}
392
-
393
- [Install]
394
- WantedBy=multi-user.target
395
- `;
396
- }
397
- var UNIT_PATH = "/etc/systemd/system/forgezero-agent.service";
398
- function planProvision(options) {
399
- const mode = options.mode;
400
- const user = options.user ?? "forgezero";
401
- const seedCredentialPath = options.seedCredentialPath ?? "/etc/forgezero/creds/agent-seed.cred";
402
- const credentialDir = seedCredentialPath.replace(/\/[^/]+$/, "");
403
- const deployRoot = options.deployRoot ?? "/opt/forgezero";
404
- const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
405
- const enrolmentEnabled = Boolean(options.enrolTokenSourcePath && options.enrolTokenCredentialPath && options.enrolStatePath);
406
- if (Boolean(options.enrolTokenSourcePath) !== Boolean(options.enrolTokenCredentialPath) || Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath))
407
- throw new Error("direct enrolment paths must be supplied together");
408
- const safePath = (value, label) => {
409
- if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
410
- throw new Error(`invalid ${label} path`);
411
- return value;
412
- };
413
- const enrolTokenSourcePath = enrolmentEnabled ? safePath(options.enrolTokenSourcePath, "enrolment source") : undefined;
414
- const enrolTokenCredentialPath = enrolmentEnabled ? safePath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
415
- const enrolStatePath = enrolmentEnabled ? safePath(options.enrolStatePath, "enrolment state") : undefined;
416
- const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
417
- const sourceBinPath = options.sourceBinPath ? safePath(options.sourceBinPath, "agent source binary") : undefined;
418
- const binPath = options.binPath ? safePath(options.binPath, "agent binary") : undefined;
419
- const gitCredentialPath = options.gitCredentialPath ? safePath(options.gitCredentialPath, "Git credential") : undefined;
420
- const gitPublicKeyPath = options.gitPublicKeyPath ? safePath(options.gitPublicKeyPath, "Git public key") : undefined;
421
- if (options.generateGitIdentity && (!gitCredentialPath || !gitPublicKeyPath)) {
422
- throw new Error("generated Git identity needs credential and public-key paths");
423
- }
424
- const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
425
- return {
426
- mode,
427
- reason: reasonFor(mode),
428
- unitPath: UNIT_PATH,
429
- unit: agentUnit({ ...options, mode }),
430
- auxiliaryUnits: [
431
- ...deploymentEnabled ? [
432
- { path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) }
433
- ] : [],
434
- ...enrolmentEnabled ? [
435
- { path: ENROLMENT_UNIT_PATH, unit: agentEnrolmentUnit(options) }
436
- ] : []
437
- ],
438
- socketPath: options.socketPath,
439
- user,
440
- steps: [
441
- {
442
- label: "vault socket access group",
443
- command: `groupadd --system ${VAULT_GROUP} || true`
444
- },
445
- ...sourceBinPath && binPath ? [{
446
- label: "root-owned agent runtime",
447
- command: `install -d -o root -g root -m 0755 ${binPath.replace(/\/[^/]+$/, "")}; ` + `install -o root -g root -m 0755 ${sourceBinPath} ${binPath}`
448
- }] : [],
449
- ...deploymentEnabled ? [{
450
- label: "deployment isolation group",
451
- command: `groupadd --system ${DEPLOYMENT_GROUP} || true`
452
- }] : [],
453
- {
454
- label: "service account",
455
- command: `useradd --system --no-create-home --shell /usr/sbin/nologin ${user} || true`
456
- },
457
- {
458
- label: "bind service account to vault group",
459
- command: `usermod -g ${VAULT_GROUP} ${user}`
460
- },
461
- ...deploymentEnabled ? [{
462
- label: "credential-free deployment account",
463
- command: `useradd --system --no-create-home --shell /usr/sbin/nologin --gid ${DEPLOYMENT_GROUP} ${DEPLOYMENT_RUNNER_USER} || true; ` + `usermod -a -G ${DEPLOYMENT_GROUP} ${user}`
464
- }] : [],
465
- {
466
- label: "credential directory",
467
- command: `install -d -o root -g root -m 0700 ${credentialDir}`
468
- },
469
- {
470
- label: "encrypted node identity",
471
- command: `test -s ${seedCredentialPath} || { ` + `openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\\n' | ` + `systemd-creds encrypt --name=agent-seed - ${seedCredentialPath}; ` + `chmod 0400 ${seedCredentialPath}; }`
472
- },
473
- ...options.generateGitIdentity && gitCredentialPath && gitPublicKeyPath ? [
474
- {
475
- label: "Git deploy identity directory",
476
- command: `install -d -o root -g root -m 0755 ${gitPublicKeyDir}`
477
- },
478
- {
479
- label: "unique encrypted Git deploy identity",
480
- command: `test -s ${gitCredentialPath} || { ` + `rm -f /run/forgezero-git-deploy-key /run/forgezero-git-deploy-key.pub; ` + `ssh-keygen -q -t ed25519 -N '' -C forgezero-compute -f /run/forgezero-git-deploy-key; ` + `systemd-creds encrypt --name=git-deploy-key /run/forgezero-git-deploy-key ${gitCredentialPath}; ` + `install -o root -g root -m 0444 /run/forgezero-git-deploy-key.pub ${gitPublicKeyPath}; ` + `rm -f /run/forgezero-git-deploy-key /run/forgezero-git-deploy-key.pub; ` + `chmod 0400 ${gitCredentialPath}; }; ` + `test -s ${gitPublicKeyPath} || { ` + `systemd-creds decrypt --name=git-deploy-key ${gitCredentialPath} /run/forgezero-git-deploy-key; ` + `ssh-keygen -y -f /run/forgezero-git-deploy-key | ` + `sed 's/$/ forgezero-compute/' > /run/forgezero-git-deploy-key.pub; ` + `install -o root -g root -m 0444 /run/forgezero-git-deploy-key.pub ${gitPublicKeyPath}; ` + `rm -f /run/forgezero-git-deploy-key /run/forgezero-git-deploy-key.pub; }; ` + `test -s ${gitCredentialPath} && test -s ${gitPublicKeyPath}`
481
- }
482
- ] : [],
483
- ...enrolmentEnabled ? [
484
- {
485
- label: "enrolment state directory",
486
- command: `install -d -o ${user} -g ${user} -m 0700 ${enrolStateDir}`
487
- },
488
- {
489
- label: "encrypted one-time enrolment capability",
490
- command: `test -s ${enrolTokenCredentialPath} || { test -r ${enrolTokenSourcePath}; ` + `systemd-creds encrypt --name=enrol-token ${enrolTokenSourcePath} ${enrolTokenCredentialPath}; ` + `chmod 0400 ${enrolTokenCredentialPath}; rm -f ${enrolTokenSourcePath}; }`
491
- }
492
- ] : [],
493
- ...deploymentEnabled ? [{
494
- label: "deployment directories",
495
- command: `install -d -o root -g root -m 0755 ${deployRoot} && ` + `install -d -o root -g ${DEPLOYMENT_GROUP} -m 3770 ${deployRoot}/releases && ` + `install -d -o ${user} -g ${user} -m 0750 ${deployRoot}/cache && ` + `install -d -o ${user} -g ${user} -m 0700 ${deployRoot}/agent-home && ` + `install -d -o ${DEPLOYMENT_RUNNER_USER} -g ${DEPLOYMENT_GROUP} -m 0700 ${deployRoot}/runner-home ${deployRoot}/runner-home/cache`
496
- }] : [],
497
- { label: "reload units", command: "systemctl daemon-reload" },
498
- ...deploymentEnabled ? [{
499
- label: "remove unsupported deployment socket activation",
500
- command: "systemctl disable --now forgezero-deploy-runner.socket 2>/dev/null || true; rm -f /etc/systemd/system/forgezero-deploy-runner.socket; systemctl daemon-reload"
501
- }] : [],
502
- {
503
- label: "enable and start",
504
- command: `systemctl enable --now ${[
505
- ...deploymentEnabled ? ["forgezero-deploy-runner.service"] : [],
506
- ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
507
- "forgezero-agent.service"
508
- ].join(" ")}`
509
- },
510
- ...enrolmentEnabled ? [{
511
- label: "prove the compute binding is durable",
512
- command: `test -s ${enrolStatePath}`
513
- }] : [],
514
- { label: "prove it is running", command: "systemctl is-active forgezero-agent.service" },
515
- { label: "prove the vault socket exists", command: `test -S ${options.socketPath}` },
516
- ...deploymentEnabled ? [{
517
- label: "prove the deployment runner socket exists",
518
- command: `test -S ${DEPLOYMENT_RUNNER_SOCKET}`
519
- }] : [],
520
- ...options.repository ? [{
521
- label: "prove the deployment control socket exists",
522
- command: `test -S ${options.controlSocketPath ?? "/run/forgezero/control.sock"}`
523
- }] : []
524
- ]
525
- };
526
- }
527
-
528
141
  // src/ubuntu.ts
529
142
  var SUPPORTED_GUEST_IMAGE = Object.freeze({
530
143
  key: "ubuntu-resolute-20260731",
@@ -717,7 +330,7 @@ var yamlFile = (path, content, permissions) => ` - path: ${JSON.stringify(path)
717
330
  encoding: b64
718
331
  content: ${base64(content)}
719
332
  `;
720
- function guestBootstrapScript(profile, attested = Boolean(profile.confidential), hasEnrolment = true) {
333
+ function guestBootstrapScript(profile, attested = Boolean(profile.confidential), hasEnrolment = true, nodeLabel = "compute") {
721
334
  const agentBun = "/usr/local/lib/forgezero/bun";
722
335
  const attestationSetup = attested ? `# The report device is not part of the encryption path, so a guest can appear
723
336
  # healthy and encrypted while attestation is silently impossible. Install and
@@ -731,26 +344,7 @@ printf 'sev-guest
731
344
  ` : "";
732
345
  return `#!/usr/bin/env bash
733
346
  set -Eeuo pipefail
734
- ${attestationSetup}groupadd --system ${VAULT_GROUP} 2>/dev/null || true
735
- useradd --system --no-create-home --shell /usr/sbin/nologin forgezero-agent 2>/dev/null || true
736
- usermod -g ${VAULT_GROUP} forgezero-agent
737
- groupadd --system ${DEPLOYMENT_GROUP} 2>/dev/null || true
738
- useradd --system --no-create-home --shell /usr/sbin/nologin --gid ${DEPLOYMENT_GROUP} ${DEPLOYMENT_RUNNER_USER} 2>/dev/null || true
739
- usermod -a -G ${DEPLOYMENT_GROUP} forgezero-agent
740
- install -d -o root -g root -m 0700 /etc/forgezero/creds
741
- install -d -o root -g root -m 0755 /etc/forgezero/git
742
- install -d -o forgezero-agent -g forgezero-agent -m 0700 /var/lib/forgezero
743
- install -d -o root -g root -m 0755 /opt/forgezero
744
- install -d -o root -g ${DEPLOYMENT_GROUP} -m 3770 /opt/forgezero/releases
745
- install -d -o forgezero-agent -g forgezero-agent -m 0700 /opt/forgezero/cache /opt/forgezero/home
746
- install -d -o ${DEPLOYMENT_RUNNER_USER} -g ${DEPLOYMENT_GROUP} -m 0700 /opt/forgezero/runner-home /opt/forgezero/runner-home/cache
747
- ${hasEnrolment ? `if [[ -s /run/forgezero-enrol-token ]]; then
748
- systemd-creds encrypt --name=enrol-token /run/forgezero-enrol-token /var/lib/forgezero/enrol-token.cred
749
- rm -f /run/forgezero-enrol-token
750
- fi
751
- chown root:root /var/lib/forgezero/enrol-token.cred
752
- chmod 0400 /var/lib/forgezero/enrol-token.cred
753
- ` : ""}if [[ ! -x /usr/local/bin/bun ]]; then
347
+ ${attestationSetup}if [[ ! -x /usr/local/bin/bun ]]; then
754
348
  curl -fsSL https://bun.sh/install -o /run/fz-bun-install
755
349
  printf '%s %s
756
350
  ' '${profile.bunInstallerSha256}' /run/fz-bun-install | sha256sum -c -
@@ -758,132 +352,16 @@ chmod 0400 /var/lib/forgezero/enrol-token.cred
758
352
  install -m 0755 ${agentBun}/bin/bun /usr/local/bin/bun
759
353
  rm -f /run/fz-bun-install
760
354
  fi
761
- if [[ ! -x /usr/local/bin/fz-agent ]]; then
355
+ if [[ ! -x /usr/local/bin/fz-agent ]] || [[ "$(/usr/local/bin/fz-agent --version 2>/dev/null || true)" != "${profile.agentVersion}" ]]; then
762
356
  env BUN_INSTALL=${agentBun} /usr/local/bin/bun add -g --no-cache --force @forgezero/agent@${profile.agentVersion}
763
357
  ln -sfn ${agentBun}/bin/fz-agent /usr/local/bin/fz-agent
764
358
  fi
765
- if [[ ! -s /etc/forgezero/creds/agent-seed.cred ]]; then
766
- umask 077
767
- openssl rand -base64 32 | tr '+/' '-_' | tr -d '=
768
- ' >/run/fz-agent-seed
769
- systemd-creds encrypt --name=agent-seed /run/fz-agent-seed /etc/forgezero/creds/agent-seed.cred
770
- rm -f /run/fz-agent-seed
771
- fi
772
- chmod 0400 /etc/forgezero/creds/agent-seed.cred
773
- if [[ ! -s /etc/forgezero/creds/git-deploy-key.cred ]]; then
774
- umask 077
775
- rm -f /run/fz-git-deploy-key /run/fz-git-deploy-key.pub
776
- ssh-keygen -q -t ed25519 -N '' -C forgezero-compute -f /run/fz-git-deploy-key
777
- systemd-creds encrypt --name=git-deploy-key /run/fz-git-deploy-key /etc/forgezero/creds/git-deploy-key.cred
778
- install -o root -g root -m 0444 /run/fz-git-deploy-key.pub /etc/forgezero/git/deploy.pub
779
- rm -f /run/fz-git-deploy-key /run/fz-git-deploy-key.pub
780
- fi
781
- if [[ ! -s /etc/forgezero/git/deploy.pub ]]; then
782
- systemd-creds decrypt --name=git-deploy-key /etc/forgezero/creds/git-deploy-key.cred /run/fz-git-deploy-key
783
- ssh-keygen -y -f /run/fz-git-deploy-key | sed 's/$/ forgezero-compute/' >/run/fz-git-deploy-key.pub
784
- install -o root -g root -m 0444 /run/fz-git-deploy-key.pub /etc/forgezero/git/deploy.pub
785
- rm -f /run/fz-git-deploy-key /run/fz-git-deploy-key.pub
786
- fi
787
- chmod 0400 /etc/forgezero/creds/git-deploy-key.cred
788
- systemctl daemon-reload
789
- systemctl disable --now forgezero-deploy-runner.socket 2>/dev/null || true
790
- rm -f /etc/systemd/system/forgezero-deploy-runner.socket
791
- systemctl daemon-reload
792
- systemctl enable --now forgezero-deploy-runner.service forgezero-agent.service
793
- `;
794
- }
795
- function guestAgentUnit(profile, name, attested = Boolean(profile.confidential), pull = true) {
796
- const attestationPrepare = attested ? `ExecStartPre=+/bin/chgrp ${VAULT_GROUP} /dev/sev-guest
797
- ExecStartPre=+/bin/chmod 0640 /dev/sev-guest
798
- ` : "";
799
- const attestationDevice = attested ? `DevicePolicy=closed
800
- DeviceAllow=/dev/sev-guest rw
801
- ` : "";
802
- return `[Unit]
803
- Description=ForgeZero compute agent (${name})
804
- After=network-online.target forgezero-deploy-runner.service
805
- Wants=network-online.target
806
- Requires=forgezero-deploy-runner.service
807
-
808
- [Service]
809
- Type=simple
810
- User=forgezero-agent
811
- Group=${VAULT_GROUP}
812
- SupplementaryGroups=${DEPLOYMENT_GROUP}
813
- LoadCredentialEncrypted=agent-seed:/etc/forgezero/creds/agent-seed.cred
814
- LoadCredentialEncrypted=git-deploy-key:/etc/forgezero/creds/git-deploy-key.cred
815
- Environment=FZ_SEED_CREDENTIAL=agent-seed
816
- Environment=FZ_GIT_PUBLIC_KEY_FILE=/etc/forgezero/git/deploy.pub
817
- Environment=FZ_API=${profile.apiUrl}
818
- Environment=FZ_ENROL_STATE_FILE=/var/lib/forgezero/enrolment.json
819
- Environment=FZ_NODE_LABEL=${name}
820
- Environment=FZ_SOCKET_PATH=/run/forgezero/vault.sock
821
- Environment=FZ_DEPLOY_ROOT=/opt/forgezero
822
- Environment=FZ_DEPLOY_PULL=${pull ? "true" : "false"}
823
- Environment=FZ_DEPLOY_RUNNER_SOCKET=${DEPLOYMENT_RUNNER_SOCKET}
824
- Environment=HOME=/opt/forgezero/home
825
- ${attestationPrepare}ExecStart=/usr/local/bin/fz-agent
826
- Restart=on-failure
827
- RestartSec=5
828
- RuntimeDirectory=forgezero
829
- RuntimeDirectoryMode=0750
830
- UMask=0007
831
- LimitCORE=0
832
- NoNewPrivileges=true
833
- PrivateTmp=true
834
- ProtectSystem=strict
835
- ProtectHome=true
836
- ReadWritePaths=/var/lib/forgezero /opt/forgezero
837
- ${attestationDevice}
838
-
839
- [Install]
840
- WantedBy=multi-user.target
841
- `;
842
- }
843
- function guestEnrolmentDropIn() {
844
- return `[Service]
845
- LoadCredentialEncrypted=enrol-token:/var/lib/forgezero/enrol-token.cred
846
- Environment=FZ_ENROL_TOKEN_CREDENTIAL=enrol-token
847
- `;
848
- }
849
- function guestEnrolmentCleanupScript() {
850
- return `#!/usr/bin/env bash
851
- set -Eeuo pipefail
852
- for _ in $(seq 1 180); do
853
- [[ -s /var/lib/forgezero/enrolment.json ]] && break
854
- sleep 1
855
- done
856
- [[ -s /var/lib/forgezero/enrolment.json ]] || { echo 'guest enrolment did not become durable' >&2; exit 1; }
857
- rm -f /var/lib/forgezero/enrol-token.cred
858
- rm -f /etc/systemd/system/forgezero-agent.service.d/enrolment.conf
859
- systemctl daemon-reload
860
- systemctl disable forgezero-enrolment-cleanup.service
861
- `;
862
- }
863
- function guestEnrolmentCleanupUnit() {
864
- return `[Unit]
865
- Description=Remove the consumed ForgeZero guest enrolment credential
866
- After=forgezero-agent.service
867
- Requires=forgezero-agent.service
868
- ConditionPathExists=/var/lib/forgezero/enrol-token.cred
869
-
870
- [Service]
871
- Type=oneshot
872
- ExecStart=/usr/local/sbin/forgezero-enrolment-cleanup
873
- TimeoutStartSec=4min
874
-
875
- [Install]
876
- WantedBy=multi-user.target
359
+ env FZ_API=${profile.apiUrl} FZ_AGENT_BIN=/usr/local/bin/fz-agent FZ_AGENT_USER=forgezero-agent FZ_SOCKET_PATH=/run/forgezero/vault.sock FZ_SEED_CREDENTIAL_PATH=/etc/forgezero/creds/agent-seed.cred FZ_GIT_CREDENTIAL_PATH=/etc/forgezero/creds/git-deploy-key.cred FZ_GIT_PUBLIC_KEY_PATH=/etc/forgezero/git/deploy.pub FZ_DEPLOY_ROOT=/opt/forgezero FZ_DEPLOY_PULL=${hasEnrolment ? "true" : "false"} FZ_NODE_LABEL=${nodeLabel} ${agentBun}/bin/fz agent install --apply${hasEnrolment ? " --enrol" : ""}
877
360
  `;
878
361
  }
879
362
  function cloudInit(profile, claim, manifest) {
880
363
  const enrolled = Boolean(claim.enrolment);
881
- const bootstrap = guestBootstrapScript(profile, claim.spec.confidential, enrolled);
882
- const agentUnit2 = guestAgentUnit(profile, manifest.name, claim.spec.confidential, enrolled);
883
- const enrolmentDropIn = guestEnrolmentDropIn();
884
- const cleanupScript = guestEnrolmentCleanupScript();
885
- const cleanupUnit = guestEnrolmentCleanupUnit();
886
- const runnerUnit = deploymentRunnerUnit({ binPath: "/usr/local/bin/fz-agent", deployRoot: "/opt/forgezero", user: "forgezero-agent" });
364
+ const bootstrap = guestBootstrapScript(profile, claim.spec.confidential, enrolled, manifest.name);
887
365
  const access = claim.access;
888
366
  const users = access ? `users:
889
367
  - default
@@ -895,8 +373,8 @@ function cloudInit(profile, claim, manifest) {
895
373
  ${access.sshPublicKeys.map((key) => ` - ${JSON.stringify(key)}`).join(`
896
374
  `)}
897
375
  ` : "";
898
- const enrolmentFiles = enrolled ? `${yamlFile("/run/forgezero-enrol-token", `${claim.enrolment.token}
899
- `, "0600")}${yamlFile("/usr/local/sbin/forgezero-enrolment-cleanup", cleanupScript, "0700")}${yamlFile("/etc/systemd/system/forgezero-agent.service.d/enrolment.conf", enrolmentDropIn, "0644")}${yamlFile("/etc/systemd/system/forgezero-enrolment-cleanup.service", cleanupUnit, "0644")}` : "";
376
+ const enrolmentFiles = enrolled ? yamlFile("/run/forgezero-enrol-token", `${claim.enrolment.token}
377
+ `, "0600") : "";
900
378
  return {
901
379
  userData: `#cloud-config
902
380
  package_update: true
@@ -905,10 +383,8 @@ ${users}disable_root: true
905
383
  # every dynamically claimed repository must be cloneable on a clean image.
906
384
  packages: [curl, ca-certificates, openssl, openssh-client, git, unzip${claim.spec.confidential ? ", python3" : ""}]
907
385
  write_files:
908
- ${enrolmentFiles}${yamlFile("/usr/local/sbin/forgezero-guest-bootstrap", bootstrap, "0700")}${yamlFile("/etc/systemd/system/forgezero-deploy-runner.service", runnerUnit, "0644")}${yamlFile("/etc/systemd/system/forgezero-agent.service", agentUnit2, "0644")}runcmd:
386
+ ${enrolmentFiles}${yamlFile("/usr/local/sbin/forgezero-guest-bootstrap", bootstrap, "0700")}runcmd:
909
387
  - [ bash, /usr/local/sbin/forgezero-guest-bootstrap ]
910
- ${enrolled ? ` - [ systemctl, enable, --now, forgezero-enrolment-cleanup.service ]
911
- ` : ""}
912
388
  `,
913
389
  metaData: `instance-id: ${manifest.name}-${claim.attempt}
914
390
  local-hostname: ${manifest.name}
@@ -70,12 +70,7 @@ export declare function validateMetalProfile(profile: MetalProvisionProfile): vo
70
70
  export declare function allocateAddress(profile: MetalProvisionProfile, computeKey: string, rows: GuestManifest[]): string;
71
71
  /** Stable tight-fit allocation of one exclusive CPU/NUMA pool. */
72
72
  export declare function allocateCpuPool(profile: MetalProvisionProfile, claim: Pick<RemoteProvisionClaim, 'computeKey' | 'spec'>, rows: GuestManifest[]): MetalCpuPool;
73
- export declare function guestBootstrapScript(profile: MetalProvisionProfile, attested?: boolean, hasEnrolment?: boolean): string;
74
- export declare function guestAgentUnit(profile: MetalProvisionProfile, name: string, attested?: boolean, pull?: boolean): string;
75
- /** Removed by the root cleanup service once the non-secret binding is durable. */
76
- export declare function guestEnrolmentDropIn(): string;
77
- export declare function guestEnrolmentCleanupScript(): string;
78
- export declare function guestEnrolmentCleanupUnit(): string;
73
+ export declare function guestBootstrapScript(profile: MetalProvisionProfile, attested?: boolean, hasEnrolment?: boolean, nodeLabel?: string): string;
79
74
  export declare function cloudInit(profile: MetalProvisionProfile, claim: CreateRemoteProvisionClaim, manifest: GuestManifest): {
80
75
  userData: string;
81
76
  metaData: string;