From 9da2d01ca0e73178ce22ea599911f976d306bc12 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 00:12:55 +0200 Subject: [PATCH] The genesis bed checks the root secrets: made, sealed to the operator key, recoverable V5: the template's password is refused by the store, the operator key and the export sit beside the bundle at 0600, the vault keeps the export, and a person with the key recovers the superuser off the mesh and opens the store with it. V2 dials the broker with the administrator password genesis made. --- test/integration/one-node-mesh.test.ts | 77 +++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/test/integration/one-node-mesh.test.ts b/test/integration/one-node-mesh.test.ts index 7bfe844..354a966 100644 --- a/test/integration/one-node-mesh.test.ts +++ b/test/integration/one-node-mesh.test.ts @@ -109,9 +109,9 @@ const CONTROL_PLANE = { module: "mesh-controller", repo: "mesh-controller", path * is not one. */ const MUST_HOLD = ["mesh-controller", "distribution", "builder", "mesh-tools", "postgres", - "mesh-catalog", "lavinmq", "amqp-ping"]; + "mesh-catalog", "lavinmq", "mesh-vault", "amqp-ping"]; const MUST_RUN = ["mesh-controller", "mesh-registry", "mesh-broker", "mesh-store", - "mesh-postgres", "mesh-catalog", "mesh-lavinmq", "amqp-ping"]; + "mesh-postgres", "mesh-catalog", "mesh-lavinmq", "mesh-vault", "amqp-ping"]; /** Named once, because the step title is also how later steps say what they waited on. */ const NEEDS = "the mesh runs a broker for that module to talk to"; const GENESIS = "a bare machine becomes a mesh of one, raised by the installer"; @@ -133,6 +133,7 @@ const DESCRIBES = "the control plane can describe the mesh, and what it says is const CATALOGUED = "the catalogue holds every module this mesh built"; const NETWORK = "the machine's networking is what the modules asked for"; const DECLARED = "every resource the mesh declared is true on the machine"; +const ROOT_SECRETS = "the root secrets are the mesh's own, sealed to an operator key, and a person can recover them"; const FOLLOWS = "a change to a module's source reaches the machine on its own"; const STORE_UPGRADES = "the store is upgraded in place, and the controller reads it through the window"; const BROKER_UPGRADES = "the broker is upgraded in place, and the mesh talks over the window"; @@ -798,10 +799,13 @@ before(async () => { // broker's loopback, reached by joining its network namespace. const image = (await on(CONTROL, `docker inspect -f '{{.Config.Image}}' mesh-catalog`)).out.trim(); + // The administrator's password is the one genesis made, kept where the lavinmq module's own + // secret lives (novox/hq issue 071) — the image's default no longer opens the broker. + const adminPassword = (await must(CONTROL, `cat /var/lib/lavinmq-module/admin.secret`)).trim(); const ask = async (tool: string, args = "{}") => must(CONTROL, `docker run --rm --network container:mesh-broker ` + - `-e MESH_BROKER_URL=amqp://guest:guest@127.0.0.1:5672/ ` + + `-e MESH_BROKER_URL=amqp://guest:${encodeURIComponent(adminPassword)}@127.0.0.1:5672/ ` + `${image} invoke mesh-catalog ${tool} ${quote(args)}`, 120_000); @@ -961,6 +965,73 @@ before(async () => { ].filter(Boolean).join("\n"); }); + // ---- V5. AND ITS ROOT SECRETS ARE ITS OWN ------------------------------------------------------ + // + // novox/hq ADR 0085 (amended), issue 071. The template raises the store and broker with fixed + // credentials; the installer replaces them with values it made, seals every secret a module + // holds for itself to an operator key it made first, installs the vault to keep those copies, + // and writes the export beside the key. Checked from outside every container — a login over + // loopback inside the store's container is trusted and proves nothing (design 13). + await step("V5", ROOT_SECRETS, DECLARED, async () => { + const said: string[] = []; + const storeImage = (await must(CONTROL, `docker inspect -f '{{.Config.Image}}' mesh-store`)).trim(); + const psql = async (password: string) => + on(CONTROL, + `docker run --rm --network host ${storeImage} psql ` + + `${quote(`postgresql://postgres:${encodeURIComponent(password)}@127.0.0.1:5432/postgres?sslmode=disable`)} -tAc 'select 1'`, + 60_000); + + // 1. The template's password does not open the store. + const stale = await psql("bootstrap"); + assert.ok(!stale.ok || !/^1$/m.test(stale.out), `the template's password still opens the store:\n${stale.out}`); + said.push(` bootstrap refused by the store`); + + // 2. The operator key and the export are beside the bundle, and only root can read them. + for (const f of ["/var/lib/mesh-host/operator.key", "/var/lib/mesh-host/root-secrets.export.json", "/var/lib/mesh-host/foundation.lock"]) { + const mode = (await must(CONTROL, `stat -c %a ${f}`)).trim(); + assert.equal(mode, "600", `${f} is mode ${mode}`); + } + const exported = await must(CONTROL, `cat /var/lib/mesh-host/root-secrets.export.json`); + const doc = JSON.parse(exported) as { kept: { node: string; module: string; name: string; origin: string }[]; unrecoverable?: unknown[] }; + for (const want of [["postgres", "superuser"], ["lavinmq", "admin"], ["mesh-controller", "inventory"]]) { + assert.ok(doc.kept.some((k) => k.module === want[0] && k.name === want[1]), + `the export lacks ${want.join("/")}:\n${doc.kept.map((k) => `${k.module}/${k.name}`).join(" ")}`); + } + const superuser = (await must(CONTROL, `cat /var/lib/postgres/superuser.secret`)).trim(); + const admin = (await must(CONTROL, `cat /var/lib/lavinmq-module/admin.secret`)).trim(); + assert.ok(!exported.includes(superuser) && !exported.includes(admin), "the export holds a plaintext root secret"); + said.push(` exported ${doc.kept.length} secret(s) sealed to the operator key; ${(doc.unrecoverable ?? []).length} not recoverable`); + + // 3. The vault keeps the same export on its own disk. + const kept = await must(CONTROL, `cat /var/lib/mesh-vault/root/export.json`); + assert.ok(kept.includes('"kept"') && !kept.includes(superuser), "the vault's copy is missing or holds plaintext"); + said.push(` vault keeps the export at /var/lib/mesh-vault/root/export.json`); + + // 4. A person with the key recovers the store's superuser from the export alone — off the + // mesh, in a throwaway container with no store or broker in reach — and it opens the store. + // The copies are relaxed to a scratch directory for the test only: the operator's real + // files stay root-owned at 0600 above. + const controllerImage = (await must(CONTROL, `docker inspect -f '{{.Config.Image}}' mesh-controller`)).trim(); + await must(CONTROL, `rm -rf /tmp/operator && mkdir -p /tmp/operator && chmod 777 /tmp/operator && ` + + `cp /var/lib/mesh-host/operator.key /var/lib/mesh-host/root-secrets.export.json /tmp/operator/ && chmod 644 /tmp/operator/*`); + const recover = async (module: string, name: string) => { + await must(CONTROL, + `docker run --rm -v /tmp/operator:/work --entrypoint /mesh-controller ${controllerImage} ` + + `secret recover ${CONTROL} ${module} ${name} --key /work/operator.key --from-export /work/root-secrets.export.json --out /work/${module}.${name}`, + 120_000); + return (await must(CONTROL, `cat /tmp/operator/${module}.${name}`)).replace(/\n$/, ""); + }; + const recoveredSuperuser = await recover("postgres", "superuser"); + assert.equal(recoveredSuperuser, superuser, "the recovered superuser is not the one the store was raised with"); + const opened = await psql(recoveredSuperuser); + assert.ok(opened.ok && /^1$/m.test(opened.out), `the recovered superuser does not open the store:\n${opened.out}`); + said.push(` recovered postgres/superuser with the operator key, and it opens the store`); + const recoveredAdmin = await recover("lavinmq", "admin"); + assert.equal(recoveredAdmin, admin, "the recovered broker admin is not the one genesis made"); + said.push(` recovered lavinmq/admin with the operator key — the broker's, as V2 dialled it`); + return said.join("\n"); + }); + // ---- 11. A CHANGE REACHES THE MACHINE ON ITS OWN ---------------------------------------------- // // The whole point of the mesh, and the capability the migration depends on: move a module's