The route-forwarding bed installs the catalogue's authority, proxy and consumer; the whole-mesh bed installs the vault first #46
@@ -179,6 +179,9 @@ const NOVOX: Mod[] = [
|
||||
{ name: "mongodb", containers: ["mongo", "mesh-mongodb"] },
|
||||
{ name: "mssql", containers: ["mssql", "mesh-mssql"] },
|
||||
{ name: "lavinmq", containers: ["lavinmq", "mesh-lavinmq"] },
|
||||
// The vault, before everything that keeps a secret from it: gitea, umami, influxdb, mailu
|
||||
// require one (novox/hq ADRs 0085, 0094).
|
||||
{ name: "mesh-vault", containers: ["mesh-vault"] },
|
||||
// ADR 0066: the proxy now REQUIRES an `acme-ca`, so the bed must assign a provider of one or
|
||||
// route-proxy is unresolvable and takes every routed module down with it. step-ca is that
|
||||
// provider, on the anchor, at mesh scope.
|
||||
@@ -306,14 +309,11 @@ const CREDENTIALS: { node: string; module: string; name: string; crash: string }
|
||||
{ node: "ace", module: "home-assistant", name: "token", crash: "no Home Assistant token" },
|
||||
{ node: "ace", module: "nzbget", name: "password", crash: "NZBGet not configured" },
|
||||
{ node: "ace", module: "qbittorrent", name: "password", crash: "qBittorrent not configured" },
|
||||
{ node: "novox", module: "umami", name: "admin", crash: "admin password is not set" },
|
||||
];
|
||||
|
||||
/** Operator secrets for the credential modules that own-secret their whole app (mailu, de-spiegel). */
|
||||
/** Operator secrets for the credential modules that own-secret their whole app (de-spiegel,
|
||||
* amqp-email-forwarder). mailu's and umami's are kept in the vault now (ADR 0094), not delivered. */
|
||||
const OPERATOR_SECRETS: { node: string; module: string; name: string; value: string }[] = [
|
||||
{ node: "novox", module: "mailu", name: "secret-key", value: "0123456789abcdef0123456789abcdef" },
|
||||
{ node: "novox", module: "mailu", name: "admin", value: "MailuAdminFakePass123" },
|
||||
{ node: "novox", module: "mailu", name: "api-token", value: "mailuapitokenfake0123456789abcd" },
|
||||
{ node: "novox", module: "de-spiegel", name: "smtp-user", value: "despiegel-smtp-fake" },
|
||||
{ node: "novox", module: "de-spiegel", name: "smtp-pass", value: "despiegel-pass-fake" },
|
||||
{ node: "novox", module: "amqp-email-forwarder", name: "smtp-user", value: "eef-smtp-fake" },
|
||||
@@ -410,56 +410,8 @@ async function psMapOf(node: string): Promise<Map<string, string>> {
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* ADR 0066: the internal CA is initialised FROM AN OPERATOR'S ROOT — it does not mint its own.
|
||||
*
|
||||
* So the bed has to be an operator. The material is made on the anchor with openssl and handed to
|
||||
* the mesh through the real `secret accept` path, exactly as a person would: the mesh cannot invent
|
||||
* a PEM, and the random 32 bytes it makes for an own-secret nobody supplied would leave step-ca
|
||||
* crash-looping on a root key that is not a key.
|
||||
*/
|
||||
async function deliverCaRoot(): Promise<boolean> {
|
||||
const made = await on(CONTROL, [
|
||||
"set -e",
|
||||
"mkdir -p /tmp/ca && cd /tmp/ca",
|
||||
// No trailing newline on a password file: step-ca reads the file as the password itself.
|
||||
"openssl rand -hex 16 | tr -d '\\n' > key-password",
|
||||
"openssl ecparam -genkey -name prime256v1 -out root.unenc",
|
||||
"openssl ec -in root.unenc -aes256 -passout file:key-password -out root.key",
|
||||
"rm -f root.unenc",
|
||||
"openssl req -x509 -new -key root.key -passin file:key-password -sha256 -days 3650" +
|
||||
` -out root.crt -subj "/CN=Mesh Internal CA/O=Novox Mesh Lab"`,
|
||||
// Readable by the control plane, which is not root. Its image is FROM scratch and runs as
|
||||
// 65534, and `docker cp` keeps the ownership and mode a file had outside — openssl writes a
|
||||
// private key 0600 root-owned, so the copy landed unreadable and `secret accept` failed with
|
||||
// `open /ca-root-key: permission denied`. The CA then crash-looped on a root it never got.
|
||||
// Chowning it inside the container is not available: there is no shell in there to do it with.
|
||||
//
|
||||
// Safe here and nowhere else: these three exist for the seconds between being written and
|
||||
// being sealed to the machine, on a lab node, for a CA thrown away with the scenario.
|
||||
"chmod 0644 /tmp/ca/root.crt /tmp/ca/root.key /tmp/ca/key-password",
|
||||
"docker cp /tmp/ca/root.crt mesh-controller:/ca-root-cert",
|
||||
"docker cp /tmp/ca/root.key mesh-controller:/ca-root-key",
|
||||
"docker cp /tmp/ca/key-password mesh-controller:/ca-root-key-password",
|
||||
].join("\n"), 180_000);
|
||||
if (!made.ok) {
|
||||
console.log(`CA ROOT NOT MADE on ${CONTROL}:\n${made.out.split("\n").slice(-8).join("\n")}`);
|
||||
return false;
|
||||
}
|
||||
for (const [name, file] of [
|
||||
["root-cert", "/ca-root-cert"],
|
||||
["root-key", "/ca-root-key"],
|
||||
["root-key-password", "/ca-root-key-password"],
|
||||
] as const) {
|
||||
try {
|
||||
await mesh(`secret accept ${CONTROL} step-ca ${name} --from ${file}`);
|
||||
} catch (err) {
|
||||
console.log(`CA ROOT ACCEPT FAILED (${name}): ${(err as Error).message.split("\n").slice(0, 3).join(" | ")}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// ADR 0098: the internal authority makes its own root at first start and serves it; the proxy
|
||||
// fetches it. No operator root is delivered — the mesh mints only the authority's password.
|
||||
|
||||
/** What a module's manifest says its route label is, or "" if it contributes no route. */
|
||||
function routeLabelOf(name: string): string {
|
||||
@@ -889,10 +841,6 @@ test("the full mesh forms across the access point and both server sets converge"
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0066: the CA's root, before the push that would otherwise deliver a random 32 bytes for it.
|
||||
const caRootDelivered = assigned["novox"]!.has("step-ca") ? await deliverCaRoot() : false;
|
||||
if (!caRootDelivered) console.log("ADR 0066: no operator root delivered; step-ca cannot initialise.");
|
||||
|
||||
// ONE push per node (workstations first — cheap — then the heavy service nodes).
|
||||
const pushError: Record<string, string> = {};
|
||||
for (const node of ["shanks", "g14", "novox", "ace"]) {
|
||||
@@ -1027,7 +975,6 @@ test("the full mesh forms across the access point and both server sets converge"
|
||||
? (await on("novox", `curl -s -o /dev/null -w '%{http_code}' --max-time 10 -H 'Host: ${probe.name}' http://127.0.0.1/`)).out.trim()
|
||||
: "";
|
||||
adr.push(`\n proxy answers for ${probe?.name ?? "(nothing composed)"}: ${servedCode || "no answer"}`);
|
||||
adr.push(` operator root delivered to step-ca: ${caRootDelivered}`);
|
||||
adr.push(` step-ca container: ${psMaps["novox"]?.get("step-ca") ?? "MISSING"}`);
|
||||
console.log(adr.join("\n"));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user