@forgezero/agent 0.1.135 → 0.1.136

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.
package/dist/fz.js CHANGED
@@ -6007,7 +6007,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
6007
6007
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
6008
6008
 
6009
6009
  // src/version.ts
6010
- var VERSION2 = "0.1.135";
6010
+ var VERSION2 = "0.1.136";
6011
6011
 
6012
6012
  // src/software.ts
6013
6013
  var PINNED_BUN_VERSION = "1.3.14";
@@ -11572,11 +11572,13 @@ function compileDeployment(sourceValue) {
11572
11572
  if (provisioning.metalHostname !== undefined && !/^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/.test(String(provisioning.metalHostname)))
11573
11573
  fail2(`deployment.spec.targets.${targetName}.provisioning.metalHostname`, "is invalid.");
11574
11574
  const provisionedResources = object(provisioning.resources, `deployment.spec.targets.${targetName}.provisioning.resources`);
11575
- exact3(provisionedResources, ["physicalCores", "vcpu", "memoryGib", "diskGib", "egressGuaranteedMbps", "egressBurstMbps", "confidential"], `deployment.spec.targets.${targetName}.provisioning.resources`);
11575
+ exact3(provisionedResources, ["physicalCores", "vcpu", "memoryGib", "diskGib", "diskEncryption", "egressGuaranteedMbps", "egressBurstMbps", "confidential"], `deployment.spec.targets.${targetName}.provisioning.resources`);
11576
11576
  integer(provisionedResources.physicalCores, `deployment.spec.targets.${targetName}.provisioning.resources.physicalCores`, 1, 1024);
11577
11577
  integer(provisionedResources.vcpu, `deployment.spec.targets.${targetName}.provisioning.resources.vcpu`, 1, 4096);
11578
11578
  integer(provisionedResources.memoryGib, `deployment.spec.targets.${targetName}.provisioning.resources.memoryGib`, 1, 4194304);
11579
11579
  integer(provisionedResources.diskGib, `deployment.spec.targets.${targetName}.provisioning.resources.diskGib`, 1, 1048576);
11580
+ if (provisionedResources.diskEncryption !== undefined && !["none", "luks2"].includes(String(provisionedResources.diskEncryption)))
11581
+ fail2(`deployment.spec.targets.${targetName}.provisioning.resources.diskEncryption`, "must be none or luks2.");
11580
11582
  integer(provisionedResources.egressGuaranteedMbps, `deployment.spec.targets.${targetName}.provisioning.resources.egressGuaranteedMbps`, 0, 1e7);
11581
11583
  integer(provisionedResources.egressBurstMbps, `deployment.spec.targets.${targetName}.provisioning.resources.egressBurstMbps`, Number(provisionedResources.egressGuaranteedMbps), 1e7);
11582
11584
  if (typeof provisionedResources.confidential !== "boolean")
@@ -11945,7 +11947,7 @@ function initCoordinates(options) {
11945
11947
  if (!/^[a-z][a-z0-9-]{0,62}$/.test(value))
11946
11948
  throw new Error(`deployment provisioning ${label} must be a lowercase typed name`);
11947
11949
  const resources = options.provisioning.resources;
11948
- for (const [label, value] of Object.entries(resources).filter(([key]) => key !== "confidential")) {
11950
+ for (const [label, value] of Object.entries(resources).filter(([key]) => !["confidential", "diskEncryption"].includes(key))) {
11949
11951
  if (!Number.isSafeInteger(value) || Number(value) < 0)
11950
11952
  throw new Error(`deployment provisioning ${label} must be a non-negative integer`);
11951
11953
  }
@@ -11954,6 +11956,8 @@ function initCoordinates(options) {
11954
11956
  }
11955
11957
  if (result.isolation === "sev-snp" && !resources.confidential)
11956
11958
  throw new Error("deployment provisioning must provide confidential capacity for sev-snp");
11959
+ if (resources.diskEncryption !== undefined && !["none", "luks2"].includes(resources.diskEncryption))
11960
+ throw new Error("deployment provisioning disk encryption mode is unsupported");
11957
11961
  if (!/^\d{1,18}$/.test(options.provisioning.monthlyAmountCents))
11958
11962
  throw new Error("deployment provisioning monthly amount must be a bounded USD-cent amount");
11959
11963
  if (!/^\d{1,18}$/.test(options.provisioning.maxMonthlySpendMinor))
@@ -11965,7 +11969,10 @@ function initCoordinates(options) {
11965
11969
  if (options.provisioning.metalHostname && !/^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/.test(options.provisioning.metalHostname))
11966
11970
  throw new Error("deployment provisioning metal hostname is invalid");
11967
11971
  }
11968
- return { ...result, ...options.provisioning ? { provisioning: options.provisioning } : {} };
11972
+ return { ...result, ...options.provisioning ? { provisioning: {
11973
+ ...options.provisioning,
11974
+ resources: { ...options.provisioning.resources, diskEncryption: options.provisioning.resources.diskEncryption ?? "luks2" }
11975
+ } } : {} };
11969
11976
  }
11970
11977
  function defaultTypeScriptDeployment(name, options = {}) {
11971
11978
  const selected = initCoordinates(options);
@@ -12430,6 +12437,10 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
12430
12437
  throw new Error("platform genesis vCPU count cannot be smaller than physical cores");
12431
12438
  const memoryGib = integer2(input.memoryGib, 1, 8192, "memory");
12432
12439
  const diskGib = integer2(input.diskGib, 8, 65536, "disk");
12440
+ const diskEncryption = input.diskEncryption ?? "none";
12441
+ if (!["none", "luks2"].includes(diskEncryption)) {
12442
+ throw new Error("platform genesis disk encryption mode is unsupported");
12443
+ }
12433
12444
  const egressGuaranteedMbps = integer2(input.egressGuaranteedMbps, 0, 1e6, "guaranteed egress");
12434
12445
  const egressBurstMbps = integer2(input.egressBurstMbps, egressGuaranteedMbps, 1e6, "burst egress");
12435
12446
  return {
@@ -12441,6 +12452,7 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
12441
12452
  vcpu,
12442
12453
  memoryGib,
12443
12454
  diskGib,
12455
+ diskEncryption,
12444
12456
  egressGuaranteedMbps,
12445
12457
  egressBurstMbps,
12446
12458
  confidential: input.confidential
@@ -12471,6 +12483,7 @@ var claimFor = (guest, keys, action) => {
12471
12483
  vcpu: guest.vcpu,
12472
12484
  memoryGib: guest.memoryGib,
12473
12485
  diskGib: guest.diskGib,
12486
+ diskEncryption: guest.diskEncryption,
12474
12487
  egressGuaranteedMbps: guest.egressGuaranteedMbps,
12475
12488
  egressBurstMbps: guest.egressBurstMbps,
12476
12489
  confidential: guest.confidential
@@ -12560,6 +12573,7 @@ function validatePlatformInitialInventory(value) {
12560
12573
  "vcpu",
12561
12574
  "memoryGib",
12562
12575
  "diskGib",
12576
+ "diskEncryption",
12563
12577
  "egressGuaranteedMbps",
12564
12578
  "egressBurstMbps",
12565
12579
  "confidential",
@@ -15866,6 +15880,7 @@ function strictBootstrapDocument(value) {
15866
15880
  "vcpu",
15867
15881
  "memoryGib",
15868
15882
  "diskGib",
15883
+ "diskEncryption",
15869
15884
  "egressGuaranteedMbps",
15870
15885
  "egressBurstMbps",
15871
15886
  "confidential",
@@ -17100,6 +17115,7 @@ async function applyMetalBootstrap(config, options) {
17100
17115
  "qemu-utils",
17101
17116
  "cloud-image-utils",
17102
17117
  "lvm2",
17118
+ "cryptsetup",
17103
17119
  "nftables",
17104
17120
  "curl",
17105
17121
  "ca-certificates"
@@ -17534,11 +17550,12 @@ var validateMetalRequestValue = (value, options = {}) => {
17534
17550
  "vcpu",
17535
17551
  "memoryGib",
17536
17552
  "diskGib",
17553
+ "diskEncryption",
17537
17554
  "egressGuaranteedMbps",
17538
17555
  "egressBurstMbps",
17539
17556
  "confidential"
17540
17557
  ], label);
17541
- if (typeof row2.name !== "string" || typeof row2.address !== "string" || typeof row2.imageKey !== "string" || row2.cpuPoolKey !== undefined && typeof row2.cpuPoolKey !== "string" || !["physicalCores", "vcpu", "memoryGib", "diskGib", "egressGuaranteedMbps", "egressBurstMbps"].every((key) => typeof row2[key] === "number") || typeof row2.confidential !== "boolean") {
17558
+ if (typeof row2.name !== "string" || typeof row2.address !== "string" || typeof row2.imageKey !== "string" || row2.cpuPoolKey !== undefined && typeof row2.cpuPoolKey !== "string" || !["physicalCores", "vcpu", "memoryGib", "diskGib", "egressGuaranteedMbps", "egressBurstMbps"].every((key) => typeof row2[key] === "number") || row2.diskEncryption !== undefined && !["none", "luks2"].includes(row2.diskEncryption) || typeof row2.confidential !== "boolean") {
17542
17559
  throw new Error(`${label} has invalid resource coordinates`);
17543
17560
  }
17544
17561
  return {
@@ -17550,6 +17567,7 @@ var validateMetalRequestValue = (value, options = {}) => {
17550
17567
  vcpu: row2.vcpu,
17551
17568
  memoryGib: row2.memoryGib,
17552
17569
  diskGib: row2.diskGib,
17570
+ diskEncryption: row2.diskEncryption ?? "none",
17553
17571
  egressGuaranteedMbps: row2.egressGuaranteedMbps,
17554
17572
  egressBurstMbps: row2.egressBurstMbps,
17555
17573
  confidential: row2.confidential
@@ -19153,6 +19171,13 @@ function parseOptions(argv2) {
19153
19171
  } else if (token === "--provision-metal") {
19154
19172
  options.deployProvisionMetal = argv2[++index];
19155
19173
  options.deployInitCustomized = true;
19174
+ } else if (token === "--provision-disk-encryption") {
19175
+ const value = argv2[++index];
19176
+ options.deployInitCustomized = true;
19177
+ if (value === "none" || value === "luks2")
19178
+ options.deployProvisionDiskEncryption = value;
19179
+ else
19180
+ options.optionError = "--provision-disk-encryption must be none or luks2.";
19156
19181
  } else if (["--provision-physical-cores", "--provision-vcpu", "--provision-memory-gib", "--provision-disk-gib", "--provision-guaranteed-mbps", "--provision-burst-mbps"].includes(token)) {
19157
19182
  const value = Number(argv2[++index] ?? "");
19158
19183
  options.deployInitCustomized = true;
@@ -20001,6 +20026,7 @@ function interactiveGenesisGuests() {
20001
20026
  vcpu: bootstrapNumber(`Guest ${index} vCPUs`),
20002
20027
  memoryGib: bootstrapNumber(`Guest ${index} memory GiB`),
20003
20028
  diskGib: bootstrapNumber(`Guest ${index} disk GiB`),
20029
+ diskEncryption: bootstrapBoolean(`Guest ${index} encrypts its persistent disk with LUKS2?`, "yes") ? "luks2" : "none",
20004
20030
  egressGuaranteedMbps: bootstrapNumber(`Guest ${index} guaranteed egress Mbps`),
20005
20031
  egressBurstMbps: bootstrapNumber(`Guest ${index} burst egress Mbps`),
20006
20032
  confidential: bootstrapBoolean(`Guest ${index} requires confidential SEV-SNP compute?`)
@@ -21014,6 +21040,7 @@ async function cmdDeploy(options, args) {
21014
21040
  options.deployProvisionMemoryGib,
21015
21041
  options.deployProvisionDiskGib,
21016
21042
  options.deployProvisionGuaranteedMbps,
21043
+ options.deployProvisionDiskEncryption,
21017
21044
  options.deployProvisionBurstMbps,
21018
21045
  options.deployProvisionMonthlyAmountCents,
21019
21046
  options.deployMaxMonthlySpendMinor
@@ -21029,6 +21056,7 @@ async function cmdDeploy(options, args) {
21029
21056
  vcpu: options.deployProvisionVcpu ?? 0,
21030
21057
  memoryGib: options.deployProvisionMemoryGib ?? 0,
21031
21058
  diskGib: options.deployProvisionDiskGib ?? 0,
21059
+ diskEncryption: options.deployProvisionDiskEncryption ?? "luks2",
21032
21060
  egressGuaranteedMbps: options.deployProvisionGuaranteedMbps ?? 0,
21033
21061
  egressBurstMbps: options.deployProvisionBurstMbps ?? 0,
21034
21062
  confidential: options.deployIsolation === "sev-snp"
@@ -21376,7 +21404,7 @@ function usage() {
21376
21404
  --provision-environment <key> --provision-ownership <platform|tenant-metal>
21377
21405
  --provision-metal <host> Required only for tenant-metal ownership
21378
21406
  --provision-physical-cores <n> --provision-vcpu <n>
21379
- --provision-memory-gib <n> --provision-disk-gib <n>
21407
+ --provision-memory-gib <n> --provision-disk-gib <n> --provision-disk-encryption <none|luks2>
21380
21408
  --provision-guaranteed-mbps <n> --provision-burst-mbps <n>
21381
21409
  --provision-monthly-amount-cents <n> Exact approved USD cost per created compute
21382
21410
  --max-monthly-spend-minor <n> Hard approved scale-out spend ceiling
@@ -366,7 +366,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
366
366
  }
367
367
 
368
368
  // src/version.ts
369
- var VERSION = "0.1.135";
369
+ var VERSION = "0.1.136";
370
370
 
371
371
  // src/otel-collector.ts
372
372
  var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
@@ -1058,6 +1058,7 @@ async function applyMetalBootstrap(config, options) {
1058
1058
  "qemu-utils",
1059
1059
  "cloud-image-utils",
1060
1060
  "lvm2",
1061
+ "cryptsetup",
1061
1062
  "nftables",
1062
1063
  "curl",
1063
1064
  "ca-certificates"
@@ -56,6 +56,10 @@ function guestUnit(spec) {
56
56
  `ExecStopPost=-/usr/sbin/ip link del ${spec.tap}`
57
57
  ].join(`
58
58
  `) : "";
59
+ const encryptedDisk = spec.diskEncryption ? `LoadCredentialEncrypted=${spec.diskEncryption.credentialName}:${spec.diskEncryption.credentialPath}
60
+ ExecStartPre=/usr/sbin/cryptsetup open --type luks2 --key-file %d/${spec.diskEncryption.credentialName} ${spec.diskEncryption.source} ${spec.diskEncryption.mapper}
61
+ ExecStopPost=-/usr/sbin/cryptsetup close ${spec.diskEncryption.mapper}
62
+ ` : "";
59
63
  return `[Unit]
60
64
  Description=ForgeZero guest ${spec.name}
61
65
  Documentation=https://www.forgezero.net
@@ -65,7 +69,7 @@ Wants=network-online.target
65
69
  [Service]
66
70
  Type=simple
67
71
  Slice=forgezero-guests.slice
68
- ${tapSetup}
72
+ ${encryptedDisk}${tapSetup}
69
73
  ExecStart=${argv}
70
74
  Restart=always
71
75
  RestartSec=5
@@ -754,8 +758,9 @@ function assertSupportedGuestImage(imageKey, confidential = false) {
754
758
  }
755
759
 
756
760
  // src/metal-provision.ts
757
- import { createHash as createHash2 } from "node:crypto";
761
+ import { createHash as createHash2, randomBytes } from "node:crypto";
758
762
  import {
763
+ chmodSync as chmodSync2,
759
764
  existsSync as existsSync2,
760
765
  mkdirSync as mkdirSync2,
761
766
  readFileSync as readFileSync2,
@@ -989,6 +994,8 @@ function guestBootstrapOperations(profile, attested = Boolean(profile.confidenti
989
994
  validateMetalProfile(profile);
990
995
  const agentBun = "/usr/local/lib/forgezero/bun";
991
996
  return [
997
+ ["/usr/sbin/swapoff", "--all"],
998
+ ["/usr/bin/systemctl", "mask", "--now", "swap.target"],
992
999
  ...attested ? [
993
1000
  ["/usr/sbin/modprobe", "sev-guest"],
994
1001
  ["/usr/bin/test", "-c", "/dev/sev-guest"]
@@ -1065,6 +1072,7 @@ ${access.sshPublicKeys.map((key) => ` - ${JSON.stringify(key)}`).join(`
1065
1072
  return {
1066
1073
  userData: `#cloud-config
1067
1074
  package_update: true
1075
+ swap: { size: 0 }
1068
1076
  ${users}disable_root: true
1069
1077
  # Git is part of the deployment transport, not a tenant-selected prerequisite:
1070
1078
  # every dynamically claimed repository must be cloneable on a clean image.
@@ -1097,7 +1105,8 @@ var checked = async (exec, argv) => {
1097
1105
  async function provisionMetalGuest(profile, claim, exec) {
1098
1106
  validateMetalProfile(profile);
1099
1107
  assertSupportedGuestImage(claim.spec.imageKey, claim.spec.confidential);
1100
- if (!claim.computeKey || !claim.spec.reference || !SAFE_NAME.test(claim.spec.imageKey) || claim.spec.guestName !== undefined && !SAFE_NAME.test(claim.spec.guestName) || claim.spec.cpuPoolKey !== undefined && !SAFE_NAME.test(claim.spec.cpuPoolKey) || !Number.isInteger(claim.spec.physicalCores) || claim.spec.physicalCores < 1 || claim.spec.physicalCores > 256 || !Number.isInteger(claim.spec.vcpu) || claim.spec.vcpu < 1 || claim.spec.vcpu > 512 || !Number.isInteger(claim.spec.memoryGib) || claim.spec.memoryGib < 1 || claim.spec.memoryGib > 8192 || !Number.isInteger(claim.spec.diskGib) || claim.spec.diskGib < 8 || claim.spec.diskGib > 65536 || !Number.isInteger(claim.spec.egressGuaranteedMbps) || claim.spec.egressGuaranteedMbps < 0 || !Number.isInteger(claim.spec.egressBurstMbps) || claim.spec.egressBurstMbps < claim.spec.egressGuaranteedMbps)
1108
+ const diskEncryption = claim.spec.diskEncryption ?? "none";
1109
+ if (!claim.computeKey || !claim.spec.reference || !SAFE_NAME.test(claim.spec.imageKey) || claim.spec.guestName !== undefined && !SAFE_NAME.test(claim.spec.guestName) || claim.spec.cpuPoolKey !== undefined && !SAFE_NAME.test(claim.spec.cpuPoolKey) || !Number.isInteger(claim.spec.physicalCores) || claim.spec.physicalCores < 1 || claim.spec.physicalCores > 256 || !Number.isInteger(claim.spec.vcpu) || claim.spec.vcpu < 1 || claim.spec.vcpu > 512 || !Number.isInteger(claim.spec.memoryGib) || claim.spec.memoryGib < 1 || claim.spec.memoryGib > 8192 || !Number.isInteger(claim.spec.diskGib) || claim.spec.diskGib < 8 || claim.spec.diskGib > 65536 || !["none", "luks2"].includes(diskEncryption) || !Number.isInteger(claim.spec.egressGuaranteedMbps) || claim.spec.egressGuaranteedMbps < 0 || !Number.isInteger(claim.spec.egressBurstMbps) || claim.spec.egressBurstMbps < claim.spec.egressGuaranteedMbps)
1101
1110
  throw new MetalProvisionError("invalid compute claim");
1102
1111
  const image = profile.images[claim.spec.imageKey];
1103
1112
  if (!image || !isAbsolute(image.path) || !SHA256.test(image.sha256)) {
@@ -1126,6 +1135,9 @@ async function provisionMetalGuest(profile, claim, exec) {
1126
1135
  const prior = manifests.find((row) => row.computeKey === claim.computeKey);
1127
1136
  if (prior && prior.reference !== claim.spec.reference)
1128
1137
  throw new MetalProvisionError("compute identity conflicts with host inventory");
1138
+ if (prior && (prior.diskEncryption ?? "none") !== diskEncryption) {
1139
+ throw new MetalProvisionError("an allocated guest disk encryption mode is immutable");
1140
+ }
1129
1141
  const address = requestedAddress(profile, claim, manifests);
1130
1142
  const cpuPool = allocateCpuPool(profile, claim, manifests);
1131
1143
  const manifest = prior ?? {
@@ -1137,6 +1149,7 @@ async function provisionMetalGuest(profile, claim, exec) {
1137
1149
  cpuPoolKey: cpuPool.key,
1138
1150
  allowedCpus: cpuPool.cpus,
1139
1151
  allowedMemoryNodes: cpuPool.memoryNodes,
1152
+ diskEncryption,
1140
1153
  phase: "allocating"
1141
1154
  };
1142
1155
  const save = () => writeFileSync2(manifestPath, `${JSON.stringify(manifest, null, 2)}
@@ -1146,8 +1159,70 @@ async function provisionMetalGuest(profile, claim, exec) {
1146
1159
  const exists = (await exec(["lvs", "--noheadings", lv])).exitCode === 0;
1147
1160
  if (!exists)
1148
1161
  await checked(exec, ["lvcreate", "-y", "-n", name, "-L", `${claim.spec.diskGib}G`, profile.volumeGroup]);
1162
+ const encrypted = diskEncryption === "luks2";
1163
+ const mapper = `fz-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
1164
+ const mappedDisk = `/dev/mapper/${mapper}`;
1165
+ const credentialName = `guest-disk-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
1166
+ const credentialPath = `/etc/forgezero/creds/${credentialName}.cred`;
1167
+ const keyPath = `/run/forgezero/${credentialName}.key`;
1168
+ let imageTarget = lv;
1169
+ if (encrypted) {
1170
+ mkdirSync2(dirname2(credentialPath), { recursive: true, mode: 448 });
1171
+ mkdirSync2(dirname2(keyPath), { recursive: true, mode: 448 });
1172
+ if (existsSync2(keyPath))
1173
+ unlinkSync2(keyPath);
1174
+ if (!existsSync2(credentialPath)) {
1175
+ if (exists && (await exec(["cryptsetup", "isLuks", lv])).exitCode === 0) {
1176
+ throw new MetalProvisionError("encrypted guest disk exists without its sealed unlock credential");
1177
+ }
1178
+ writeFileSync2(keyPath, randomBytes(64), { mode: 384, flag: "wx" });
1179
+ try {
1180
+ await checked(exec, ["systemd-creds", "encrypt", `--name=${credentialName}`, keyPath, credentialPath]);
1181
+ chmodSync2(credentialPath, 384);
1182
+ } catch (cause) {
1183
+ if (existsSync2(credentialPath))
1184
+ unlinkSync2(credentialPath);
1185
+ throw cause;
1186
+ }
1187
+ } else {
1188
+ await checked(exec, ["systemd-creds", "decrypt", `--name=${credentialName}`, credentialPath, keyPath]);
1189
+ }
1190
+ try {
1191
+ if ((await exec(["cryptsetup", "isLuks", lv])).exitCode !== 0) {
1192
+ await checked(exec, [
1193
+ "cryptsetup",
1194
+ "luksFormat",
1195
+ "--batch-mode",
1196
+ "--type",
1197
+ "luks2",
1198
+ "--cipher",
1199
+ "aes-xts-plain64",
1200
+ "--key-size",
1201
+ "512",
1202
+ "--key-file",
1203
+ keyPath,
1204
+ lv
1205
+ ]);
1206
+ }
1207
+ if ((await exec(["cryptsetup", "status", mapper])).exitCode !== 0) {
1208
+ await checked(exec, ["cryptsetup", "open", "--type", "luks2", "--key-file", keyPath, lv, mapper]);
1209
+ }
1210
+ imageTarget = mappedDisk;
1211
+ if (manifest.phase === "allocating") {
1212
+ await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, imageTarget]);
1213
+ manifest.phase = "image-ready";
1214
+ save();
1215
+ }
1216
+ } finally {
1217
+ if (existsSync2(keyPath))
1218
+ unlinkSync2(keyPath);
1219
+ if ((await exec(["cryptsetup", "status", mapper])).exitCode === 0) {
1220
+ await checked(exec, ["cryptsetup", "close", mapper]);
1221
+ }
1222
+ }
1223
+ }
1149
1224
  if (manifest.phase === "allocating") {
1150
- await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, lv]);
1225
+ await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, imageTarget]);
1151
1226
  manifest.phase = "image-ready";
1152
1227
  save();
1153
1228
  }
@@ -1171,7 +1246,8 @@ async function provisionMetalGuest(profile, claim, exec) {
1171
1246
  memoryGib: claim.spec.memoryGib,
1172
1247
  allowedCpus: manifest.allowedCpus,
1173
1248
  allowedMemoryNodes: manifest.allowedMemoryNodes,
1174
- disk: lv,
1249
+ disk: encrypted ? mappedDisk : lv,
1250
+ ...encrypted ? { diskEncryption: { source: lv, mapper, credentialName, credentialPath } } : {},
1175
1251
  seed,
1176
1252
  bridge: profile.bridge,
1177
1253
  mac: manifest.mac,
@@ -1229,6 +1305,7 @@ function legacyPlatformManifest(profile, claim, name) {
1229
1305
  cpuPoolKey: claim.spec.cpuPoolKey ?? "",
1230
1306
  allowedCpus: values.get("CPUSET") ?? "",
1231
1307
  allowedMemoryNodes: values.get("NUMA"),
1308
+ diskEncryption: "none",
1232
1309
  phase: "running",
1233
1310
  disk,
1234
1311
  unit: `forgezero-guest@${name}.service`
@@ -1243,6 +1320,9 @@ async function removeMetalGuest(profile, claim, exec) {
1243
1320
  const service = `forgezero-guest@${name}.service`;
1244
1321
  const unitPath = join2(profile.unitDir, service);
1245
1322
  const lv = `/dev/${profile.volumeGroup}/${name}`;
1323
+ const mapper = `fz-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
1324
+ const credentialName = `guest-disk-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
1325
+ const credentialPath = `/etc/forgezero/creds/${credentialName}.cred`;
1246
1326
  const manifest = existsSync2(manifestPath) ? JSON.parse(readFileSync2(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
1247
1327
  if (!manifest) {
1248
1328
  const seedBase = join2(profile.seedDir, name);
@@ -1277,9 +1357,14 @@ async function removeMetalGuest(profile, claim, exec) {
1277
1357
  else if ((await exec(["systemctl", "is-active", service])).exitCode === 0) {
1278
1358
  throw new MetalProvisionError("guest unit is active but its owned unit file is missing");
1279
1359
  }
1360
+ const encrypted = (manifest.diskEncryption ?? "none") === "luks2";
1361
+ if (encrypted && (await exec(["cryptsetup", "status", mapper])).exitCode === 0) {
1362
+ throw new MetalProvisionError("guest disk mapping remained active after the guest stopped");
1363
+ }
1280
1364
  if ((await exec(["lvs", "--noheadings", lv])).exitCode === 0)
1281
1365
  await checked(exec, ["lvremove", "-fy", lv]);
1282
1366
  for (const path2 of [
1367
+ ...encrypted ? [credentialPath] : [],
1283
1368
  unitPath,
1284
1369
  join2(profile.seedDir, `${name}-seed.iso`),
1285
1370
  join2(profile.seedDir, `${name}-user-data`),
@@ -1302,7 +1387,7 @@ async function removeMetalGuest(profile, claim, exec) {
1302
1387
  }
1303
1388
 
1304
1389
  // src/metal-helper-socket.ts
1305
- import { chmodSync as chmodSync2, existsSync as existsSync3, unlinkSync as unlinkSync3 } from "node:fs";
1390
+ import { chmodSync as chmodSync3, existsSync as existsSync3, unlinkSync as unlinkSync3 } from "node:fs";
1306
1391
  import { connect, createServer } from "node:net";
1307
1392
  var DEFAULT_METAL_HELPER_SOCKET = "/run/forgezero-metal/helper.sock";
1308
1393
  var MAX_REQUEST_BYTES = 32 * 1024;
@@ -1382,7 +1467,7 @@ function startMetalHelper(options) {
1382
1467
  });
1383
1468
  socket.on("error", () => socket.destroy());
1384
1469
  });
1385
- server.listen(socketPath, () => chmodSync2(socketPath, 432));
1470
+ server.listen(socketPath, () => chmodSync3(socketPath, 432));
1386
1471
  return {
1387
1472
  server,
1388
1473
  async stop() {
@@ -63,6 +63,7 @@ export interface GuestManifest {
63
63
  cpuPoolKey: string;
64
64
  allowedCpus: string;
65
65
  allowedMemoryNodes?: string;
66
+ diskEncryption?: 'none' | 'luks2';
66
67
  phase: 'allocating' | 'image-ready' | 'running';
67
68
  }
68
69
  export declare class MetalProvisionError extends Error {
@@ -56,6 +56,10 @@ function guestUnit(spec) {
56
56
  `ExecStopPost=-/usr/sbin/ip link del ${spec.tap}`
57
57
  ].join(`
58
58
  `) : "";
59
+ const encryptedDisk = spec.diskEncryption ? `LoadCredentialEncrypted=${spec.diskEncryption.credentialName}:${spec.diskEncryption.credentialPath}
60
+ ExecStartPre=/usr/sbin/cryptsetup open --type luks2 --key-file %d/${spec.diskEncryption.credentialName} ${spec.diskEncryption.source} ${spec.diskEncryption.mapper}
61
+ ExecStopPost=-/usr/sbin/cryptsetup close ${spec.diskEncryption.mapper}
62
+ ` : "";
59
63
  return `[Unit]
60
64
  Description=ForgeZero guest ${spec.name}
61
65
  Documentation=https://www.forgezero.net
@@ -65,7 +69,7 @@ Wants=network-online.target
65
69
  [Service]
66
70
  Type=simple
67
71
  Slice=forgezero-guests.slice
68
- ${tapSetup}
72
+ ${encryptedDisk}${tapSetup}
69
73
  ExecStart=${argv}
70
74
  Restart=always
71
75
  RestartSec=5
@@ -754,8 +758,9 @@ function assertSupportedGuestImage(imageKey, confidential = false) {
754
758
  }
755
759
 
756
760
  // src/metal-provision.ts
757
- import { createHash as createHash2 } from "node:crypto";
761
+ import { createHash as createHash2, randomBytes } from "node:crypto";
758
762
  import {
763
+ chmodSync as chmodSync2,
759
764
  existsSync as existsSync2,
760
765
  mkdirSync as mkdirSync2,
761
766
  readFileSync as readFileSync2,
@@ -989,6 +994,8 @@ function guestBootstrapOperations(profile, attested = Boolean(profile.confidenti
989
994
  validateMetalProfile(profile);
990
995
  const agentBun = "/usr/local/lib/forgezero/bun";
991
996
  return [
997
+ ["/usr/sbin/swapoff", "--all"],
998
+ ["/usr/bin/systemctl", "mask", "--now", "swap.target"],
992
999
  ...attested ? [
993
1000
  ["/usr/sbin/modprobe", "sev-guest"],
994
1001
  ["/usr/bin/test", "-c", "/dev/sev-guest"]
@@ -1065,6 +1072,7 @@ ${access.sshPublicKeys.map((key) => ` - ${JSON.stringify(key)}`).join(`
1065
1072
  return {
1066
1073
  userData: `#cloud-config
1067
1074
  package_update: true
1075
+ swap: { size: 0 }
1068
1076
  ${users}disable_root: true
1069
1077
  # Git is part of the deployment transport, not a tenant-selected prerequisite:
1070
1078
  # every dynamically claimed repository must be cloneable on a clean image.
@@ -1097,7 +1105,8 @@ var checked = async (exec, argv) => {
1097
1105
  async function provisionMetalGuest(profile, claim, exec) {
1098
1106
  validateMetalProfile(profile);
1099
1107
  assertSupportedGuestImage(claim.spec.imageKey, claim.spec.confidential);
1100
- if (!claim.computeKey || !claim.spec.reference || !SAFE_NAME.test(claim.spec.imageKey) || claim.spec.guestName !== undefined && !SAFE_NAME.test(claim.spec.guestName) || claim.spec.cpuPoolKey !== undefined && !SAFE_NAME.test(claim.spec.cpuPoolKey) || !Number.isInteger(claim.spec.physicalCores) || claim.spec.physicalCores < 1 || claim.spec.physicalCores > 256 || !Number.isInteger(claim.spec.vcpu) || claim.spec.vcpu < 1 || claim.spec.vcpu > 512 || !Number.isInteger(claim.spec.memoryGib) || claim.spec.memoryGib < 1 || claim.spec.memoryGib > 8192 || !Number.isInteger(claim.spec.diskGib) || claim.spec.diskGib < 8 || claim.spec.diskGib > 65536 || !Number.isInteger(claim.spec.egressGuaranteedMbps) || claim.spec.egressGuaranteedMbps < 0 || !Number.isInteger(claim.spec.egressBurstMbps) || claim.spec.egressBurstMbps < claim.spec.egressGuaranteedMbps)
1108
+ const diskEncryption = claim.spec.diskEncryption ?? "none";
1109
+ if (!claim.computeKey || !claim.spec.reference || !SAFE_NAME.test(claim.spec.imageKey) || claim.spec.guestName !== undefined && !SAFE_NAME.test(claim.spec.guestName) || claim.spec.cpuPoolKey !== undefined && !SAFE_NAME.test(claim.spec.cpuPoolKey) || !Number.isInteger(claim.spec.physicalCores) || claim.spec.physicalCores < 1 || claim.spec.physicalCores > 256 || !Number.isInteger(claim.spec.vcpu) || claim.spec.vcpu < 1 || claim.spec.vcpu > 512 || !Number.isInteger(claim.spec.memoryGib) || claim.spec.memoryGib < 1 || claim.spec.memoryGib > 8192 || !Number.isInteger(claim.spec.diskGib) || claim.spec.diskGib < 8 || claim.spec.diskGib > 65536 || !["none", "luks2"].includes(diskEncryption) || !Number.isInteger(claim.spec.egressGuaranteedMbps) || claim.spec.egressGuaranteedMbps < 0 || !Number.isInteger(claim.spec.egressBurstMbps) || claim.spec.egressBurstMbps < claim.spec.egressGuaranteedMbps)
1101
1110
  throw new MetalProvisionError("invalid compute claim");
1102
1111
  const image = profile.images[claim.spec.imageKey];
1103
1112
  if (!image || !isAbsolute(image.path) || !SHA256.test(image.sha256)) {
@@ -1126,6 +1135,9 @@ async function provisionMetalGuest(profile, claim, exec) {
1126
1135
  const prior = manifests.find((row) => row.computeKey === claim.computeKey);
1127
1136
  if (prior && prior.reference !== claim.spec.reference)
1128
1137
  throw new MetalProvisionError("compute identity conflicts with host inventory");
1138
+ if (prior && (prior.diskEncryption ?? "none") !== diskEncryption) {
1139
+ throw new MetalProvisionError("an allocated guest disk encryption mode is immutable");
1140
+ }
1129
1141
  const address = requestedAddress(profile, claim, manifests);
1130
1142
  const cpuPool = allocateCpuPool(profile, claim, manifests);
1131
1143
  const manifest = prior ?? {
@@ -1137,6 +1149,7 @@ async function provisionMetalGuest(profile, claim, exec) {
1137
1149
  cpuPoolKey: cpuPool.key,
1138
1150
  allowedCpus: cpuPool.cpus,
1139
1151
  allowedMemoryNodes: cpuPool.memoryNodes,
1152
+ diskEncryption,
1140
1153
  phase: "allocating"
1141
1154
  };
1142
1155
  const save = () => writeFileSync2(manifestPath, `${JSON.stringify(manifest, null, 2)}
@@ -1146,8 +1159,70 @@ async function provisionMetalGuest(profile, claim, exec) {
1146
1159
  const exists = (await exec(["lvs", "--noheadings", lv])).exitCode === 0;
1147
1160
  if (!exists)
1148
1161
  await checked(exec, ["lvcreate", "-y", "-n", name, "-L", `${claim.spec.diskGib}G`, profile.volumeGroup]);
1162
+ const encrypted = diskEncryption === "luks2";
1163
+ const mapper = `fz-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
1164
+ const mappedDisk = `/dev/mapper/${mapper}`;
1165
+ const credentialName = `guest-disk-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
1166
+ const credentialPath = `/etc/forgezero/creds/${credentialName}.cred`;
1167
+ const keyPath = `/run/forgezero/${credentialName}.key`;
1168
+ let imageTarget = lv;
1169
+ if (encrypted) {
1170
+ mkdirSync2(dirname2(credentialPath), { recursive: true, mode: 448 });
1171
+ mkdirSync2(dirname2(keyPath), { recursive: true, mode: 448 });
1172
+ if (existsSync2(keyPath))
1173
+ unlinkSync2(keyPath);
1174
+ if (!existsSync2(credentialPath)) {
1175
+ if (exists && (await exec(["cryptsetup", "isLuks", lv])).exitCode === 0) {
1176
+ throw new MetalProvisionError("encrypted guest disk exists without its sealed unlock credential");
1177
+ }
1178
+ writeFileSync2(keyPath, randomBytes(64), { mode: 384, flag: "wx" });
1179
+ try {
1180
+ await checked(exec, ["systemd-creds", "encrypt", `--name=${credentialName}`, keyPath, credentialPath]);
1181
+ chmodSync2(credentialPath, 384);
1182
+ } catch (cause) {
1183
+ if (existsSync2(credentialPath))
1184
+ unlinkSync2(credentialPath);
1185
+ throw cause;
1186
+ }
1187
+ } else {
1188
+ await checked(exec, ["systemd-creds", "decrypt", `--name=${credentialName}`, credentialPath, keyPath]);
1189
+ }
1190
+ try {
1191
+ if ((await exec(["cryptsetup", "isLuks", lv])).exitCode !== 0) {
1192
+ await checked(exec, [
1193
+ "cryptsetup",
1194
+ "luksFormat",
1195
+ "--batch-mode",
1196
+ "--type",
1197
+ "luks2",
1198
+ "--cipher",
1199
+ "aes-xts-plain64",
1200
+ "--key-size",
1201
+ "512",
1202
+ "--key-file",
1203
+ keyPath,
1204
+ lv
1205
+ ]);
1206
+ }
1207
+ if ((await exec(["cryptsetup", "status", mapper])).exitCode !== 0) {
1208
+ await checked(exec, ["cryptsetup", "open", "--type", "luks2", "--key-file", keyPath, lv, mapper]);
1209
+ }
1210
+ imageTarget = mappedDisk;
1211
+ if (manifest.phase === "allocating") {
1212
+ await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, imageTarget]);
1213
+ manifest.phase = "image-ready";
1214
+ save();
1215
+ }
1216
+ } finally {
1217
+ if (existsSync2(keyPath))
1218
+ unlinkSync2(keyPath);
1219
+ if ((await exec(["cryptsetup", "status", mapper])).exitCode === 0) {
1220
+ await checked(exec, ["cryptsetup", "close", mapper]);
1221
+ }
1222
+ }
1223
+ }
1149
1224
  if (manifest.phase === "allocating") {
1150
- await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, lv]);
1225
+ await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, imageTarget]);
1151
1226
  manifest.phase = "image-ready";
1152
1227
  save();
1153
1228
  }
@@ -1171,7 +1246,8 @@ async function provisionMetalGuest(profile, claim, exec) {
1171
1246
  memoryGib: claim.spec.memoryGib,
1172
1247
  allowedCpus: manifest.allowedCpus,
1173
1248
  allowedMemoryNodes: manifest.allowedMemoryNodes,
1174
- disk: lv,
1249
+ disk: encrypted ? mappedDisk : lv,
1250
+ ...encrypted ? { diskEncryption: { source: lv, mapper, credentialName, credentialPath } } : {},
1175
1251
  seed,
1176
1252
  bridge: profile.bridge,
1177
1253
  mac: manifest.mac,
@@ -1229,6 +1305,7 @@ function legacyPlatformManifest(profile, claim, name) {
1229
1305
  cpuPoolKey: claim.spec.cpuPoolKey ?? "",
1230
1306
  allowedCpus: values.get("CPUSET") ?? "",
1231
1307
  allowedMemoryNodes: values.get("NUMA"),
1308
+ diskEncryption: "none",
1232
1309
  phase: "running",
1233
1310
  disk,
1234
1311
  unit: `forgezero-guest@${name}.service`
@@ -1243,6 +1320,9 @@ async function removeMetalGuest(profile, claim, exec) {
1243
1320
  const service = `forgezero-guest@${name}.service`;
1244
1321
  const unitPath = join2(profile.unitDir, service);
1245
1322
  const lv = `/dev/${profile.volumeGroup}/${name}`;
1323
+ const mapper = `fz-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
1324
+ const credentialName = `guest-disk-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
1325
+ const credentialPath = `/etc/forgezero/creds/${credentialName}.cred`;
1246
1326
  const manifest = existsSync2(manifestPath) ? JSON.parse(readFileSync2(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
1247
1327
  if (!manifest) {
1248
1328
  const seedBase = join2(profile.seedDir, name);
@@ -1277,9 +1357,14 @@ async function removeMetalGuest(profile, claim, exec) {
1277
1357
  else if ((await exec(["systemctl", "is-active", service])).exitCode === 0) {
1278
1358
  throw new MetalProvisionError("guest unit is active but its owned unit file is missing");
1279
1359
  }
1360
+ const encrypted = (manifest.diskEncryption ?? "none") === "luks2";
1361
+ if (encrypted && (await exec(["cryptsetup", "status", mapper])).exitCode === 0) {
1362
+ throw new MetalProvisionError("guest disk mapping remained active after the guest stopped");
1363
+ }
1280
1364
  if ((await exec(["lvs", "--noheadings", lv])).exitCode === 0)
1281
1365
  await checked(exec, ["lvremove", "-fy", lv]);
1282
1366
  for (const path2 of [
1367
+ ...encrypted ? [credentialPath] : [],
1283
1368
  unitPath,
1284
1369
  join2(profile.seedDir, `${name}-seed.iso`),
1285
1370
  join2(profile.seedDir, `${name}-user-data`),