@forgezero/agent 0.1.102 → 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.d.ts +4 -0
- package/dist/bootstrap.js +126 -9
- 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 +313 -97
- package/dist/metal-bootstrap.d.ts +8 -0
- package/dist/metal-bootstrap.js +10 -3
- package/dist/operator-bootstrap.d.ts +10 -0
- package/dist/operator-bootstrap.js +181 -12
- package/dist/platform-bootstrap-runtime.d.ts +21 -1
- package/dist/platform-bootstrap-runtime.js +20 -1
- package/dist/platform-fleet-verification.js +149 -28
- package/dist/platform-genesis-config.d.ts +1 -1
- package/dist/platform-launch-env.d.ts +19 -0
- package/dist/platform-launch-profile.d.ts +5 -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
|
@@ -1869,7 +1869,7 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
|
|
|
1869
1869
|
// src/deployment-connectivity.ts
|
|
1870
1870
|
import { createHash as createHash3 } from "node:crypto";
|
|
1871
1871
|
import { mkdirSync as mkdirSync4, renameSync as renameSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1872
|
-
import { createConnection } from "node:net";
|
|
1872
|
+
import { createConnection, isIP } from "node:net";
|
|
1873
1873
|
import { dirname as dirname5 } from "node:path";
|
|
1874
1874
|
|
|
1875
1875
|
// src/process-input.ts
|
|
@@ -1956,21 +1956,99 @@ function validate(request) {
|
|
|
1956
1956
|
}
|
|
1957
1957
|
} else if (request.capabilities.private)
|
|
1958
1958
|
throw new Error("unexpected private deployment capability");
|
|
1959
|
+
if (request.firewallPolicy) {
|
|
1960
|
+
const policy = request.firewallPolicy;
|
|
1961
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(policy.generation) || !Array.isArray(policy.rules) || policy.rules.length > 256) {
|
|
1962
|
+
throw new Error("deployment firewall policy is malformed");
|
|
1963
|
+
}
|
|
1964
|
+
let expanded = 0;
|
|
1965
|
+
for (const rule of policy.rules) {
|
|
1966
|
+
expanded += rule.sourceAddresses.length;
|
|
1967
|
+
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) => isIP(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) {
|
|
1968
|
+
throw new Error("deployment firewall policy is malformed");
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
if (expanded > 2048)
|
|
1972
|
+
throw new Error("deployment firewall policy expands beyond its rule limit");
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
async function replaceTaggedUfwRules(host, comment) {
|
|
1976
|
+
const status = await checked2(host, ["/usr/sbin/ufw", "status", "numbered"], "firewall inventory");
|
|
1977
|
+
const numbers = status.split(`
|
|
1978
|
+
`).flatMap((line) => {
|
|
1979
|
+
if (!line.includes(comment))
|
|
1980
|
+
return [];
|
|
1981
|
+
const match = line.match(/^\s*\[\s*(\d{1,6})\]/);
|
|
1982
|
+
return match ? [Number(match[1])] : [];
|
|
1983
|
+
}).filter((value) => Number.isSafeInteger(value) && value > 0).sort((left, right) => right - left);
|
|
1984
|
+
for (const number of numbers)
|
|
1985
|
+
await checked2(host, ["/usr/sbin/ufw", "--force", "delete", String(number)], `stale firewall rule ${number}`);
|
|
1959
1986
|
}
|
|
1960
1987
|
async function applyDeploymentConnectivity(request, host = defaultHost) {
|
|
1961
1988
|
validate(request);
|
|
1962
1989
|
const id = idFor(request.key);
|
|
1963
1990
|
const evidence = { key: request.key };
|
|
1991
|
+
if (request.firewallPolicy) {
|
|
1992
|
+
const policy = request.firewallPolicy;
|
|
1993
|
+
const comment = `fz-policy-${id}`;
|
|
1994
|
+
await replaceTaggedUfwRules(host, comment);
|
|
1995
|
+
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));
|
|
1996
|
+
const commands = [];
|
|
1997
|
+
for (const rule of ordered)
|
|
1998
|
+
for (const source of rule.sourceAddresses)
|
|
1999
|
+
commands.push([
|
|
2000
|
+
"/usr/sbin/ufw",
|
|
2001
|
+
"insert",
|
|
2002
|
+
"1",
|
|
2003
|
+
rule.action,
|
|
2004
|
+
"from",
|
|
2005
|
+
source,
|
|
2006
|
+
"to",
|
|
2007
|
+
"any",
|
|
2008
|
+
"port",
|
|
2009
|
+
rule.portFrom === rule.portTo ? String(rule.portFrom) : `${rule.portFrom}:${rule.portTo}`,
|
|
2010
|
+
"proto",
|
|
2011
|
+
rule.protocol,
|
|
2012
|
+
"comment",
|
|
2013
|
+
comment
|
|
2014
|
+
]);
|
|
2015
|
+
const ranges = new Map;
|
|
2016
|
+
for (const rule of ordered)
|
|
2017
|
+
ranges.set(`${rule.protocol}:${rule.portFrom}:${rule.portTo}`, rule);
|
|
2018
|
+
for (const range of ranges.values())
|
|
2019
|
+
commands.push([
|
|
2020
|
+
"/usr/sbin/ufw",
|
|
2021
|
+
"insert",
|
|
2022
|
+
"1",
|
|
2023
|
+
"deny",
|
|
2024
|
+
"to",
|
|
2025
|
+
"any",
|
|
2026
|
+
"port",
|
|
2027
|
+
range.portFrom === range.portTo ? String(range.portFrom) : `${range.portFrom}:${range.portTo}`,
|
|
2028
|
+
"proto",
|
|
2029
|
+
range.protocol,
|
|
2030
|
+
"comment",
|
|
2031
|
+
comment
|
|
2032
|
+
]);
|
|
2033
|
+
for (const command2 of commands.toReversed())
|
|
2034
|
+
await checked2(host, command2, "deployment firewall rule");
|
|
2035
|
+
evidence.firewall = { generation: policy.generation, rules: commands.length, active: true };
|
|
2036
|
+
}
|
|
1964
2037
|
const topology = request.topology;
|
|
1965
2038
|
if (topology) {
|
|
1966
|
-
const values = [
|
|
2039
|
+
const values = [
|
|
2040
|
+
topology.localRelayAddress,
|
|
2041
|
+
...topology.localPeerAddresses,
|
|
2042
|
+
...topology.remoteRelayAddresses,
|
|
2043
|
+
...topology.remoteMemberAddresses
|
|
2044
|
+
];
|
|
1967
2045
|
const identities = [
|
|
1968
2046
|
topology.nodeIdentity,
|
|
1969
2047
|
...topology.localPeerIdentities,
|
|
1970
2048
|
...topology.remoteRelayIdentities,
|
|
1971
2049
|
...topology.memberIdentities
|
|
1972
2050
|
];
|
|
1973
|
-
if (!/^[A-Za-z0-9.-]{1,253}$/.test(topology.site) || !/^(?:\d{1,3}\.){3}0\/24$/.test(topology.siteCidr) || values.some((value) =>
|
|
2051
|
+
if (!/^[A-Za-z0-9.-]{1,253}$/.test(topology.site) || !/^(?:\d{1,3}\.){3}0\/24$/.test(topology.siteCidr) || values.some((value) => isIP(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") {
|
|
1974
2052
|
throw new Error("deployment topology is malformed");
|
|
1975
2053
|
}
|
|
1976
2054
|
if (request.intent.private?.mode === "cloudflare-warp" !== (topology.transport === "cloudflare-warp")) {
|
|
@@ -2097,28 +2175,46 @@ ${forwarding}${noOp}${starts}${starts ? `
|
|
|
2097
2175
|
[Install]
|
|
2098
2176
|
WantedBy=multi-user.target
|
|
2099
2177
|
`, 420);
|
|
2100
|
-
|
|
2178
|
+
const firewallComment = `fz-topology-${id}`;
|
|
2179
|
+
await replaceTaggedUfwRules(host, firewallComment);
|
|
2180
|
+
for (const source of [...new Set([...topology.localPeerAddresses, ...topology.remoteMemberAddresses])]) {
|
|
2101
2181
|
for (const port of topology.routedTcpPorts) {
|
|
2102
|
-
await checked2(host, [
|
|
2182
|
+
await checked2(host, [
|
|
2183
|
+
"/usr/sbin/ufw",
|
|
2184
|
+
"allow",
|
|
2185
|
+
"from",
|
|
2186
|
+
source,
|
|
2187
|
+
"to",
|
|
2188
|
+
"any",
|
|
2189
|
+
"port",
|
|
2190
|
+
String(port),
|
|
2191
|
+
"proto",
|
|
2192
|
+
"tcp",
|
|
2193
|
+
"comment",
|
|
2194
|
+
firewallComment
|
|
2195
|
+
], `private service ${source}:${port}`);
|
|
2103
2196
|
}
|
|
2104
2197
|
}
|
|
2105
2198
|
if (topology.role !== "member")
|
|
2106
|
-
for (const
|
|
2107
|
-
for (const
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2199
|
+
for (const remoteAddress of topology.remoteMemberAddresses) {
|
|
2200
|
+
for (const source of topology.localPeerAddresses)
|
|
2201
|
+
for (const port of topology.routedTcpPorts) {
|
|
2202
|
+
await checked2(host, [
|
|
2203
|
+
"/usr/sbin/ufw",
|
|
2204
|
+
"route",
|
|
2205
|
+
"allow",
|
|
2206
|
+
"proto",
|
|
2207
|
+
"tcp",
|
|
2208
|
+
"from",
|
|
2209
|
+
source,
|
|
2210
|
+
"to",
|
|
2211
|
+
remoteAddress,
|
|
2212
|
+
"port",
|
|
2213
|
+
String(port),
|
|
2214
|
+
"comment",
|
|
2215
|
+
firewallComment
|
|
2216
|
+
], `private routed service ${source}->${remoteAddress}:${port}`);
|
|
2217
|
+
}
|
|
2122
2218
|
}
|
|
2123
2219
|
await checked2(host, ["/usr/bin/systemctl", "daemon-reload"], "topology daemon reload");
|
|
2124
2220
|
await checked2(host, ["/usr/bin/systemctl", "enable", "--now", routeUnit], "topology routes");
|
|
@@ -2908,7 +3004,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
2908
3004
|
}
|
|
2909
3005
|
|
|
2910
3006
|
// src/version.ts
|
|
2911
|
-
var VERSION3 = "0.1.
|
|
3007
|
+
var VERSION3 = "0.1.107";
|
|
2912
3008
|
|
|
2913
3009
|
// src/egress-policy.ts
|
|
2914
3010
|
import { realpathSync as realpathSync3 } from "node:fs";
|
|
@@ -2971,7 +3067,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
|
|
|
2971
3067
|
}
|
|
2972
3068
|
|
|
2973
3069
|
// src/provision.ts
|
|
2974
|
-
import { isIP } from "node:net";
|
|
3070
|
+
import { isIP as isIP2 } from "node:net";
|
|
2975
3071
|
function atLeast(version, floor) {
|
|
2976
3072
|
const parse = (value) => (value.trim().replace(/^v/, "").match(/\d+/g) ?? []).slice(0, 3).map(Number);
|
|
2977
3073
|
const got = parse(version);
|
|
@@ -3451,7 +3547,7 @@ function agentUnit(options) {
|
|
|
3451
3547
|
throw new Error("compute telemetry endpoint must be an absolute collector URL");
|
|
3452
3548
|
}
|
|
3453
3549
|
const localCollector = endpoint.protocol === "http:" && endpoint.hostname === "127.0.0.1" && endpoint.port === "4318" && endpoint.pathname === "/";
|
|
3454
|
-
const publicCollector = endpoint.protocol === "https:" && !endpoint.username && !endpoint.password && !endpoint.search && !endpoint.hash &&
|
|
3550
|
+
const publicCollector = endpoint.protocol === "https:" && !endpoint.username && !endpoint.password && !endpoint.search && !endpoint.hash && isIP2(endpoint.hostname) === 0 && endpoint.hostname.includes(".") && endpoint.hostname !== "localhost" && !endpoint.hostname.endsWith(".local");
|
|
3455
3551
|
if (!localCollector && !publicCollector || endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
|
|
3456
3552
|
throw new Error("compute telemetry endpoint must be the supervised loopback collector or credential-free public HTTPS");
|
|
3457
3553
|
}
|
|
@@ -4142,10 +4238,16 @@ var httpsOrigin = (name, raw) => {
|
|
|
4142
4238
|
};
|
|
4143
4239
|
var agentTelemetryOrigin = (raw) => raw === "http://127.0.0.1:4318" ? raw : httpsOrigin("agentOtlpEndpoint", raw);
|
|
4144
4240
|
function validatePlatformInitialInventory(value) {
|
|
4145
|
-
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "region", "computes", "attestation", "deployment"].includes(key)) || !/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(value.metalHostname) || !value.region || typeof value.region !== "object" || Array.isArray(value.region) || Object.keys(value.region).some((key) => !["key", "label", "country", "city", "confidentialCapable"].includes(key)) || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.region.key) || !/^[A-Z]{2}$/.test(value.region.country) || value.region.confidentialCapable !== true || !Array.isArray(value.computes)) {
|
|
4241
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "metalIdentity", "region", "computes", "attestation", "deployment"].includes(key)) || !/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(value.metalHostname) || !value.region || typeof value.region !== "object" || Array.isArray(value.region) || Object.keys(value.region).some((key) => !["key", "label", "country", "city", "confidentialCapable"].includes(key)) || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.region.key) || !/^[A-Z]{2}$/.test(value.region.country) || value.region.confidentialCapable !== true || !Array.isArray(value.computes)) {
|
|
4146
4242
|
throw new Error("initial platform inventory coordinates are invalid");
|
|
4147
4243
|
}
|
|
4148
4244
|
safeAtom("initialInventory.region.label", value.region.label);
|
|
4245
|
+
if (value.metalIdentity !== undefined) {
|
|
4246
|
+
const identity = value.metalIdentity;
|
|
4247
|
+
if (!identity || typeof identity !== "object" || Array.isArray(identity) || Object.keys(identity).some((key) => !["nodeKey", "publicKeys"].includes(key)) || !/^[A-Za-z0-9_-]{43}$/.test(identity.nodeKey) || !identity.publicKeys || identity.publicKeys.ed25519 !== identity.nodeKey || !/^[A-Za-z0-9_-]{1,8192}$/.test(identity.publicKeys.mlDsa) || Object.keys(identity.publicKeys).some((key) => !["ed25519", "mlDsa"].includes(key))) {
|
|
4248
|
+
throw new Error("initial platform Metal identity is invalid");
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4149
4251
|
if (value.region.city !== undefined)
|
|
4150
4252
|
safeAtom("initialInventory.region.city", value.region.city);
|
|
4151
4253
|
if (value.deployment !== undefined && (!value.deployment || typeof value.deployment !== "object" || Array.isArray(value.deployment) || Object.keys(value.deployment).some((key) => !["source", "branch", "revision", "bundleSha256"].includes(key)) || value.deployment.source !== "bootstrap-bundle" || !["dev", "main"].includes(value.deployment.branch) || !/^[a-f0-9]{40}$/.test(value.deployment.revision) || !/^[a-f0-9]{64}$/.test(value.deployment.bundleSha256)))
|
|
@@ -4180,6 +4282,7 @@ function validatePlatformInitialInventory(value) {
|
|
|
4180
4282
|
}
|
|
4181
4283
|
return {
|
|
4182
4284
|
metalHostname: value.metalHostname,
|
|
4285
|
+
...value.metalIdentity ? { metalIdentity: structuredClone(value.metalIdentity) } : {},
|
|
4183
4286
|
region: { ...value.region },
|
|
4184
4287
|
computes: value.computes.map((compute, index) => ({
|
|
4185
4288
|
...computes[index],
|
|
@@ -4233,6 +4336,11 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
4233
4336
|
} else if (input.email !== undefined) {
|
|
4234
4337
|
throw new Error("Bootstrap email provider must be smtp or jetemail.");
|
|
4235
4338
|
}
|
|
4339
|
+
if (input.githubApp) {
|
|
4340
|
+
if (!/^(?:Iv1\.[A-Fa-f0-9]{16}|Ov23li[A-Za-z0-9]{14,})$/.test(input.githubApp.clientId) || !/^[1-9][0-9]{0,19}$/.test(input.githubApp.appId) || !/^[a-z0-9][a-z0-9-]{0,99}$/.test(input.githubApp.slug)) {
|
|
4341
|
+
throw new Error("GitHub App client id, app id or slug is malformed.");
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4236
4344
|
boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
|
|
4237
4345
|
if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
|
|
4238
4346
|
throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
|
|
@@ -4363,6 +4471,9 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
4363
4471
|
FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
|
|
4364
4472
|
FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
|
|
4365
4473
|
FZ_REALTIME_PRODUCER: value.realtime?.producer ?? "",
|
|
4474
|
+
FZ_GITHUB_CLIENT_ID: value.githubApp?.clientId ?? "",
|
|
4475
|
+
FZ_GITHUB_APP_ID: value.githubApp?.appId ?? "",
|
|
4476
|
+
FZ_GITHUB_APP_SLUG: value.githubApp?.slug ?? "",
|
|
4366
4477
|
FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
|
|
4367
4478
|
};
|
|
4368
4479
|
return `# Generated by fz bootstrap platform. Non-secret coordinates only.
|
|
@@ -4374,6 +4485,9 @@ function platformApiCredentialSpecs(options) {
|
|
|
4374
4485
|
const optional = [
|
|
4375
4486
|
["fz_smtp.password", options.emailProvider === "smtp"],
|
|
4376
4487
|
["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
|
|
4488
|
+
["fz_github.clientSecret", options.githubApp],
|
|
4489
|
+
["fz_github.privateKey", options.githubApp],
|
|
4490
|
+
["fz_github.webhookSecret", options.githubApp],
|
|
4377
4491
|
["CF_API_TOKEN", options.cloudflareKv],
|
|
4378
4492
|
["CF_TUNNEL_TOKEN", options.cloudflareKv],
|
|
4379
4493
|
["REALTIME_PUBLISH_SECRET", options.realtime],
|
|
@@ -4381,6 +4495,7 @@ function platformApiCredentialSpecs(options) {
|
|
|
4381
4495
|
];
|
|
4382
4496
|
return [
|
|
4383
4497
|
{ name: "arangodb-jwt", encryptedPath: "/etc/forgezero/creds/arangodb-jwt.cred", required: true },
|
|
4498
|
+
{ name: "arangodb-root-password", encryptedPath: "/etc/forgezero/creds/arangodb-root-password.cred", required: true },
|
|
4384
4499
|
{ name: "seed-sync-root", encryptedPath: "/etc/forgezero/creds/seed-sync-root.cred", required: true },
|
|
4385
4500
|
...optional.filter(([, present]) => present).map(([name]) => ({
|
|
4386
4501
|
name,
|
|
@@ -4727,7 +4842,7 @@ function planLocalOtlpProof(endpoint, collectorUnit) {
|
|
|
4727
4842
|
import { lstatSync as lstatSync5 } from "node:fs";
|
|
4728
4843
|
|
|
4729
4844
|
// src/bootstrap.ts
|
|
4730
|
-
import { createHash as createHash7, createHmac as createHmac2, randomBytes as randomBytes3 } from "node:crypto";
|
|
4845
|
+
import { createHash as createHash7, createHmac as createHmac2, createPrivateKey, randomBytes as randomBytes3 } from "node:crypto";
|
|
4731
4846
|
import {
|
|
4732
4847
|
chmodSync as chmodSync9,
|
|
4733
4848
|
existsSync as existsSync11,
|
|
@@ -5060,10 +5175,10 @@ import { constants } from "node:fs";
|
|
|
5060
5175
|
import { createHmac, randomUUID as randomUUID4 } from "node:crypto";
|
|
5061
5176
|
import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "node:fs/promises";
|
|
5062
5177
|
import { dirname as dirname10, join as join6, resolve as resolve5 } from "node:path";
|
|
5063
|
-
import { isIP as
|
|
5178
|
+
import { isIP as isIP4 } from "node:net";
|
|
5064
5179
|
|
|
5065
5180
|
// src/cloudflare-edge.ts
|
|
5066
|
-
import { isIP as
|
|
5181
|
+
import { isIP as isIP3 } from "node:net";
|
|
5067
5182
|
|
|
5068
5183
|
// src/otel-collector.ts
|
|
5069
5184
|
var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
|
|
@@ -5128,6 +5243,7 @@ var BOOTSTRAP_STATE_PATH = "/var/lib/forgezero/bootstrap.json";
|
|
|
5128
5243
|
var STATE_PATH = BOOTSTRAP_STATE_PATH;
|
|
5129
5244
|
var CREDS = "/etc/forgezero/creds";
|
|
5130
5245
|
var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
|
|
5246
|
+
var ARANGO_ROOT_CREDENTIAL = `${CREDS}/arangodb-root-password.cred`;
|
|
5131
5247
|
var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
|
|
5132
5248
|
var TUNNEL_CREDENTIAL = `${CREDS}/CF_TUNNEL_CONNECTOR_TOKEN.cred`;
|
|
5133
5249
|
var CF_API_CREDENTIAL = `${CREDS}/CF_API_TOKEN.cred`;
|
|
@@ -5289,6 +5405,11 @@ async function bootstrapStatus(host = localBootstrapHost()) {
|
|
|
5289
5405
|
if (nginx3.exitCode !== 0)
|
|
5290
5406
|
problems.push("nginx configuration is invalid");
|
|
5291
5407
|
if (state.databaseRole !== "none") {
|
|
5408
|
+
for (const credential of [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL]) {
|
|
5409
|
+
services[credential] = host.exists(credential);
|
|
5410
|
+
if (!services[credential])
|
|
5411
|
+
problems.push(`${credential} is missing`);
|
|
5412
|
+
}
|
|
5292
5413
|
const unitPath = "/etc/systemd/system/forgezero-db.service";
|
|
5293
5414
|
const expectsNoAgency = state.databaseAgency === "none";
|
|
5294
5415
|
const unitHasNoAgency = host.exists(unitPath) && host.read(unitPath).includes("--cluster.start-agent=false");
|
|
@@ -15,4 +15,4 @@ export declare function platformGenesisBootstrapConfigs(template: PlatformBootst
|
|
|
15
15
|
branch: 'dev' | 'main';
|
|
16
16
|
revision: string;
|
|
17
17
|
bundleSha256: string;
|
|
18
|
-
}, attestation?: import('./platform-bootstrap-runtime').PlatformInitialInventory['attestation']): readonly PlatformBootstrapConfig[];
|
|
18
|
+
}, attestation?: import('./platform-bootstrap-runtime').PlatformInitialInventory['attestation'], metalIdentity?: NonNullable<import('./platform-bootstrap-runtime').PlatformInitialInventory['metalIdentity']>): readonly PlatformBootstrapConfig[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ForgeZeroLaunchOwnerInput } from './platform-launch-profile';
|
|
2
|
+
export interface PlatformLaunchEnvironmentInput {
|
|
3
|
+
owner: ForgeZeroLaunchOwnerInput;
|
|
4
|
+
emailSecret: string;
|
|
5
|
+
cloudflareTunnelToken: string;
|
|
6
|
+
cloudflareApiToken: string;
|
|
7
|
+
githubClientSecret?: string;
|
|
8
|
+
githubPrivateKeyBase64?: string;
|
|
9
|
+
githubWebhookSecret?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Read the development-only attended-launch adapter.
|
|
13
|
+
*
|
|
14
|
+
* The file may contain the API's ordinary non-secret environment as well; this
|
|
15
|
+
* reader selects only the fixed bootstrap coordinates below. Secret values are
|
|
16
|
+
* returned to the caller for immediate systemd-creds sealing and are never
|
|
17
|
+
* copied into the API environment rendered on a target.
|
|
18
|
+
*/
|
|
19
|
+
export declare function readPlatformLaunchEnvironment(path: string): PlatformLaunchEnvironmentInput;
|
|
@@ -45,6 +45,11 @@ export interface ForgeZeroLaunchOwnerInput {
|
|
|
45
45
|
from: string;
|
|
46
46
|
eu: boolean;
|
|
47
47
|
};
|
|
48
|
+
githubApp?: {
|
|
49
|
+
clientId: string;
|
|
50
|
+
appId: string;
|
|
51
|
+
slug: string;
|
|
52
|
+
};
|
|
48
53
|
}
|
|
49
54
|
/**
|
|
50
55
|
* ForgeZero's own genesis public coordinates. They are reviewed source data,
|
package/dist/provision.js
CHANGED
|
@@ -1869,7 +1869,7 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
|
|
|
1869
1869
|
// src/deployment-connectivity.ts
|
|
1870
1870
|
import { createHash as createHash3 } from "node:crypto";
|
|
1871
1871
|
import { mkdirSync as mkdirSync4, renameSync as renameSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1872
|
-
import { createConnection } from "node:net";
|
|
1872
|
+
import { createConnection, isIP } from "node:net";
|
|
1873
1873
|
import { dirname as dirname5 } from "node:path";
|
|
1874
1874
|
|
|
1875
1875
|
// src/process-input.ts
|
|
@@ -1956,21 +1956,99 @@ function validate(request) {
|
|
|
1956
1956
|
}
|
|
1957
1957
|
} else if (request.capabilities.private)
|
|
1958
1958
|
throw new Error("unexpected private deployment capability");
|
|
1959
|
+
if (request.firewallPolicy) {
|
|
1960
|
+
const policy = request.firewallPolicy;
|
|
1961
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(policy.generation) || !Array.isArray(policy.rules) || policy.rules.length > 256) {
|
|
1962
|
+
throw new Error("deployment firewall policy is malformed");
|
|
1963
|
+
}
|
|
1964
|
+
let expanded = 0;
|
|
1965
|
+
for (const rule of policy.rules) {
|
|
1966
|
+
expanded += rule.sourceAddresses.length;
|
|
1967
|
+
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) => isIP(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) {
|
|
1968
|
+
throw new Error("deployment firewall policy is malformed");
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
if (expanded > 2048)
|
|
1972
|
+
throw new Error("deployment firewall policy expands beyond its rule limit");
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
async function replaceTaggedUfwRules(host, comment) {
|
|
1976
|
+
const status = await checked2(host, ["/usr/sbin/ufw", "status", "numbered"], "firewall inventory");
|
|
1977
|
+
const numbers = status.split(`
|
|
1978
|
+
`).flatMap((line) => {
|
|
1979
|
+
if (!line.includes(comment))
|
|
1980
|
+
return [];
|
|
1981
|
+
const match = line.match(/^\s*\[\s*(\d{1,6})\]/);
|
|
1982
|
+
return match ? [Number(match[1])] : [];
|
|
1983
|
+
}).filter((value) => Number.isSafeInteger(value) && value > 0).sort((left, right) => right - left);
|
|
1984
|
+
for (const number of numbers)
|
|
1985
|
+
await checked2(host, ["/usr/sbin/ufw", "--force", "delete", String(number)], `stale firewall rule ${number}`);
|
|
1959
1986
|
}
|
|
1960
1987
|
async function applyDeploymentConnectivity(request, host = defaultHost) {
|
|
1961
1988
|
validate(request);
|
|
1962
1989
|
const id = idFor(request.key);
|
|
1963
1990
|
const evidence = { key: request.key };
|
|
1991
|
+
if (request.firewallPolicy) {
|
|
1992
|
+
const policy = request.firewallPolicy;
|
|
1993
|
+
const comment = `fz-policy-${id}`;
|
|
1994
|
+
await replaceTaggedUfwRules(host, comment);
|
|
1995
|
+
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));
|
|
1996
|
+
const commands = [];
|
|
1997
|
+
for (const rule of ordered)
|
|
1998
|
+
for (const source of rule.sourceAddresses)
|
|
1999
|
+
commands.push([
|
|
2000
|
+
"/usr/sbin/ufw",
|
|
2001
|
+
"insert",
|
|
2002
|
+
"1",
|
|
2003
|
+
rule.action,
|
|
2004
|
+
"from",
|
|
2005
|
+
source,
|
|
2006
|
+
"to",
|
|
2007
|
+
"any",
|
|
2008
|
+
"port",
|
|
2009
|
+
rule.portFrom === rule.portTo ? String(rule.portFrom) : `${rule.portFrom}:${rule.portTo}`,
|
|
2010
|
+
"proto",
|
|
2011
|
+
rule.protocol,
|
|
2012
|
+
"comment",
|
|
2013
|
+
comment
|
|
2014
|
+
]);
|
|
2015
|
+
const ranges = new Map;
|
|
2016
|
+
for (const rule of ordered)
|
|
2017
|
+
ranges.set(`${rule.protocol}:${rule.portFrom}:${rule.portTo}`, rule);
|
|
2018
|
+
for (const range of ranges.values())
|
|
2019
|
+
commands.push([
|
|
2020
|
+
"/usr/sbin/ufw",
|
|
2021
|
+
"insert",
|
|
2022
|
+
"1",
|
|
2023
|
+
"deny",
|
|
2024
|
+
"to",
|
|
2025
|
+
"any",
|
|
2026
|
+
"port",
|
|
2027
|
+
range.portFrom === range.portTo ? String(range.portFrom) : `${range.portFrom}:${range.portTo}`,
|
|
2028
|
+
"proto",
|
|
2029
|
+
range.protocol,
|
|
2030
|
+
"comment",
|
|
2031
|
+
comment
|
|
2032
|
+
]);
|
|
2033
|
+
for (const command2 of commands.toReversed())
|
|
2034
|
+
await checked2(host, command2, "deployment firewall rule");
|
|
2035
|
+
evidence.firewall = { generation: policy.generation, rules: commands.length, active: true };
|
|
2036
|
+
}
|
|
1964
2037
|
const topology = request.topology;
|
|
1965
2038
|
if (topology) {
|
|
1966
|
-
const values = [
|
|
2039
|
+
const values = [
|
|
2040
|
+
topology.localRelayAddress,
|
|
2041
|
+
...topology.localPeerAddresses,
|
|
2042
|
+
...topology.remoteRelayAddresses,
|
|
2043
|
+
...topology.remoteMemberAddresses
|
|
2044
|
+
];
|
|
1967
2045
|
const identities = [
|
|
1968
2046
|
topology.nodeIdentity,
|
|
1969
2047
|
...topology.localPeerIdentities,
|
|
1970
2048
|
...topology.remoteRelayIdentities,
|
|
1971
2049
|
...topology.memberIdentities
|
|
1972
2050
|
];
|
|
1973
|
-
if (!/^[A-Za-z0-9.-]{1,253}$/.test(topology.site) || !/^(?:\d{1,3}\.){3}0\/24$/.test(topology.siteCidr) || values.some((value) =>
|
|
2051
|
+
if (!/^[A-Za-z0-9.-]{1,253}$/.test(topology.site) || !/^(?:\d{1,3}\.){3}0\/24$/.test(topology.siteCidr) || values.some((value) => isIP(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") {
|
|
1974
2052
|
throw new Error("deployment topology is malformed");
|
|
1975
2053
|
}
|
|
1976
2054
|
if (request.intent.private?.mode === "cloudflare-warp" !== (topology.transport === "cloudflare-warp")) {
|
|
@@ -2097,28 +2175,46 @@ ${forwarding}${noOp}${starts}${starts ? `
|
|
|
2097
2175
|
[Install]
|
|
2098
2176
|
WantedBy=multi-user.target
|
|
2099
2177
|
`, 420);
|
|
2100
|
-
|
|
2178
|
+
const firewallComment = `fz-topology-${id}`;
|
|
2179
|
+
await replaceTaggedUfwRules(host, firewallComment);
|
|
2180
|
+
for (const source of [...new Set([...topology.localPeerAddresses, ...topology.remoteMemberAddresses])]) {
|
|
2101
2181
|
for (const port of topology.routedTcpPorts) {
|
|
2102
|
-
await checked2(host, [
|
|
2182
|
+
await checked2(host, [
|
|
2183
|
+
"/usr/sbin/ufw",
|
|
2184
|
+
"allow",
|
|
2185
|
+
"from",
|
|
2186
|
+
source,
|
|
2187
|
+
"to",
|
|
2188
|
+
"any",
|
|
2189
|
+
"port",
|
|
2190
|
+
String(port),
|
|
2191
|
+
"proto",
|
|
2192
|
+
"tcp",
|
|
2193
|
+
"comment",
|
|
2194
|
+
firewallComment
|
|
2195
|
+
], `private service ${source}:${port}`);
|
|
2103
2196
|
}
|
|
2104
2197
|
}
|
|
2105
2198
|
if (topology.role !== "member")
|
|
2106
|
-
for (const
|
|
2107
|
-
for (const
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2199
|
+
for (const remoteAddress of topology.remoteMemberAddresses) {
|
|
2200
|
+
for (const source of topology.localPeerAddresses)
|
|
2201
|
+
for (const port of topology.routedTcpPorts) {
|
|
2202
|
+
await checked2(host, [
|
|
2203
|
+
"/usr/sbin/ufw",
|
|
2204
|
+
"route",
|
|
2205
|
+
"allow",
|
|
2206
|
+
"proto",
|
|
2207
|
+
"tcp",
|
|
2208
|
+
"from",
|
|
2209
|
+
source,
|
|
2210
|
+
"to",
|
|
2211
|
+
remoteAddress,
|
|
2212
|
+
"port",
|
|
2213
|
+
String(port),
|
|
2214
|
+
"comment",
|
|
2215
|
+
firewallComment
|
|
2216
|
+
], `private routed service ${source}->${remoteAddress}:${port}`);
|
|
2217
|
+
}
|
|
2122
2218
|
}
|
|
2123
2219
|
await checked2(host, ["/usr/bin/systemctl", "daemon-reload"], "topology daemon reload");
|
|
2124
2220
|
await checked2(host, ["/usr/bin/systemctl", "enable", "--now", routeUnit], "topology routes");
|
|
@@ -2908,7 +3004,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
2908
3004
|
}
|
|
2909
3005
|
|
|
2910
3006
|
// src/version.ts
|
|
2911
|
-
var VERSION3 = "0.1.
|
|
3007
|
+
var VERSION3 = "0.1.107";
|
|
2912
3008
|
|
|
2913
3009
|
// src/egress-policy.ts
|
|
2914
3010
|
import { realpathSync as realpathSync3 } from "node:fs";
|
|
@@ -2971,7 +3067,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
|
|
|
2971
3067
|
}
|
|
2972
3068
|
|
|
2973
3069
|
// src/provision.ts
|
|
2974
|
-
import { isIP } from "node:net";
|
|
3070
|
+
import { isIP as isIP2 } from "node:net";
|
|
2975
3071
|
function atLeast(version, floor) {
|
|
2976
3072
|
const parse = (value) => (value.trim().replace(/^v/, "").match(/\d+/g) ?? []).slice(0, 3).map(Number);
|
|
2977
3073
|
const got = parse(version);
|
|
@@ -3451,7 +3547,7 @@ function agentUnit(options) {
|
|
|
3451
3547
|
throw new Error("compute telemetry endpoint must be an absolute collector URL");
|
|
3452
3548
|
}
|
|
3453
3549
|
const localCollector = endpoint.protocol === "http:" && endpoint.hostname === "127.0.0.1" && endpoint.port === "4318" && endpoint.pathname === "/";
|
|
3454
|
-
const publicCollector = endpoint.protocol === "https:" && !endpoint.username && !endpoint.password && !endpoint.search && !endpoint.hash &&
|
|
3550
|
+
const publicCollector = endpoint.protocol === "https:" && !endpoint.username && !endpoint.password && !endpoint.search && !endpoint.hash && isIP2(endpoint.hostname) === 0 && endpoint.hostname.includes(".") && endpoint.hostname !== "localhost" && !endpoint.hostname.endsWith(".local");
|
|
3455
3551
|
if (!localCollector && !publicCollector || endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
|
|
3456
3552
|
throw new Error("compute telemetry endpoint must be the supervised loopback collector or credential-free public HTTPS");
|
|
3457
3553
|
}
|