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.
This commit is contained in:
2026-09-21 00:12:55 +02:00
parent 639175ec4a
commit 9da2d01ca0
+74 -3
View File
@@ -109,9 +109,9 @@ const CONTROL_PLANE = { module: "mesh-controller", repo: "mesh-controller", path
* is not one. * is not one.
*/ */
const MUST_HOLD = ["mesh-controller", "distribution", "builder", "mesh-tools", "postgres", 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", 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. */ /** 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 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"; 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 CATALOGUED = "the catalogue holds every module this mesh built";
const NETWORK = "the machine's networking is what the modules asked for"; 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 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 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 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"; 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. // broker's loopback, reached by joining its network namespace.
const image = (await on(CONTROL, const image = (await on(CONTROL,
`docker inspect -f '{{.Config.Image}}' mesh-catalog`)).out.trim(); `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 = "{}") => const ask = async (tool: string, args = "{}") =>
must(CONTROL, must(CONTROL,
`docker run --rm --network container:mesh-broker ` + `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)}`, `${image} invoke mesh-catalog ${tool} ${quote(args)}`,
120_000); 120_000);
@@ -961,6 +965,73 @@ before(async () => {
].filter(Boolean).join("\n"); ].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 ---------------------------------------------- // ---- 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 // The whole point of the mesh, and the capability the migration depends on: move a module's