@forgezero/agent 0.1.103 → 0.1.107
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/agent-heartbeat.js +1 -1
- package/dist/bootstrap-bundle.d.ts +6 -0
- package/dist/bootstrap-bundle.js +41 -0
- package/dist/bootstrap.js +60 -4
- package/dist/database-auth-verify.d.ts +6 -0
- package/dist/deployment-connectivity.d.ts +21 -0
- package/dist/deployment-connectivity.js +117 -21
- package/dist/deployment-pull.d.ts +1 -1
- package/dist/deployment-topology.d.ts +2 -0
- package/dist/deployment-topology.js +2 -0
- package/dist/deployment.d.ts +18 -1
- package/dist/fz-agent.js +251 -34
- package/dist/fz.js +164 -97
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.js +60 -4
- package/dist/platform-bootstrap-runtime.d.ts +1 -1
- package/dist/platform-bootstrap-runtime.js +1 -0
- package/dist/platform-fleet-verification.js +129 -26
- package/dist/platform-launch-env.d.ts +19 -0
- package/dist/provision.js +120 -24
- package/dist/software-helper.js +117 -21
- package/dist/version.d.ts +1 -1
- package/package.json +2 -2
package/dist/fz-agent.js
CHANGED
|
@@ -7924,12 +7924,24 @@ function parseBootstrapBundleManifest(value) {
|
|
|
7924
7924
|
// src/deployment.ts
|
|
7925
7925
|
var RELEASE_GROUP = "forgezero-deploy";
|
|
7926
7926
|
var deploymentAttestationChallenge = (revision) => createHash5("sha256").update("forgezero/deployment-attestation/v1\x00").update(revision, "utf8").digest("hex");
|
|
7927
|
+
var deploymentExecution = (phases) => ({
|
|
7928
|
+
steps: phases.flatMap((phase) => phase.steps.map((step2) => ({
|
|
7929
|
+
phase: phase.pipeline.slice(0, 128),
|
|
7930
|
+
step: step2.name.slice(0, 128),
|
|
7931
|
+
outcome: step2.outcome,
|
|
7932
|
+
exitCode: step2.exitCode,
|
|
7933
|
+
durationMs: Math.max(0, Math.min(step2.durationMs, 86400000)),
|
|
7934
|
+
...step2.outcome === "failed" && step2.log.trim() ? { detail: step2.log.trim().slice(0, 2000) } : {}
|
|
7935
|
+
}))).slice(0, 128)
|
|
7936
|
+
});
|
|
7927
7937
|
|
|
7928
7938
|
class DeploymentError extends Error {
|
|
7929
7939
|
code;
|
|
7930
|
-
|
|
7940
|
+
execution;
|
|
7941
|
+
constructor(code, message, execution) {
|
|
7931
7942
|
super(message);
|
|
7932
7943
|
this.code = code;
|
|
7944
|
+
this.execution = execution;
|
|
7933
7945
|
this.name = "DeploymentError";
|
|
7934
7946
|
}
|
|
7935
7947
|
}
|
|
@@ -8280,7 +8292,7 @@ function createDeploymentManager(options) {
|
|
|
8280
8292
|
}
|
|
8281
8293
|
const assignedTopology = assigned.topology;
|
|
8282
8294
|
const selectedTopology = selected.topology;
|
|
8283
|
-
const topologyMatches = selectedTopology ? Boolean(assignedTopology && /^[A-Za-z0-9.-]{1,253}$/.test(assignedTopology.site) && /^[A-Za-z0-9.-]{1,253}$/.test(assignedTopology.nodeIdentity) && /^[A-Za-z0-9._:-]{1,256}$/.test(assignedTopology.localRelayReference) && /^sha256:[a-f0-9]{64}$/.test(assignedTopology.generation) && /^(?:\d{1,3}\.){3}0\/24$/.test(assignedTopology.siteCidr) && assignedTopology.localRelayAddress.length <= 64 && assignedTopology.localPeerAddresses.length <= 1023 && assignedTopology.localPeerAddresses.every((address) => typeof address === "string" && address.length <= 64) && assignedTopology.localPeerIdentities.length === assignedTopology.localPeerAddresses.length && assignedTopology.localPeerIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && assignedTopology.remoteRelayAddresses.length <= 1023 && assignedTopology.remoteRelayAddresses.every((address) => typeof address === "string" && address.length <= 64) && assignedTopology.remoteRelayIdentities.length === assignedTopology.remoteRelayAddresses.length && assignedTopology.remoteRelayIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && assignedTopology.memberIdentities.length >= 1 && assignedTopology.memberIdentities.length <= 1024 && new Set(assignedTopology.memberIdentities).size === assignedTopology.memberIdentities.length && assignedTopology.memberIdentities.includes(assignedTopology.nodeIdentity) && assignedTopology.memberIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && (assignedTopology.role === "member" ? assignedTopology.remoteRelayAddresses.length === 0 : assignedTopology.remoteSiteCidrs.length === assignedTopology.remoteRelayAddresses.length) && assignedTopology.remoteSiteCidrs.every((cidr) => typeof cidr === "string" && /^(?:\d{1,3}\.){3}0\/24$/.test(cidr)) && Number.isSafeInteger(assignedTopology.healthPort) && assignedTopology.healthPort >= 1 && assignedTopology.healthPort <= 65535 && assignedTopology.routedTcpPorts.length >= 1 && assignedTopology.routedTcpPorts.length <= 64 && new Set(assignedTopology.routedTcpPorts).size === assignedTopology.routedTcpPorts.length && assignedTopology.routedTcpPorts.includes(assignedTopology.healthPort) && assignedTopology.routedTcpPorts.every((port) => Number.isSafeInteger(port) && port >= 1 && port <= 65535) && (assignedTopology.role !== "member" || assignedTopology.transport === "private-lan" && assignedTopology.warpRequired === false && assignedTopology.remoteRelayAddresses.length === 0 && assignedTopology.localPeerAddresses.length === 1) && (assignedTopology.warpRequired ? assignedTopology.role !== "member" && assignedTopology.transport === "cloudflare-warp" && selectedTopology.crossMetal.mode !== "private-lan" : assignedTopology.transport === "private-lan")) : assignedTopology === undefined;
|
|
8295
|
+
const topologyMatches = selectedTopology ? Boolean(assignedTopology && /^[A-Za-z0-9.-]{1,253}$/.test(assignedTopology.site) && /^[A-Za-z0-9.-]{1,253}$/.test(assignedTopology.nodeIdentity) && /^[A-Za-z0-9._:-]{1,256}$/.test(assignedTopology.localRelayReference) && /^sha256:[a-f0-9]{64}$/.test(assignedTopology.generation) && /^(?:\d{1,3}\.){3}0\/24$/.test(assignedTopology.siteCidr) && assignedTopology.localRelayAddress.length <= 64 && assignedTopology.localPeerAddresses.length <= 1023 && assignedTopology.localPeerAddresses.every((address) => typeof address === "string" && address.length <= 64) && assignedTopology.localPeerIdentities.length === assignedTopology.localPeerAddresses.length && assignedTopology.localPeerIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && assignedTopology.remoteRelayAddresses.length <= 1023 && assignedTopology.remoteRelayAddresses.every((address) => typeof address === "string" && address.length <= 64) && assignedTopology.remoteRelayIdentities.length === assignedTopology.remoteRelayAddresses.length && assignedTopology.remoteRelayIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && assignedTopology.remoteMemberAddresses.length <= 1024 && new Set(assignedTopology.remoteMemberAddresses).size === assignedTopology.remoteMemberAddresses.length && assignedTopology.remoteMemberAddresses.every((address) => typeof address === "string" && address.length <= 64) && assignedTopology.memberIdentities.length >= 1 && assignedTopology.memberIdentities.length <= 1024 && new Set(assignedTopology.memberIdentities).size === assignedTopology.memberIdentities.length && assignedTopology.memberIdentities.includes(assignedTopology.nodeIdentity) && assignedTopology.memberIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && (assignedTopology.role === "member" ? assignedTopology.remoteRelayAddresses.length === 0 : assignedTopology.remoteSiteCidrs.length === assignedTopology.remoteRelayAddresses.length) && assignedTopology.remoteSiteCidrs.every((cidr) => typeof cidr === "string" && /^(?:\d{1,3}\.){3}0\/24$/.test(cidr)) && Number.isSafeInteger(assignedTopology.healthPort) && assignedTopology.healthPort >= 1 && assignedTopology.healthPort <= 65535 && assignedTopology.routedTcpPorts.length >= 1 && assignedTopology.routedTcpPorts.length <= 64 && new Set(assignedTopology.routedTcpPorts).size === assignedTopology.routedTcpPorts.length && assignedTopology.routedTcpPorts.includes(assignedTopology.healthPort) && assignedTopology.routedTcpPorts.every((port) => Number.isSafeInteger(port) && port >= 1 && port <= 65535) && (assignedTopology.role !== "member" || assignedTopology.transport === "private-lan" && assignedTopology.warpRequired === false && assignedTopology.remoteRelayAddresses.length === 0 && assignedTopology.localPeerAddresses.length === 1) && (assignedTopology.warpRequired ? assignedTopology.role !== "member" && assignedTopology.transport === "cloudflare-warp" && selectedTopology.crossMetal.mode !== "private-lan" : assignedTopology.transport === "private-lan")) : assignedTopology === undefined;
|
|
8284
8296
|
const expectedPrivate = selectedTopology ? assignedTopology?.warpRequired ? selectedTopology.crossMetal.mode === "private-lan" ? undefined : {
|
|
8285
8297
|
mode: "cloudflare-warp",
|
|
8286
8298
|
credential: selectedTopology.crossMetal.credential,
|
|
@@ -8318,6 +8330,7 @@ function createDeploymentManager(options) {
|
|
|
8318
8330
|
FZ_TOPOLOGY_LOCAL_PEERS: assigned.topology.localPeerAddresses.join(","),
|
|
8319
8331
|
FZ_TOPOLOGY_LOCAL_PEER_IDENTITIES: assigned.topology.localPeerIdentities.join(","),
|
|
8320
8332
|
FZ_TOPOLOGY_REMOTE_RELAYS: assigned.topology.remoteRelayAddresses.join(","),
|
|
8333
|
+
FZ_TOPOLOGY_REMOTE_MEMBERS: assigned.topology.remoteMemberAddresses.join(","),
|
|
8321
8334
|
FZ_TOPOLOGY_REMOTE_RELAY_IDENTITIES: assigned.topology.remoteRelayIdentities.join(","),
|
|
8322
8335
|
FZ_TOPOLOGY_REMOTE_CIDRS: assigned.topology.remoteSiteCidrs.join(","),
|
|
8323
8336
|
FZ_TOPOLOGY_MEMBERS: assigned.topology.memberIdentities.join(","),
|
|
@@ -8338,6 +8351,18 @@ function createDeploymentManager(options) {
|
|
|
8338
8351
|
throw new DeploymentError("PIPELINE_FAILED", `component limits exceed the reserved capacity of target ${targetName}`);
|
|
8339
8352
|
}
|
|
8340
8353
|
}
|
|
8354
|
+
if (assigned?.firewallPolicy) {
|
|
8355
|
+
if (!options.ensureSoftware || !options.applyConnectivity) {
|
|
8356
|
+
throw new DeploymentError("PIPELINE_FAILED", "the supervised firewall helper is unavailable");
|
|
8357
|
+
}
|
|
8358
|
+
await options.ensureSoftware([{ id: "ufw", version: "latest" }]);
|
|
8359
|
+
await options.applyConnectivity({
|
|
8360
|
+
key: `${options.key}:${assigned.targetName}:${assigned.slot}`,
|
|
8361
|
+
intent: {},
|
|
8362
|
+
capabilities: {},
|
|
8363
|
+
firewallPolicy: structuredClone(assigned.firewallPolicy)
|
|
8364
|
+
});
|
|
8365
|
+
}
|
|
8341
8366
|
const inputValues = resolveDeploymentInputs(plan, options.environment ?? {});
|
|
8342
8367
|
const projectExec2 = options.projectExec ?? spawnExact;
|
|
8343
8368
|
const materializePlanArgv = (argv2) => argv2.map((argument) => {
|
|
@@ -8416,6 +8441,7 @@ function createDeploymentManager(options) {
|
|
|
8416
8441
|
localPeerIdentities: assigned.topology.localPeerIdentities,
|
|
8417
8442
|
remoteRelayAddresses: assigned.topology.remoteRelayAddresses,
|
|
8418
8443
|
remoteRelayIdentities: assigned.topology.remoteRelayIdentities,
|
|
8444
|
+
remoteMemberAddresses: assigned.topology.remoteMemberAddresses,
|
|
8419
8445
|
remoteSiteCidrs: assigned.topology.remoteSiteCidrs,
|
|
8420
8446
|
memberIdentities: assigned.topology.memberIdentities,
|
|
8421
8447
|
healthPort: assigned.topology.healthPort,
|
|
@@ -8430,6 +8456,7 @@ function createDeploymentManager(options) {
|
|
|
8430
8456
|
throw new DeploymentActionError("refused", "exec.argv requires an exact argv array");
|
|
8431
8457
|
const environment = { ...phaseEnvironment2 };
|
|
8432
8458
|
const bindings = resolved.credentials;
|
|
8459
|
+
const credentialValues = [];
|
|
8433
8460
|
if (bindings !== undefined) {
|
|
8434
8461
|
if (!bindings || typeof bindings !== "object" || Array.isArray(bindings))
|
|
8435
8462
|
throw new DeploymentActionError("refused", "exec.argv credentials must map environment names to credential declarations");
|
|
@@ -8440,11 +8467,12 @@ function createDeploymentManager(options) {
|
|
|
8440
8467
|
if (!credential || !options.cache)
|
|
8441
8468
|
throw new DeploymentActionError("refused", `credential ${credentialName} is unavailable`);
|
|
8442
8469
|
environment[environmentName] = await options.cache.get(credential.source.name);
|
|
8470
|
+
credentialValues.push(environment[environmentName]);
|
|
8443
8471
|
}
|
|
8444
8472
|
}
|
|
8445
8473
|
const result = await projectExec2({ argv: materializePlanArgv(resolved.argv), cwd: release, env: environment, timeoutMs: step2.timeoutMs });
|
|
8446
8474
|
if (result.exitCode !== 0)
|
|
8447
|
-
throw new DeploymentActionError("failed", result.output.slice(0, 2000) || `command exited ${result.exitCode}`);
|
|
8475
|
+
throw new DeploymentActionError("failed", redact(result.output, credentialValues).slice(0, 2000) || `command exited ${result.exitCode}`);
|
|
8448
8476
|
return { outputs: { exitCode: result.exitCode } };
|
|
8449
8477
|
},
|
|
8450
8478
|
"forgezero.health/http@1": async ({ resolved }) => {
|
|
@@ -8562,7 +8590,7 @@ function createDeploymentManager(options) {
|
|
|
8562
8590
|
};
|
|
8563
8591
|
if (!run2.ok) {
|
|
8564
8592
|
const failed = run2.steps.find((step2) => step2.status === "failed" || step2.status === "manual-intervention");
|
|
8565
|
-
throw new DeploymentError("PIPELINE_FAILED", `deployment plan failed at ${failed?.id ?? "unknown step"}: ${failed?.error?.message ?? run2.status}
|
|
8593
|
+
throw new DeploymentError("PIPELINE_FAILED", `deployment plan failed at ${failed?.id ?? "unknown step"}: ${failed?.error?.message ?? run2.status}`, deploymentExecution([phase]));
|
|
8566
8594
|
}
|
|
8567
8595
|
return {
|
|
8568
8596
|
key: options.key,
|
|
@@ -8575,6 +8603,7 @@ function createDeploymentManager(options) {
|
|
|
8575
8603
|
release,
|
|
8576
8604
|
ok: true,
|
|
8577
8605
|
phases: [phase],
|
|
8606
|
+
execution: deploymentExecution([phase]),
|
|
8578
8607
|
resolvedTargets
|
|
8579
8608
|
};
|
|
8580
8609
|
}
|
|
@@ -8652,7 +8681,7 @@ function createDeploymentManager(options) {
|
|
|
8652
8681
|
if (!result.ok) {
|
|
8653
8682
|
const failedStep = result.steps.find((step2) => step2.outcome === "failed");
|
|
8654
8683
|
const detail = failedStep?.log.trim().slice(0, 2000);
|
|
8655
|
-
throw new DeploymentError("PIPELINE_FAILED", `${pipeline.name} failed at ${failedStep?.name ?? "unknown step"}` + (detail ? `: ${detail}` : ` (exit ${failedStep?.exitCode ?? "unknown"}).`));
|
|
8684
|
+
throw new DeploymentError("PIPELINE_FAILED", `${pipeline.name} failed at ${failedStep?.name ?? "unknown step"}` + (detail ? `: ${detail}` : ` (exit ${failedStep?.exitCode ?? "unknown"}).`), deploymentExecution(phases));
|
|
8656
8685
|
}
|
|
8657
8686
|
if (pipeline.phase === "release")
|
|
8658
8687
|
await activateSelectedService();
|
|
@@ -8694,6 +8723,7 @@ function createDeploymentManager(options) {
|
|
|
8694
8723
|
release,
|
|
8695
8724
|
ok: true,
|
|
8696
8725
|
phases,
|
|
8726
|
+
execution: deploymentExecution(phases),
|
|
8697
8727
|
...capacity ? { capacity } : {}
|
|
8698
8728
|
};
|
|
8699
8729
|
};
|
|
@@ -9000,7 +9030,8 @@ async function pullDeploymentOnce(options) {
|
|
|
9000
9030
|
runKey: claim.runKey,
|
|
9001
9031
|
claimToken: claim.claimToken,
|
|
9002
9032
|
ok: false,
|
|
9003
|
-
detail: reason
|
|
9033
|
+
detail: reason,
|
|
9034
|
+
...cause instanceof DeploymentError && cause.execution ? { execution: cause.execution } : {}
|
|
9004
9035
|
});
|
|
9005
9036
|
if (options.telemetry) {
|
|
9006
9037
|
await options.telemetry.observe("deployment.complete", acknowledge2, () => "success", remoteOutcome);
|
|
@@ -9017,6 +9048,7 @@ async function pullDeploymentOnce(options) {
|
|
|
9017
9048
|
definitionDigest: result.definitionDigest,
|
|
9018
9049
|
definitionVersion: result.definitionVersion,
|
|
9019
9050
|
profile: result.profile,
|
|
9051
|
+
execution: result.execution,
|
|
9020
9052
|
...result.resolvedTargets ? { resolvedTargets: result.resolvedTargets.map((target) => ({
|
|
9021
9053
|
...target,
|
|
9022
9054
|
profiles: [...target.profiles],
|
|
@@ -9496,7 +9528,7 @@ async function writeAndCloseProcessInput(input, value) {
|
|
|
9496
9528
|
}
|
|
9497
9529
|
|
|
9498
9530
|
// src/version.ts
|
|
9499
|
-
var VERSION2 = "0.1.
|
|
9531
|
+
var VERSION2 = "0.1.107";
|
|
9500
9532
|
|
|
9501
9533
|
// src/ssh-bootstrap.ts
|
|
9502
9534
|
class SshBootstrapError extends Error {
|
|
@@ -13384,7 +13416,7 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
13384
13416
|
// src/deployment-connectivity.ts
|
|
13385
13417
|
import { createHash as createHash11 } from "crypto";
|
|
13386
13418
|
import { mkdirSync as mkdirSync12, renameSync as renameSync12, writeFileSync as writeFileSync13 } from "fs";
|
|
13387
|
-
import { createConnection as createConnection2 } from "net";
|
|
13419
|
+
import { createConnection as createConnection2, isIP as isIP4 } from "net";
|
|
13388
13420
|
import { dirname as dirname11 } from "path";
|
|
13389
13421
|
var TOKEN2 = /^[A-Za-z0-9._-]{40,16384}$/;
|
|
13390
13422
|
var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
|
|
@@ -13463,21 +13495,99 @@ function validate2(request) {
|
|
|
13463
13495
|
}
|
|
13464
13496
|
} else if (request.capabilities.private)
|
|
13465
13497
|
throw new Error("unexpected private deployment capability");
|
|
13498
|
+
if (request.firewallPolicy) {
|
|
13499
|
+
const policy = request.firewallPolicy;
|
|
13500
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(policy.generation) || !Array.isArray(policy.rules) || policy.rules.length > 256) {
|
|
13501
|
+
throw new Error("deployment firewall policy is malformed");
|
|
13502
|
+
}
|
|
13503
|
+
let expanded = 0;
|
|
13504
|
+
for (const rule of policy.rules) {
|
|
13505
|
+
expanded += rule.sourceAddresses.length;
|
|
13506
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(rule.ruleKey) || !["allow", "deny"].includes(rule.action) || !["tcp", "udp"].includes(rule.protocol) || rule.sourceAddresses.length < 1 || rule.sourceAddresses.length > 1024 || new Set(rule.sourceAddresses).size !== rule.sourceAddresses.length || rule.sourceAddresses.some((address) => isIP4(address) !== 4) || !Number.isSafeInteger(rule.portFrom) || rule.portFrom < 1 || rule.portFrom > 65535 || !Number.isSafeInteger(rule.portTo) || rule.portTo < rule.portFrom || rule.portTo > 65535 || !Number.isSafeInteger(rule.priority) || rule.priority < 0 || rule.priority > 1e6) {
|
|
13507
|
+
throw new Error("deployment firewall policy is malformed");
|
|
13508
|
+
}
|
|
13509
|
+
}
|
|
13510
|
+
if (expanded > 2048)
|
|
13511
|
+
throw new Error("deployment firewall policy expands beyond its rule limit");
|
|
13512
|
+
}
|
|
13513
|
+
}
|
|
13514
|
+
async function replaceTaggedUfwRules(host, comment) {
|
|
13515
|
+
const status = await checked7(host, ["/usr/sbin/ufw", "status", "numbered"], "firewall inventory");
|
|
13516
|
+
const numbers = status.split(`
|
|
13517
|
+
`).flatMap((line) => {
|
|
13518
|
+
if (!line.includes(comment))
|
|
13519
|
+
return [];
|
|
13520
|
+
const match = line.match(/^\s*\[\s*(\d{1,6})\]/);
|
|
13521
|
+
return match ? [Number(match[1])] : [];
|
|
13522
|
+
}).filter((value) => Number.isSafeInteger(value) && value > 0).sort((left, right) => right - left);
|
|
13523
|
+
for (const number2 of numbers)
|
|
13524
|
+
await checked7(host, ["/usr/sbin/ufw", "--force", "delete", String(number2)], `stale firewall rule ${number2}`);
|
|
13466
13525
|
}
|
|
13467
13526
|
async function applyDeploymentConnectivity(request, host = defaultHost3) {
|
|
13468
13527
|
validate2(request);
|
|
13469
13528
|
const id2 = idFor3(request.key);
|
|
13470
13529
|
const evidence = { key: request.key };
|
|
13530
|
+
if (request.firewallPolicy) {
|
|
13531
|
+
const policy = request.firewallPolicy;
|
|
13532
|
+
const comment = `fz-policy-${id2}`;
|
|
13533
|
+
await replaceTaggedUfwRules(host, comment);
|
|
13534
|
+
const ordered = [...policy.rules].sort((left, right) => left.priority - right.priority || (left.action === right.action ? left.ruleKey.localeCompare(right.ruleKey) : left.action === "deny" ? -1 : 1));
|
|
13535
|
+
const commands = [];
|
|
13536
|
+
for (const rule of ordered)
|
|
13537
|
+
for (const source of rule.sourceAddresses)
|
|
13538
|
+
commands.push([
|
|
13539
|
+
"/usr/sbin/ufw",
|
|
13540
|
+
"insert",
|
|
13541
|
+
"1",
|
|
13542
|
+
rule.action,
|
|
13543
|
+
"from",
|
|
13544
|
+
source,
|
|
13545
|
+
"to",
|
|
13546
|
+
"any",
|
|
13547
|
+
"port",
|
|
13548
|
+
rule.portFrom === rule.portTo ? String(rule.portFrom) : `${rule.portFrom}:${rule.portTo}`,
|
|
13549
|
+
"proto",
|
|
13550
|
+
rule.protocol,
|
|
13551
|
+
"comment",
|
|
13552
|
+
comment
|
|
13553
|
+
]);
|
|
13554
|
+
const ranges = new Map;
|
|
13555
|
+
for (const rule of ordered)
|
|
13556
|
+
ranges.set(`${rule.protocol}:${rule.portFrom}:${rule.portTo}`, rule);
|
|
13557
|
+
for (const range of ranges.values())
|
|
13558
|
+
commands.push([
|
|
13559
|
+
"/usr/sbin/ufw",
|
|
13560
|
+
"insert",
|
|
13561
|
+
"1",
|
|
13562
|
+
"deny",
|
|
13563
|
+
"to",
|
|
13564
|
+
"any",
|
|
13565
|
+
"port",
|
|
13566
|
+
range.portFrom === range.portTo ? String(range.portFrom) : `${range.portFrom}:${range.portTo}`,
|
|
13567
|
+
"proto",
|
|
13568
|
+
range.protocol,
|
|
13569
|
+
"comment",
|
|
13570
|
+
comment
|
|
13571
|
+
]);
|
|
13572
|
+
for (const command3 of commands.toReversed())
|
|
13573
|
+
await checked7(host, command3, "deployment firewall rule");
|
|
13574
|
+
evidence.firewall = { generation: policy.generation, rules: commands.length, active: true };
|
|
13575
|
+
}
|
|
13471
13576
|
const topology = request.topology;
|
|
13472
13577
|
if (topology) {
|
|
13473
|
-
const values = [
|
|
13578
|
+
const values = [
|
|
13579
|
+
topology.localRelayAddress,
|
|
13580
|
+
...topology.localPeerAddresses,
|
|
13581
|
+
...topology.remoteRelayAddresses,
|
|
13582
|
+
...topology.remoteMemberAddresses
|
|
13583
|
+
];
|
|
13474
13584
|
const identities = [
|
|
13475
13585
|
topology.nodeIdentity,
|
|
13476
13586
|
...topology.localPeerIdentities,
|
|
13477
13587
|
...topology.remoteRelayIdentities,
|
|
13478
13588
|
...topology.memberIdentities
|
|
13479
13589
|
];
|
|
13480
|
-
if (!/^[A-Za-z0-9.-]{1,253}$/.test(topology.site) || !/^(?:\d{1,3}\.){3}0\/24$/.test(topology.siteCidr) || values.some((value) =>
|
|
13590
|
+
if (!/^[A-Za-z0-9.-]{1,253}$/.test(topology.site) || !/^(?:\d{1,3}\.){3}0\/24$/.test(topology.siteCidr) || values.some((value) => isIP4(value) !== 4) || identities.some((value) => !/^[A-Za-z0-9.-]{1,253}$/.test(value)) || topology.localPeerIdentities.length !== topology.localPeerAddresses.length || topology.remoteRelayIdentities.length !== topology.remoteRelayAddresses.length || topology.remoteMemberAddresses.length > 1024 || new Set(topology.remoteMemberAddresses).size !== topology.remoteMemberAddresses.length || topology.memberIdentities.length < 1 || topology.memberIdentities.length > 1024 || new Set(topology.memberIdentities).size !== topology.memberIdentities.length || !topology.memberIdentities.includes(topology.nodeIdentity) || !/^sha256:[a-f0-9]{64}$/.test(topology.generation) || (topology.role === "member" ? topology.remoteRelayAddresses.length !== 0 : topology.remoteSiteCidrs.length !== topology.remoteRelayAddresses.length) || topology.remoteSiteCidrs.some((value) => !/^(?:\d{1,3}\.){3}0\/24$/.test(value)) || new Set(topology.remoteSiteCidrs).size !== topology.remoteSiteCidrs.length || !Number.isSafeInteger(topology.healthPort) || topology.healthPort < 1 || topology.healthPort > 65535 || topology.routedTcpPorts.length < 1 || topology.routedTcpPorts.length > 64 || new Set(topology.routedTcpPorts).size !== topology.routedTcpPorts.length || !topology.routedTcpPorts.includes(topology.healthPort) || topology.routedTcpPorts.some((port) => !Number.isSafeInteger(port) || port < 1 || port > 65535) || topology.role === "member" && topology.transport !== "private-lan") {
|
|
13481
13591
|
throw new Error("deployment topology is malformed");
|
|
13482
13592
|
}
|
|
13483
13593
|
if (request.intent.private?.mode === "cloudflare-warp" !== (topology.transport === "cloudflare-warp")) {
|
|
@@ -13604,28 +13714,46 @@ ${forwarding}${noOp}${starts}${starts ? `
|
|
|
13604
13714
|
[Install]
|
|
13605
13715
|
WantedBy=multi-user.target
|
|
13606
13716
|
`, 420);
|
|
13607
|
-
|
|
13717
|
+
const firewallComment = `fz-topology-${id2}`;
|
|
13718
|
+
await replaceTaggedUfwRules(host, firewallComment);
|
|
13719
|
+
for (const source of [...new Set([...topology.localPeerAddresses, ...topology.remoteMemberAddresses])]) {
|
|
13608
13720
|
for (const port of topology.routedTcpPorts) {
|
|
13609
|
-
await checked7(host, [
|
|
13721
|
+
await checked7(host, [
|
|
13722
|
+
"/usr/sbin/ufw",
|
|
13723
|
+
"allow",
|
|
13724
|
+
"from",
|
|
13725
|
+
source,
|
|
13726
|
+
"to",
|
|
13727
|
+
"any",
|
|
13728
|
+
"port",
|
|
13729
|
+
String(port),
|
|
13730
|
+
"proto",
|
|
13731
|
+
"tcp",
|
|
13732
|
+
"comment",
|
|
13733
|
+
firewallComment
|
|
13734
|
+
], `private service ${source}:${port}`);
|
|
13610
13735
|
}
|
|
13611
13736
|
}
|
|
13612
13737
|
if (topology.role !== "member")
|
|
13613
|
-
for (const
|
|
13614
|
-
for (const
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
|
|
13618
|
-
|
|
13619
|
-
|
|
13620
|
-
|
|
13621
|
-
|
|
13622
|
-
|
|
13623
|
-
|
|
13624
|
-
|
|
13625
|
-
|
|
13626
|
-
|
|
13627
|
-
|
|
13628
|
-
|
|
13738
|
+
for (const remoteAddress of topology.remoteMemberAddresses) {
|
|
13739
|
+
for (const source of topology.localPeerAddresses)
|
|
13740
|
+
for (const port of topology.routedTcpPorts) {
|
|
13741
|
+
await checked7(host, [
|
|
13742
|
+
"/usr/sbin/ufw",
|
|
13743
|
+
"route",
|
|
13744
|
+
"allow",
|
|
13745
|
+
"proto",
|
|
13746
|
+
"tcp",
|
|
13747
|
+
"from",
|
|
13748
|
+
source,
|
|
13749
|
+
"to",
|
|
13750
|
+
remoteAddress,
|
|
13751
|
+
"port",
|
|
13752
|
+
String(port),
|
|
13753
|
+
"comment",
|
|
13754
|
+
firewallComment
|
|
13755
|
+
], `private routed service ${source}->${remoteAddress}:${port}`);
|
|
13756
|
+
}
|
|
13629
13757
|
}
|
|
13630
13758
|
await checked7(host, ["/usr/bin/systemctl", "daemon-reload"], "topology daemon reload");
|
|
13631
13759
|
await checked7(host, ["/usr/bin/systemctl", "enable", "--now", routeUnit], "topology routes");
|
|
@@ -15172,7 +15300,7 @@ import { dirname as dirname14 } from "path";
|
|
|
15172
15300
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
15173
15301
|
|
|
15174
15302
|
// src/provision.ts
|
|
15175
|
-
import { isIP as
|
|
15303
|
+
import { isIP as isIP5 } from "net";
|
|
15176
15304
|
function atLeast(version, floor) {
|
|
15177
15305
|
const parse = (value) => (value.trim().replace(/^v/, "").match(/\d+/g) ?? []).slice(0, 3).map(Number);
|
|
15178
15306
|
const got = parse(version);
|
|
@@ -15652,7 +15780,7 @@ function agentUnit(options) {
|
|
|
15652
15780
|
throw new Error("compute telemetry endpoint must be an absolute collector URL");
|
|
15653
15781
|
}
|
|
15654
15782
|
const localCollector = endpoint.protocol === "http:" && endpoint.hostname === "127.0.0.1" && endpoint.port === "4318" && endpoint.pathname === "/";
|
|
15655
|
-
const publicCollector = endpoint.protocol === "https:" && !endpoint.username && !endpoint.password && !endpoint.search && !endpoint.hash &&
|
|
15783
|
+
const publicCollector = endpoint.protocol === "https:" && !endpoint.username && !endpoint.password && !endpoint.search && !endpoint.hash && isIP5(endpoint.hostname) === 0 && endpoint.hostname.includes(".") && endpoint.hostname !== "localhost" && !endpoint.hostname.endsWith(".local");
|
|
15656
15784
|
if (!localCollector && !publicCollector || endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
|
|
15657
15785
|
throw new Error("compute telemetry endpoint must be the supervised loopback collector or credential-free public HTTPS");
|
|
15658
15786
|
}
|
|
@@ -16480,6 +16608,7 @@ var BOOTSTRAP_STATE_PATH = "/var/lib/forgezero/bootstrap.json";
|
|
|
16480
16608
|
var STATE_PATH = BOOTSTRAP_STATE_PATH;
|
|
16481
16609
|
var CREDS = "/etc/forgezero/creds";
|
|
16482
16610
|
var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
|
|
16611
|
+
var ARANGO_ROOT_CREDENTIAL = `${CREDS}/arangodb-root-password.cred`;
|
|
16483
16612
|
var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
|
|
16484
16613
|
var TUNNEL_CREDENTIAL = `${CREDS}/CF_TUNNEL_CONNECTOR_TOKEN.cred`;
|
|
16485
16614
|
var CF_API_CREDENTIAL = `${CREDS}/CF_API_TOKEN.cred`;
|
|
@@ -16641,6 +16770,11 @@ async function bootstrapStatus(host = localBootstrapHost()) {
|
|
|
16641
16770
|
if (nginx3.exitCode !== 0)
|
|
16642
16771
|
problems.push("nginx configuration is invalid");
|
|
16643
16772
|
if (state.databaseRole !== "none") {
|
|
16773
|
+
for (const credential of [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL]) {
|
|
16774
|
+
services[credential] = host.exists(credential);
|
|
16775
|
+
if (!services[credential])
|
|
16776
|
+
problems.push(`${credential} is missing`);
|
|
16777
|
+
}
|
|
16644
16778
|
const unitPath = "/etc/systemd/system/forgezero-db.service";
|
|
16645
16779
|
const expectsNoAgency = state.databaseAgency === "none";
|
|
16646
16780
|
const unitHasNoAgency = host.exists(unitPath) && host.read(unitPath).includes("--cluster.start-agent=false");
|
|
@@ -16864,7 +16998,7 @@ async function verifyPlatformFleetHost(expectedVersion, runtime = localRuntime()
|
|
|
16864
16998
|
// src/community-rehearsal-host.ts
|
|
16865
16999
|
import { createHash as createHash12 } from "crypto";
|
|
16866
17000
|
import { lstatSync as lstatSync8, mkdirSync as mkdirSync17, readFileSync as readFileSync18, rmSync as rmSync13, symlinkSync as symlinkSync8, unlinkSync as unlinkSync13, writeFileSync as writeFileSync17 } from "fs";
|
|
16867
|
-
import { isIP as
|
|
17001
|
+
import { isIP as isIP6 } from "net";
|
|
16868
17002
|
import { dirname as dirname15 } from "path";
|
|
16869
17003
|
var COMMUNITY_REHEARSAL_VERSION = "3.11.14";
|
|
16870
17004
|
var COMMUNITY_REHEARSAL_ROOT = "/opt/forgezero-rehearsals/community-cluster-api";
|
|
@@ -16897,7 +17031,7 @@ var validateCommunityRehearsalTopology = (value) => {
|
|
|
16897
17031
|
throw new Error("community rehearsal requires exactly four reviewed nodes");
|
|
16898
17032
|
const rows = value.map((item, index) => {
|
|
16899
17033
|
const row2 = exactObject(item);
|
|
16900
|
-
if (Object.keys(row2).some((key) => !["name", "address", "agency", "starter", "readPreferred"].includes(key)) || typeof row2.name !== "string" || !SAFE_NAME2.test(row2.name) || typeof row2.address !== "string" ||
|
|
17034
|
+
if (Object.keys(row2).some((key) => !["name", "address", "agency", "starter", "readPreferred"].includes(key)) || typeof row2.name !== "string" || !SAFE_NAME2.test(row2.name) || typeof row2.address !== "string" || isIP6(row2.address) !== 4 || typeof row2.agency !== "boolean" || typeof row2.starter !== "boolean" || typeof row2.readPreferred !== "boolean") {
|
|
16901
17035
|
throw new Error(`community rehearsal topology node ${index + 1} is invalid`);
|
|
16902
17036
|
}
|
|
16903
17037
|
return row2;
|
|
@@ -17250,8 +17384,75 @@ async function readBoundedStdin(maxBytes) {
|
|
|
17250
17384
|
throw new Error(`stdin request exceeds ${maxBytes} bytes`);
|
|
17251
17385
|
return Buffer.from(stdout).toString("utf8");
|
|
17252
17386
|
}
|
|
17387
|
+
|
|
17388
|
+
// src/database-auth-verify.ts
|
|
17389
|
+
import { isIP as isIP7 } from "net";
|
|
17390
|
+
var MAX_RESPONSE_BYTES2 = 16 * 1024;
|
|
17391
|
+
function databaseEndpoint(raw) {
|
|
17392
|
+
let endpoint;
|
|
17393
|
+
try {
|
|
17394
|
+
endpoint = new URL(raw);
|
|
17395
|
+
} catch {
|
|
17396
|
+
throw new Error("database authentication endpoint is malformed");
|
|
17397
|
+
}
|
|
17398
|
+
const hostname2 = endpoint.hostname.replace(/^\[|\]$/g, "");
|
|
17399
|
+
const ipVersion = isIP7(hostname2);
|
|
17400
|
+
const privateIp2 = ipVersion === 4 ? /^(?:10\.|192\.168\.|172\.(?:1[6-9]|2[0-9]|3[01])\.)/.test(hostname2) : ipVersion === 6 && /^(?:fc|fd)[0-9a-f]{2}:/i.test(hostname2);
|
|
17401
|
+
if (endpoint.protocol !== "http:" || endpoint.username || endpoint.password || endpoint.port !== "8529" || endpoint.pathname !== "/" || endpoint.search || endpoint.hash || !privateIp2)
|
|
17402
|
+
throw new Error("database authentication endpoint must be an exact private IP on port 8529");
|
|
17403
|
+
return endpoint;
|
|
17404
|
+
}
|
|
17405
|
+
function rootCredential(readCredential) {
|
|
17406
|
+
const password = readCredential();
|
|
17407
|
+
if (password.length < 48 || password.length > 256 || /[\r\n\0]/.test(password)) {
|
|
17408
|
+
throw new Error("ArangoDB root credential is malformed");
|
|
17409
|
+
}
|
|
17410
|
+
return password;
|
|
17411
|
+
}
|
|
17412
|
+
async function boundedBody(response) {
|
|
17413
|
+
const body = await response.text();
|
|
17414
|
+
if (Buffer.byteLength(body) > MAX_RESPONSE_BYTES2)
|
|
17415
|
+
throw new Error("ArangoDB authentication proof response is too large");
|
|
17416
|
+
return body;
|
|
17417
|
+
}
|
|
17418
|
+
async function refused2(fetcher, url, authorization) {
|
|
17419
|
+
const response = await fetcher(url, {
|
|
17420
|
+
method: "GET",
|
|
17421
|
+
headers: authorization ? { authorization } : undefined,
|
|
17422
|
+
redirect: "error"
|
|
17423
|
+
});
|
|
17424
|
+
await boundedBody(response);
|
|
17425
|
+
if (response.status !== 401 && response.status !== 403) {
|
|
17426
|
+
throw new Error("ArangoDB accepted an unauthenticated or empty-password request");
|
|
17427
|
+
}
|
|
17428
|
+
}
|
|
17429
|
+
async function verifyDatabaseAuthentication(rawEndpoint, options) {
|
|
17430
|
+
const endpoint = databaseEndpoint(rawEndpoint);
|
|
17431
|
+
const password = rootCredential(options.readCredential);
|
|
17432
|
+
const versionUrl = new URL("/_api/version", endpoint);
|
|
17433
|
+
const fetcher = options.fetcher ?? fetch;
|
|
17434
|
+
await refused2(fetcher, versionUrl);
|
|
17435
|
+
await refused2(fetcher, versionUrl, `Basic ${Buffer.from("root:").toString("base64")}`);
|
|
17436
|
+
const response = await fetcher(versionUrl, {
|
|
17437
|
+
method: "GET",
|
|
17438
|
+
headers: { authorization: `Basic ${Buffer.from(`root:${password}`).toString("base64")}` },
|
|
17439
|
+
redirect: "error"
|
|
17440
|
+
});
|
|
17441
|
+
const body = await boundedBody(response);
|
|
17442
|
+
if (!response.ok)
|
|
17443
|
+
throw new Error(`ArangoDB sealed root authentication failed with status ${response.status}`);
|
|
17444
|
+
let value;
|
|
17445
|
+
try {
|
|
17446
|
+
value = JSON.parse(body);
|
|
17447
|
+
} catch {
|
|
17448
|
+
throw new Error("ArangoDB authentication proof returned malformed JSON");
|
|
17449
|
+
}
|
|
17450
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || value.version !== "3.11.14") {
|
|
17451
|
+
throw new Error("ArangoDB authentication proof returned the wrong version");
|
|
17452
|
+
}
|
|
17453
|
+
}
|
|
17253
17454
|
// src/deployment-topology.ts
|
|
17254
|
-
import { isIP as
|
|
17455
|
+
import { isIP as isIP8 } from "net";
|
|
17255
17456
|
|
|
17256
17457
|
class DeploymentTopologyError extends Error {
|
|
17257
17458
|
constructor(message) {
|
|
@@ -17280,7 +17481,7 @@ function planDeploymentTopology(args) {
|
|
|
17280
17481
|
const references = new Set;
|
|
17281
17482
|
const slots = new Set;
|
|
17282
17483
|
for (const placement of args.placements) {
|
|
17283
|
-
if (!SAFE.test(placement.computeReference) || !SAFE.test(placement.nodeIdentity) || !SAFE.test(placement.metalHostname) ||
|
|
17484
|
+
if (!SAFE.test(placement.computeReference) || !SAFE.test(placement.nodeIdentity) || !SAFE.test(placement.metalHostname) || isIP8(placement.privateAddress) !== 4 || !PRIVATE_V4(placement.privateAddress) || !validSiteCidr(placement.siteCidr) || !placement.privateAddress.startsWith(placement.siteCidr.slice(0, -4)) || !Number.isSafeInteger(placement.slot) || placement.slot < 0 || placement.slot > 1e6 || references.has(placement.computeReference) || slots.has(placement.slot)) {
|
|
17284
17485
|
throw new DeploymentTopologyError("topology placement is malformed or duplicated");
|
|
17285
17486
|
}
|
|
17286
17487
|
references.add(placement.computeReference);
|
|
@@ -17332,6 +17533,7 @@ function planDeploymentTopology(args) {
|
|
|
17332
17533
|
const relay = active.get(site);
|
|
17333
17534
|
const remoteRelayAddresses = siteNames.filter((other) => other !== site).map((other) => active.get(other).privateAddress);
|
|
17334
17535
|
const remoteRelayIdentities = siteNames.filter((other) => other !== site).map((other) => active.get(other).nodeIdentity);
|
|
17536
|
+
const remoteMemberAddresses = siteNames.filter((other) => other !== site).flatMap((other) => sites.get(other).map(({ privateAddress }) => privateAddress));
|
|
17335
17537
|
const remoteSiteCidrs = siteNames.filter((other) => other !== site).map((other) => siteCidrs.get(other));
|
|
17336
17538
|
return members2.map((placement, index) => {
|
|
17337
17539
|
const role = index === 0 ? "relay" : index === 1 && topology.relays.standbyPerMetal === 1 ? "standby" : "member";
|
|
@@ -17349,6 +17551,7 @@ function planDeploymentTopology(args) {
|
|
|
17349
17551
|
localPeerIdentities,
|
|
17350
17552
|
remoteRelayAddresses: crossSite ? remoteRelayAddresses : [],
|
|
17351
17553
|
remoteRelayIdentities: crossSite ? remoteRelayIdentities : [],
|
|
17554
|
+
remoteMemberAddresses: siteNames.length > 1 ? remoteMemberAddresses : [],
|
|
17352
17555
|
remoteSiteCidrs: siteNames.length > 1 ? remoteSiteCidrs : [],
|
|
17353
17556
|
healthPort: topology.relays.healthPort,
|
|
17354
17557
|
routedTcpPorts: [...topology.relays.routedTcpPorts],
|
|
@@ -17591,6 +17794,20 @@ if (import.meta.main) {
|
|
|
17591
17794
|
console.log("project release shape verified");
|
|
17592
17795
|
process.exit(0);
|
|
17593
17796
|
}
|
|
17797
|
+
if (command3 === "database-auth-verify") {
|
|
17798
|
+
const values = args.filter((arg) => arg.startsWith("--endpoint="));
|
|
17799
|
+
if (values.length !== 1 || args.length !== 2) {
|
|
17800
|
+
throw new Error("database-auth-verify requires exactly one --endpoint coordinate");
|
|
17801
|
+
}
|
|
17802
|
+
const directory = process.env.CREDENTIALS_DIRECTORY;
|
|
17803
|
+
if (!directory?.startsWith("/"))
|
|
17804
|
+
throw new Error("database-auth-verify requires a systemd credential");
|
|
17805
|
+
await verifyDatabaseAuthentication(values[0].slice("--endpoint=".length), {
|
|
17806
|
+
readCredential: () => readFileSync20(join12(directory, "arangodb-root-password"), "utf8").trim()
|
|
17807
|
+
});
|
|
17808
|
+
console.log("[database] anonymous and empty-root access refused; sealed root credential verified");
|
|
17809
|
+
process.exit(0);
|
|
17810
|
+
}
|
|
17594
17811
|
const allowFileSeed = process.env.FZ_ALLOW_FILE_SEED === "1";
|
|
17595
17812
|
if (command3 === "identity") {
|
|
17596
17813
|
const seed = configuredAgentSeed({
|