@forgezero/agent 0.1.22 → 0.1.24
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/README.md +24 -0
- package/dist/cli/agent-install.d.ts +10 -0
- package/dist/fz-agent.js +494 -218
- package/dist/fz.js +221 -18
- package/dist/guest-enrolment.d.ts +9 -0
- package/dist/guest-enrolment.js +22 -0
- package/dist/index.d.ts +5 -0
- package/dist/lifecycle-helper.d.ts +36 -0
- package/dist/lifecycle-helper.js +216 -0
- package/dist/metal-helper-socket.js +8 -532
- package/dist/metal-provision.d.ts +1 -6
- package/dist/metal-provision.js +8 -536
- package/dist/migration-pull.d.ts +55 -0
- package/dist/migration-pull.js +185 -0
- package/dist/provision.d.ts +22 -0
- package/dist/provision.js +215 -16
- package/dist/version.d.ts +1 -1
- package/dist/warp-config.d.ts +20 -0
- package/package.json +10 -2
package/dist/fz.js
CHANGED
|
@@ -101,7 +101,8 @@ async function spawnWith(command, env, report = () => {}) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// src/cli/index.ts
|
|
104
|
-
import { existsSync, readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
104
|
+
import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
105
|
+
import { dirname } from "path";
|
|
105
106
|
import { fileURLToPath } from "url";
|
|
106
107
|
import { DEFAULT_SOCKET } from "@forgezero/vault";
|
|
107
108
|
|
|
@@ -149,9 +150,104 @@ var reasonFor = (mode) => mode === "attested" ? "SEV-SNP guest device present, s
|
|
|
149
150
|
var DEPLOYMENT_RUNNER_USER = "forgezero-runner";
|
|
150
151
|
var DEPLOYMENT_GROUP = "forgezero-deploy";
|
|
151
152
|
var VAULT_GROUP = "forgezero-vault";
|
|
153
|
+
var LIFECYCLE_GROUP = "forgezero-lifecycle";
|
|
152
154
|
var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
|
|
153
155
|
var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
|
|
154
156
|
var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
|
|
157
|
+
var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
|
|
158
|
+
var LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
|
|
159
|
+
var WARP_CONFIG_UNIT_PATH = "/etc/systemd/system/forgezero-warp-config.service";
|
|
160
|
+
var WARP_SERVICE_DROP_IN_PATH = "/etc/systemd/system/warp-svc.service.d/forgezero.conf";
|
|
161
|
+
var systemdPath = (value, label) => {
|
|
162
|
+
if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
|
|
163
|
+
throw new Error(`invalid ${label} path`);
|
|
164
|
+
return value;
|
|
165
|
+
};
|
|
166
|
+
function warpConfigUnit(options) {
|
|
167
|
+
if (!options.warpOrganization || !/^[a-z0-9][a-z0-9-]{0,62}$/i.test(options.warpOrganization)) {
|
|
168
|
+
throw new Error("WARP organization is invalid");
|
|
169
|
+
}
|
|
170
|
+
if (!options.warpClientIdCredentialPath || !options.warpClientSecretCredentialPath) {
|
|
171
|
+
throw new Error("WARP service-token credential paths are required");
|
|
172
|
+
}
|
|
173
|
+
const clientIdPath = systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential");
|
|
174
|
+
const clientSecretPath = systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential");
|
|
175
|
+
const bin = options.binPath ?? "fz-agent";
|
|
176
|
+
return `[Unit]
|
|
177
|
+
Description=Materialize Cloudflare One enrollment in tmpfs
|
|
178
|
+
Documentation=https://www.forgezero.net/docs/agent
|
|
179
|
+
Before=warp-svc.service
|
|
180
|
+
|
|
181
|
+
[Service]
|
|
182
|
+
Type=oneshot
|
|
183
|
+
RemainAfterExit=yes
|
|
184
|
+
LoadCredentialEncrypted=warp-auth-client-id:${clientIdPath}
|
|
185
|
+
LoadCredentialEncrypted=warp-auth-client-secret:${clientSecretPath}
|
|
186
|
+
Environment=FZ_WARP_CLIENT_ID_CREDENTIAL=warp-auth-client-id
|
|
187
|
+
Environment=FZ_WARP_CLIENT_SECRET_CREDENTIAL=warp-auth-client-secret
|
|
188
|
+
ExecStart=${bin} warp-config --organization=${options.warpOrganization}
|
|
189
|
+
RuntimeDirectory=forgezero-warp
|
|
190
|
+
RuntimeDirectoryMode=0700
|
|
191
|
+
RuntimeDirectoryPreserve=yes
|
|
192
|
+
UMask=0077
|
|
193
|
+
LimitCORE=0
|
|
194
|
+
NoNewPrivileges=true
|
|
195
|
+
PrivateTmp=true
|
|
196
|
+
ProtectSystem=strict
|
|
197
|
+
ProtectHome=true
|
|
198
|
+
ReadWritePaths=/var/lib/cloudflare-warp
|
|
199
|
+
|
|
200
|
+
[Install]
|
|
201
|
+
WantedBy=multi-user.target
|
|
202
|
+
`;
|
|
203
|
+
}
|
|
204
|
+
function warpServiceDropIn() {
|
|
205
|
+
return `[Unit]
|
|
206
|
+
Requires=forgezero-warp-config.service
|
|
207
|
+
After=forgezero-warp-config.service
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
function lifecycleHelperUnit(options) {
|
|
211
|
+
if (!options.lifecycleProfilePath)
|
|
212
|
+
throw new Error("lifecycle helper needs a root-owned profile");
|
|
213
|
+
const bin = options.binPath ?? "fz-agent";
|
|
214
|
+
const profile = systemdPath(options.lifecycleProfilePath, "lifecycle profile");
|
|
215
|
+
const socket = systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket");
|
|
216
|
+
return `[Unit]
|
|
217
|
+
Description=ForgeZero fixed-operation compute lifecycle helper
|
|
218
|
+
Documentation=https://www.forgezero.net/docs/agent
|
|
219
|
+
After=network-online.target
|
|
220
|
+
Wants=network-online.target
|
|
221
|
+
|
|
222
|
+
[Service]
|
|
223
|
+
Type=simple
|
|
224
|
+
User=root
|
|
225
|
+
Group=${LIFECYCLE_GROUP}
|
|
226
|
+
Environment=FZ_LIFECYCLE_HELPER_SOCKET=${socket}
|
|
227
|
+
ExecStart=${bin} lifecycle-helper --profile=${profile}
|
|
228
|
+
Restart=always
|
|
229
|
+
RestartSec=2
|
|
230
|
+
RuntimeDirectory=forgezero-lifecycle
|
|
231
|
+
RuntimeDirectoryMode=0750
|
|
232
|
+
UMask=0007
|
|
233
|
+
LimitCORE=0
|
|
234
|
+
NoNewPrivileges=true
|
|
235
|
+
PrivateTmp=true
|
|
236
|
+
ProtectSystem=strict
|
|
237
|
+
ProtectHome=true
|
|
238
|
+
ProtectKernelTunables=true
|
|
239
|
+
ProtectKernelModules=true
|
|
240
|
+
ProtectControlGroups=true
|
|
241
|
+
RestrictSUIDSGID=true
|
|
242
|
+
RestrictRealtime=true
|
|
243
|
+
MemoryDenyWriteExecute=true
|
|
244
|
+
LockPersonality=true
|
|
245
|
+
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
|
246
|
+
|
|
247
|
+
[Install]
|
|
248
|
+
WantedBy=multi-user.target
|
|
249
|
+
`;
|
|
250
|
+
}
|
|
155
251
|
function agentEnrolmentUnit(options) {
|
|
156
252
|
if (!options.apiUrl || !options.enrolTokenCredentialPath || !options.enrolStatePath) {
|
|
157
253
|
throw new Error("direct enrolment needs API, credential and state paths");
|
|
@@ -163,6 +259,16 @@ function agentEnrolmentUnit(options) {
|
|
|
163
259
|
` : "";
|
|
164
260
|
const gitPublicKey = options.gitPublicKeyPath ? `Environment=FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}
|
|
165
261
|
` : "";
|
|
262
|
+
const networkAttachment = [
|
|
263
|
+
options.cloudflareAccountId ? `Environment=FZ_CF_ACCOUNT_ID=${options.cloudflareAccountId}
|
|
264
|
+
` : "",
|
|
265
|
+
options.cloudflareTunnelId ? `Environment=FZ_CF_TUNNEL_ID=${options.cloudflareTunnelId}
|
|
266
|
+
` : "",
|
|
267
|
+
options.cloudflareVirtualNetworkId ? `Environment=FZ_CF_VIRTUAL_NETWORK_ID=${options.cloudflareVirtualNetworkId}
|
|
268
|
+
` : "",
|
|
269
|
+
options.cloudflareWarpPolicyId ? `Environment=FZ_CF_WARP_POLICY_ID=${options.cloudflareWarpPolicyId}
|
|
270
|
+
` : ""
|
|
271
|
+
].join("");
|
|
166
272
|
const stateDir = options.enrolStatePath.replace(/\/[^/]+$/, "");
|
|
167
273
|
return `[Unit]
|
|
168
274
|
Description=Bind this machine to its ForgeZero compute
|
|
@@ -181,7 +287,7 @@ Environment=FZ_SEED_CREDENTIAL=agent-seed
|
|
|
181
287
|
Environment=FZ_ENROL_TOKEN_CREDENTIAL=enrol-token
|
|
182
288
|
Environment=FZ_ENROL_STATE_FILE=${options.enrolStatePath}
|
|
183
289
|
Environment=FZ_API=${options.apiUrl}
|
|
184
|
-
${label}${gitPublicKey}ExecStart=${bin} enrol
|
|
290
|
+
${label}${gitPublicKey}${networkAttachment}ExecStart=${bin} enrol
|
|
185
291
|
# A '+' fixed command runs as root solely to remove the host-bound one-time
|
|
186
292
|
# ciphertext. Tenant code and the agent never receive a privilege boundary.
|
|
187
293
|
ExecStartPost=+/usr/bin/rm -f ${options.enrolTokenCredentialPath}
|
|
@@ -246,6 +352,34 @@ function agentUnit(options) {
|
|
|
246
352
|
const controlSocketPath = options.controlSocketPath ?? "/run/forgezero/control.sock";
|
|
247
353
|
const deployRoot = options.deployRoot ?? "/opt/forgezero";
|
|
248
354
|
const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
|
|
355
|
+
const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
|
|
356
|
+
if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
|
|
357
|
+
throw new Error("migration pull and lifecycle profile must be supplied together");
|
|
358
|
+
}
|
|
359
|
+
const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
|
|
360
|
+
const warpValues = [
|
|
361
|
+
options.warpOrganization,
|
|
362
|
+
options.warpClientIdCredentialPath,
|
|
363
|
+
options.warpClientSecretCredentialPath
|
|
364
|
+
];
|
|
365
|
+
const warpEnabled = warpValues.every(Boolean);
|
|
366
|
+
if (warpValues.some(Boolean) && !warpEnabled)
|
|
367
|
+
throw new Error("WARP configuration must be supplied together");
|
|
368
|
+
const networkAttachmentValues = [
|
|
369
|
+
options.cloudflareAccountId,
|
|
370
|
+
options.cloudflareTunnelId,
|
|
371
|
+
options.cloudflareVirtualNetworkId,
|
|
372
|
+
options.cloudflareWarpPolicyId
|
|
373
|
+
];
|
|
374
|
+
if (networkAttachmentValues.some(Boolean)) {
|
|
375
|
+
if (!options.cloudflareAccountId || !options.cloudflareTunnelId || !options.cloudflareWarpPolicyId) {
|
|
376
|
+
throw new Error("private-network attachment requires account, Tunnel and WARP policy ids");
|
|
377
|
+
}
|
|
378
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
379
|
+
if (!/^[a-f0-9]{32}$/i.test(options.cloudflareAccountId) || !uuid.test(options.cloudflareTunnelId) || options.cloudflareVirtualNetworkId && !uuid.test(options.cloudflareVirtualNetworkId) || !/^[A-Za-z0-9-]{1,64}$/.test(options.cloudflareWarpPolicyId)) {
|
|
380
|
+
throw new Error("private-network attachment coordinates are invalid");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
249
383
|
const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath && options.enrolStatePath);
|
|
250
384
|
const deploymentEnvironment = options.deploymentEnvironment ?? {};
|
|
251
385
|
const deploymentCredentials = options.deploymentCredentials ?? {};
|
|
@@ -280,7 +414,9 @@ function agentUnit(options) {
|
|
|
280
414
|
Object.keys(deploymentEnvironment).length > 0 ? `FZ_DEPLOY_ENV_NAMES=${Object.keys(deploymentEnvironment).join(",")}` : null,
|
|
281
415
|
...Object.entries(deploymentEnvironment).map(([name, value]) => `${name}=${value}`),
|
|
282
416
|
options.publicApiUrl ? `FZ_PUBLIC_API_URL=${options.publicApiUrl}` : null,
|
|
283
|
-
options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null
|
|
417
|
+
options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null,
|
|
418
|
+
options.pullMigrations ? "FZ_MIGRATION_PULL=true" : null,
|
|
419
|
+
options.pullMigrations ? `FZ_LIFECYCLE_HELPER_SOCKET=${lifecycleHelperSocketPath}` : null
|
|
284
420
|
].filter((line) => line !== null);
|
|
285
421
|
if (deploymentEnabled) {
|
|
286
422
|
environment.push(`HOME=${deployRoot}/agent-home`, `XDG_CACHE_HOME=${deployRoot}/cache`);
|
|
@@ -290,14 +426,22 @@ function agentUnit(options) {
|
|
|
290
426
|
const projectCredentials = Object.entries(deploymentCredentials).map(([name, path]) => `LoadCredentialEncrypted=${name}:${path}`).join(`
|
|
291
427
|
`);
|
|
292
428
|
const deploymentWrites = deploymentEnabled ? `ReadWritePaths=${deployRoot}/releases ${deployRoot}/agent-home ${deployRoot}/cache` : "";
|
|
293
|
-
const
|
|
429
|
+
const supplementaryGroups = [
|
|
430
|
+
deploymentEnabled ? DEPLOYMENT_GROUP : null,
|
|
431
|
+
lifecycleEnabled ? LIFECYCLE_GROUP : null
|
|
432
|
+
].filter((value) => value !== null);
|
|
433
|
+
const deploymentGroup = supplementaryGroups.length > 0 ? `SupplementaryGroups=${supplementaryGroups.join(" ")}` : "";
|
|
294
434
|
const after = [
|
|
295
435
|
"network-online.target",
|
|
296
436
|
deploymentEnabled ? "forgezero-deploy-runner.service" : null,
|
|
437
|
+
lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
|
|
438
|
+
warpEnabled ? "warp-svc.service" : null,
|
|
297
439
|
enrolmentEnabled ? "forgezero-agent-enrol.service" : null
|
|
298
440
|
].filter((value) => value !== null);
|
|
299
441
|
const requires = [
|
|
300
442
|
deploymentEnabled ? "forgezero-deploy-runner.service" : null,
|
|
443
|
+
lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
|
|
444
|
+
warpEnabled ? "warp-svc.service" : null,
|
|
301
445
|
enrolmentEnabled ? "forgezero-agent-enrol.service" : null
|
|
302
446
|
].filter((value) => value !== null);
|
|
303
447
|
const deploymentDependency = [
|
|
@@ -369,37 +513,63 @@ function planProvision(options) {
|
|
|
369
513
|
const credentialDir = seedCredentialPath.replace(/\/[^/]+$/, "");
|
|
370
514
|
const deployRoot = options.deployRoot ?? "/opt/forgezero";
|
|
371
515
|
const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
|
|
516
|
+
const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
|
|
517
|
+
if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
|
|
518
|
+
throw new Error("migration pull and lifecycle profile must be supplied together");
|
|
519
|
+
}
|
|
520
|
+
const warpValues = [
|
|
521
|
+
options.warpOrganization,
|
|
522
|
+
options.warpClientIdCredentialPath,
|
|
523
|
+
options.warpClientSecretCredentialPath
|
|
524
|
+
];
|
|
525
|
+
const warpEnabled = warpValues.every(Boolean);
|
|
526
|
+
if (warpValues.some(Boolean) && !warpEnabled)
|
|
527
|
+
throw new Error("WARP configuration must be supplied together");
|
|
372
528
|
const enrolmentEnabled = Boolean(options.enrolTokenSourcePath && options.enrolTokenCredentialPath && options.enrolStatePath);
|
|
373
529
|
if (Boolean(options.enrolTokenSourcePath) !== Boolean(options.enrolTokenCredentialPath) || Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath))
|
|
374
530
|
throw new Error("direct enrolment paths must be supplied together");
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
return value;
|
|
379
|
-
};
|
|
380
|
-
const enrolTokenSourcePath = enrolmentEnabled ? safePath(options.enrolTokenSourcePath, "enrolment source") : undefined;
|
|
381
|
-
const enrolTokenCredentialPath = enrolmentEnabled ? safePath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
|
|
382
|
-
const enrolStatePath = enrolmentEnabled ? safePath(options.enrolStatePath, "enrolment state") : undefined;
|
|
531
|
+
const enrolTokenSourcePath = enrolmentEnabled ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
|
|
532
|
+
const enrolTokenCredentialPath = enrolmentEnabled ? systemdPath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
|
|
533
|
+
const enrolStatePath = enrolmentEnabled ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
|
|
383
534
|
const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
|
|
384
|
-
const sourceBinPath = options.sourceBinPath ?
|
|
385
|
-
const binPath = options.binPath ?
|
|
386
|
-
const gitCredentialPath = options.gitCredentialPath ?
|
|
387
|
-
const gitPublicKeyPath = options.gitPublicKeyPath ?
|
|
535
|
+
const sourceBinPath = options.sourceBinPath ? systemdPath(options.sourceBinPath, "agent source binary") : undefined;
|
|
536
|
+
const binPath = options.binPath ? systemdPath(options.binPath, "agent binary") : undefined;
|
|
537
|
+
const gitCredentialPath = options.gitCredentialPath ? systemdPath(options.gitCredentialPath, "Git credential") : undefined;
|
|
538
|
+
const gitPublicKeyPath = options.gitPublicKeyPath ? systemdPath(options.gitPublicKeyPath, "Git public key") : undefined;
|
|
388
539
|
if (options.generateGitIdentity && (!gitCredentialPath || !gitPublicKeyPath)) {
|
|
389
540
|
throw new Error("generated Git identity needs credential and public-key paths");
|
|
390
541
|
}
|
|
391
542
|
const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
|
|
543
|
+
const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
|
|
544
|
+
const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
|
|
545
|
+
const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
|
|
546
|
+
const warpClientSecretCredentialPath = warpEnabled ? systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential") : undefined;
|
|
392
547
|
return {
|
|
393
548
|
mode,
|
|
394
549
|
reason: reasonFor(mode),
|
|
395
550
|
unitPath: UNIT_PATH,
|
|
396
|
-
unit: agentUnit({ ...options, mode }),
|
|
551
|
+
unit: agentUnit({ ...options, mode, lifecycleProfilePath, lifecycleHelperSocketPath }),
|
|
397
552
|
auxiliaryUnits: [
|
|
398
553
|
...deploymentEnabled ? [
|
|
399
554
|
{ path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) }
|
|
400
555
|
] : [],
|
|
401
556
|
...enrolmentEnabled ? [
|
|
402
557
|
{ path: ENROLMENT_UNIT_PATH, unit: agentEnrolmentUnit(options) }
|
|
558
|
+
] : [],
|
|
559
|
+
...lifecycleEnabled ? [
|
|
560
|
+
{ path: LIFECYCLE_HELPER_UNIT_PATH, unit: lifecycleHelperUnit({
|
|
561
|
+
...options,
|
|
562
|
+
lifecycleProfilePath,
|
|
563
|
+
lifecycleHelperSocketPath
|
|
564
|
+
}) }
|
|
565
|
+
] : [],
|
|
566
|
+
...warpEnabled ? [
|
|
567
|
+
{ path: WARP_CONFIG_UNIT_PATH, unit: warpConfigUnit({
|
|
568
|
+
...options,
|
|
569
|
+
warpClientIdCredentialPath,
|
|
570
|
+
warpClientSecretCredentialPath
|
|
571
|
+
}) },
|
|
572
|
+
{ path: WARP_SERVICE_DROP_IN_PATH, unit: warpServiceDropIn() }
|
|
403
573
|
] : []
|
|
404
574
|
],
|
|
405
575
|
socketPath: options.socketPath,
|
|
@@ -413,10 +583,18 @@ function planProvision(options) {
|
|
|
413
583
|
label: "root-owned agent runtime",
|
|
414
584
|
command: `install -d -o root -g root -m 0755 ${binPath.replace(/\/[^/]+$/, "")}; ` + `install -o root -g root -m 0755 ${sourceBinPath} ${binPath}`
|
|
415
585
|
}] : [],
|
|
586
|
+
...warpEnabled ? [{
|
|
587
|
+
label: "Cloudflare One client for Ubuntu 26.04",
|
|
588
|
+
command: `. /etc/os-release; test "$ID" = ubuntu && test "$VERSION_ID" = 26.04; ` + `install -d -m 0755 /usr/share/keyrings /etc/apt/sources.list.d /etc/systemd/system/warp-svc.service.d; ` + `curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg -o /run/cloudflare-warp-key.gpg; ` + `gpg --batch --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg /run/cloudflare-warp-key.gpg; ` + `rm -f /run/cloudflare-warp-key.gpg; ` + `printf 'deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ %s main\\n' "$VERSION_CODENAME" > /etc/apt/sources.list.d/cloudflare-client.list; ` + `apt-get update -qq; DEBIAN_FRONTEND=noninteractive apt-get install -y cloudflare-warp`
|
|
589
|
+
}] : [],
|
|
416
590
|
...deploymentEnabled ? [{
|
|
417
591
|
label: "deployment isolation group",
|
|
418
592
|
command: `groupadd --system ${DEPLOYMENT_GROUP} || true`
|
|
419
593
|
}] : [],
|
|
594
|
+
...lifecycleEnabled ? [{
|
|
595
|
+
label: "lifecycle helper access group",
|
|
596
|
+
command: `groupadd --system ${LIFECYCLE_GROUP} || true`
|
|
597
|
+
}] : [],
|
|
420
598
|
{
|
|
421
599
|
label: "service account",
|
|
422
600
|
command: `useradd --system --no-create-home --shell /usr/sbin/nologin ${user} || true`
|
|
@@ -425,6 +603,10 @@ function planProvision(options) {
|
|
|
425
603
|
label: "bind service account to vault group",
|
|
426
604
|
command: `usermod -g ${VAULT_GROUP} ${user}`
|
|
427
605
|
},
|
|
606
|
+
...lifecycleEnabled ? [{
|
|
607
|
+
label: "grant lifecycle helper socket access",
|
|
608
|
+
command: `usermod -a -G ${LIFECYCLE_GROUP} ${user}`
|
|
609
|
+
}] : [],
|
|
428
610
|
...deploymentEnabled ? [{
|
|
429
611
|
label: "credential-free deployment account",
|
|
430
612
|
command: `useradd --system --no-create-home --shell /usr/sbin/nologin --gid ${DEPLOYMENT_GROUP} ${DEPLOYMENT_RUNNER_USER} || true; ` + `usermod -a -G ${DEPLOYMENT_GROUP} ${user}`
|
|
@@ -470,6 +652,8 @@ function planProvision(options) {
|
|
|
470
652
|
label: "enable and start",
|
|
471
653
|
command: `systemctl enable --now ${[
|
|
472
654
|
...deploymentEnabled ? ["forgezero-deploy-runner.service"] : [],
|
|
655
|
+
...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
|
|
656
|
+
...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
|
|
473
657
|
...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
|
|
474
658
|
"forgezero-agent.service"
|
|
475
659
|
].join(" ")}`
|
|
@@ -484,6 +668,14 @@ function planProvision(options) {
|
|
|
484
668
|
label: "prove the deployment runner socket exists",
|
|
485
669
|
command: `test -S ${DEPLOYMENT_RUNNER_SOCKET}`
|
|
486
670
|
}] : [],
|
|
671
|
+
...lifecycleEnabled ? [{
|
|
672
|
+
label: "prove the lifecycle helper socket exists",
|
|
673
|
+
command: `test -S ${lifecycleHelperSocketPath}`
|
|
674
|
+
}] : [],
|
|
675
|
+
...warpEnabled ? [{
|
|
676
|
+
label: "prove Cloudflare WARP is connected",
|
|
677
|
+
command: `warp-cli --accept-tos status | grep -Eiq '(^|[[:space:]])Connected([[:space:]]|$)'`
|
|
678
|
+
}] : [],
|
|
487
679
|
...options.repository ? [{
|
|
488
680
|
label: "prove the deployment control socket exists",
|
|
489
681
|
command: `test -S ${options.controlSocketPath ?? "/run/forgezero/control.sock"}`
|
|
@@ -774,7 +966,7 @@ async function resolveIdentity(selector, socketPath) {
|
|
|
774
966
|
}
|
|
775
967
|
|
|
776
968
|
// src/version.ts
|
|
777
|
-
var VERSION = "0.1.
|
|
969
|
+
var VERSION = "0.1.24";
|
|
778
970
|
|
|
779
971
|
// src/cli/index.ts
|
|
780
972
|
var DEFAULT_MODE = THRESHOLD_MODES[0].id;
|
|
@@ -997,6 +1189,16 @@ async function cmdAgent(options, args) {
|
|
|
997
1189
|
deploymentEnvironment: parseAssignments(process.env.FZ_DEPLOY_ENV),
|
|
998
1190
|
deploymentCredentials: parseAssignments(process.env.FZ_DEPLOY_CREDENTIALS),
|
|
999
1191
|
pullDeployments: process.env.FZ_DEPLOY_PULL === "true",
|
|
1192
|
+
pullMigrations: process.env.FZ_MIGRATION_PULL === "true",
|
|
1193
|
+
lifecycleProfilePath: process.env.FZ_LIFECYCLE_PROFILE,
|
|
1194
|
+
lifecycleHelperSocketPath: process.env.FZ_LIFECYCLE_HELPER_SOCKET,
|
|
1195
|
+
warpOrganization: process.env.FZ_WARP_ORGANIZATION,
|
|
1196
|
+
warpClientIdCredentialPath: process.env.FZ_WARP_CLIENT_ID_CREDENTIAL_PATH,
|
|
1197
|
+
warpClientSecretCredentialPath: process.env.FZ_WARP_CLIENT_SECRET_CREDENTIAL_PATH,
|
|
1198
|
+
cloudflareAccountId: process.env.FZ_CF_ACCOUNT_ID,
|
|
1199
|
+
cloudflareTunnelId: process.env.FZ_CF_TUNNEL_ID,
|
|
1200
|
+
cloudflareVirtualNetworkId: process.env.FZ_CF_VIRTUAL_NETWORK_ID,
|
|
1201
|
+
cloudflareWarpPolicyId: process.env.FZ_CF_WARP_POLICY_ID,
|
|
1000
1202
|
...options.enrol ? {
|
|
1001
1203
|
pullDeployments: true,
|
|
1002
1204
|
enrolTokenSourcePath,
|
|
@@ -1033,6 +1235,7 @@ async function cmdAgent(options, args) {
|
|
|
1033
1235
|
writeFileSync(plan.unitPath, plan.unit, { mode: 420 });
|
|
1034
1236
|
out.ok(`Wrote ${plan.unitPath}`);
|
|
1035
1237
|
for (const auxiliary of plan.auxiliaryUnits) {
|
|
1238
|
+
mkdirSync(dirname(auxiliary.path), { recursive: true, mode: 493 });
|
|
1036
1239
|
writeFileSync(auxiliary.path, auxiliary.unit, { mode: 420 });
|
|
1037
1240
|
out.ok(`Wrote ${auxiliary.path}`);
|
|
1038
1241
|
}
|
|
@@ -7,6 +7,14 @@ export interface GuestBinding {
|
|
|
7
7
|
realm: 'platform' | 'tenant';
|
|
8
8
|
tenantSlug?: string;
|
|
9
9
|
}
|
|
10
|
+
export interface GuestPrivateNetworkAttachment {
|
|
11
|
+
accountId: string;
|
|
12
|
+
tunnelId: string;
|
|
13
|
+
virtualNetworkId?: string;
|
|
14
|
+
warpPolicyId: string;
|
|
15
|
+
}
|
|
16
|
+
/** Fail closed when setup supplied only part of a cross-network attachment. */
|
|
17
|
+
export declare function privateNetworkAttachmentFromEnvironment(env?: Record<string, string | undefined>): GuestPrivateNetworkAttachment | undefined;
|
|
10
18
|
export interface GuestEnrolmentOptions {
|
|
11
19
|
apiUrl: string;
|
|
12
20
|
/** Already-unsealed systemd credential. Production uses this path. */
|
|
@@ -21,6 +29,7 @@ export interface GuestEnrolmentOptions {
|
|
|
21
29
|
keys: NodeKeyPair;
|
|
22
30
|
label?: string;
|
|
23
31
|
gitDeployPublicKey?: string;
|
|
32
|
+
privateNetworkAttachment?: GuestPrivateNetworkAttachment;
|
|
24
33
|
fetch?: (input: URL, init: RequestInit) => Promise<Response>;
|
|
25
34
|
requestTimeoutMs?: number;
|
|
26
35
|
}
|
package/dist/guest-enrolment.js
CHANGED
|
@@ -68,6 +68,26 @@ async function postSignedNode(options, path, body) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// src/guest-enrolment.ts
|
|
71
|
+
function privateNetworkAttachmentFromEnvironment(env = process.env) {
|
|
72
|
+
const values = {
|
|
73
|
+
accountId: env.FZ_CF_ACCOUNT_ID?.trim() ?? "",
|
|
74
|
+
tunnelId: env.FZ_CF_TUNNEL_ID?.trim() ?? "",
|
|
75
|
+
virtualNetworkId: env.FZ_CF_VIRTUAL_NETWORK_ID?.trim() ?? "",
|
|
76
|
+
warpPolicyId: env.FZ_CF_WARP_POLICY_ID?.trim() ?? ""
|
|
77
|
+
};
|
|
78
|
+
const supplied = [values.accountId, values.tunnelId, values.virtualNetworkId, values.warpPolicyId].some(Boolean);
|
|
79
|
+
if (!supplied)
|
|
80
|
+
return;
|
|
81
|
+
if (!values.accountId || !values.tunnelId || !values.warpPolicyId) {
|
|
82
|
+
throw new Error("private-network attachment requires account, Tunnel and WARP policy ids");
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
accountId: values.accountId,
|
|
86
|
+
tunnelId: values.tunnelId,
|
|
87
|
+
...values.virtualNetworkId ? { virtualNetworkId: values.virtualNetworkId } : {},
|
|
88
|
+
warpPolicyId: values.warpPolicyId
|
|
89
|
+
};
|
|
90
|
+
}
|
|
71
91
|
var validBinding = (value, expectedNodeKey) => {
|
|
72
92
|
if (!value || typeof value !== "object")
|
|
73
93
|
return false;
|
|
@@ -114,6 +134,7 @@ async function enrolGuestIdentity(options) {
|
|
|
114
134
|
token,
|
|
115
135
|
label: options.label,
|
|
116
136
|
gitDeployPublicKey: options.gitDeployPublicKey,
|
|
137
|
+
privateNetworkAttachment: options.privateNetworkAttachment,
|
|
117
138
|
publicKeys: {
|
|
118
139
|
ed25519: options.keys.ed25519.publicKey,
|
|
119
140
|
mlDsa: options.keys.mlDsa.publicKey
|
|
@@ -138,6 +159,7 @@ async function enrolGuestIdentity(options) {
|
|
|
138
159
|
return binding;
|
|
139
160
|
}
|
|
140
161
|
export {
|
|
162
|
+
privateNetworkAttachmentFromEnvironment,
|
|
141
163
|
loadGuestBinding,
|
|
142
164
|
enrolGuestIdentity
|
|
143
165
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export { pullDeploymentOnce, startDeploymentPull } from './deployment-pull';
|
|
|
14
14
|
export type { DeploymentPullOptions, RemoteDeploymentClaim, PullResult } from './deployment-pull';
|
|
15
15
|
export { pullProvisioningOnce, startProvisioningPull } from './provisioning-pull';
|
|
16
16
|
export type { CreateRemoteProvisionClaim, ProvisioningPullOptions, RemoteProvisionClaim, GuestAccess, ProvisionPullResult, ProvisionRunner, ProvisionResult } from './provisioning-pull';
|
|
17
|
+
export { pullMigrationOnce, startMigrationPull } from './migration-pull';
|
|
18
|
+
export type { MigrationAction, MigrationEvidence, MigrationNetwork, MigrationPullOptions, MigrationPullResult, RemoteMigrationClaim } from './migration-pull';
|
|
17
19
|
export { enrolGuestIdentity, loadGuestBinding } from './guest-enrolment';
|
|
18
20
|
export type { GuestBinding, GuestEnrolmentOptions } from './guest-enrolment';
|
|
19
21
|
export { createNodeVaultCache, projectVaultCacheKey, projectVaultCoordinate, startNodeVaultSync, tenantNodeApiUrl } from './node-vault';
|
|
@@ -22,6 +24,9 @@ export { allocateAddress, allocateCpuPool, cloudInit, guestNameFor, provisionMet
|
|
|
22
24
|
export { validateMetalProfile } from './metal-provision';
|
|
23
25
|
export type { GuestManifest, MetalCommandResult, MetalCpuPool, MetalExec, MetalProvisionProfile } from './metal-provision';
|
|
24
26
|
export { DEFAULT_METAL_HELPER_SOCKET, requestMetalProvision, startMetalHelper } from './metal-helper-socket';
|
|
27
|
+
export { DEFAULT_LIFECYCLE_HELPER_SOCKET, executeLifecycleAction, loadLifecycleProfile, requestLifecycleAction, startLifecycleHelper, validateLifecycleProfile } from './lifecycle-helper';
|
|
28
|
+
export type { LifecycleCommandResult, LifecycleExec, LifecycleProfile } from './lifecycle-helper';
|
|
29
|
+
export { materializeWarpMdm, renderWarpMdm } from './warp-config';
|
|
25
30
|
export { DEFAULT_DEPLOYMENT_RUNNER_SOCKET, requestDeploymentCommand, startDeploymentRunner } from './deployment-runner';
|
|
26
31
|
export { createSnpAttestationSource } from './snp-attestation';
|
|
27
32
|
export type { SnpAttestationOptions } from './snp-attestation';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Server } from 'node:net';
|
|
2
|
+
import type { MigrationEvidence, RemoteMigrationClaim } from './migration-pull';
|
|
3
|
+
export declare const DEFAULT_LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
|
|
4
|
+
export interface LifecycleProfile {
|
|
5
|
+
/** Root-owned systemd units whose normal stop path performs application drain. */
|
|
6
|
+
apiUnits: readonly string[];
|
|
7
|
+
/** Root-owned database service stopped only at the final retirement stage. */
|
|
8
|
+
databaseUnit: string;
|
|
9
|
+
/** Loopback health endpoint for the API on this compute. */
|
|
10
|
+
apiHealthUrl: string;
|
|
11
|
+
/** Loopback health endpoint for the database member on this compute. */
|
|
12
|
+
databaseHealthUrl: string;
|
|
13
|
+
/** Ports that must be reachable on every signed controller-provided private peer. */
|
|
14
|
+
databasePorts: readonly number[];
|
|
15
|
+
}
|
|
16
|
+
export interface LifecycleCommandResult {
|
|
17
|
+
exitCode: number;
|
|
18
|
+
stdout: string;
|
|
19
|
+
stderr: string;
|
|
20
|
+
}
|
|
21
|
+
export type LifecycleExec = (argv: readonly string[]) => Promise<LifecycleCommandResult>;
|
|
22
|
+
export declare function validateLifecycleProfile(profile: LifecycleProfile): void;
|
|
23
|
+
export declare function loadLifecycleProfile(path: string): LifecycleProfile;
|
|
24
|
+
export declare const spawnLifecycleCommand: LifecycleExec;
|
|
25
|
+
export declare function executeLifecycleAction(profile: LifecycleProfile, claim: RemoteMigrationClaim, exec?: LifecycleExec, tcpProbe?: (host: string, port: number) => Promise<void>, fetcher?: typeof fetch): Promise<MigrationEvidence>;
|
|
26
|
+
export declare function startLifecycleHelper(options: {
|
|
27
|
+
profile: LifecycleProfile;
|
|
28
|
+
socketPath?: string;
|
|
29
|
+
exec?: LifecycleExec;
|
|
30
|
+
tcpProbe?: (host: string, port: number) => Promise<void>;
|
|
31
|
+
fetch?: typeof fetch;
|
|
32
|
+
}): {
|
|
33
|
+
server: Server;
|
|
34
|
+
stop(): Promise<void>;
|
|
35
|
+
};
|
|
36
|
+
export declare function requestLifecycleAction(claim: RemoteMigrationClaim, socketPath?: string): Promise<MigrationEvidence>;
|