From acb8d3da8852746676eaf8a806569b37d31ff854 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 22:53:28 +0200 Subject: [PATCH] whole-mesh-full: the vault before the modules that keep secrets in it; no operator root or app secrets delivered (ADRs 0094, 0098) --- test/integration/whole-mesh-full.test.ts | 67 +++--------------------- 1 file changed, 7 insertions(+), 60 deletions(-) diff --git a/test/integration/whole-mesh-full.test.ts b/test/integration/whole-mesh-full.test.ts index 17d1dbf..a73493c 100644 --- a/test/integration/whole-mesh-full.test.ts +++ b/test/integration/whole-mesh-full.test.ts @@ -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> { 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 { - 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 = {}; 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"));