whole-mesh-full: the vault before the modules that keep secrets in it; no operator root or app secrets delivered (ADRs 0094, 0098)
This commit is contained in:
@@ -179,6 +179,9 @@ const NOVOX: Mod[] = [
|
|||||||
{ name: "mongodb", containers: ["mongo", "mesh-mongodb"] },
|
{ name: "mongodb", containers: ["mongo", "mesh-mongodb"] },
|
||||||
{ name: "mssql", containers: ["mssql", "mesh-mssql"] },
|
{ name: "mssql", containers: ["mssql", "mesh-mssql"] },
|
||||||
{ name: "lavinmq", containers: ["lavinmq", "mesh-lavinmq"] },
|
{ 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
|
// 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
|
// route-proxy is unresolvable and takes every routed module down with it. step-ca is that
|
||||||
// provider, on the anchor, at mesh scope.
|
// 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: "home-assistant", name: "token", crash: "no Home Assistant token" },
|
||||||
{ node: "ace", module: "nzbget", name: "password", crash: "NZBGet not configured" },
|
{ node: "ace", module: "nzbget", name: "password", crash: "NZBGet not configured" },
|
||||||
{ node: "ace", module: "qbittorrent", name: "password", crash: "qBittorrent 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 }[] = [
|
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-user", value: "despiegel-smtp-fake" },
|
||||||
{ node: "novox", module: "de-spiegel", name: "smtp-pass", value: "despiegel-pass-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" },
|
{ 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;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ADR 0098: the internal authority makes its own root at first start and serves it; the proxy
|
||||||
* ADR 0066: the internal CA is initialised FROM AN OPERATOR'S ROOT — it does not mint its own.
|
// fetches it. No operator root is delivered — the mesh mints only the authority's password.
|
||||||
*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** What a module's manifest says its route label is, or "" if it contributes no route. */
|
/** What a module's manifest says its route label is, or "" if it contributes no route. */
|
||||||
function routeLabelOf(name: string): string {
|
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).
|
// ONE push per node (workstations first — cheap — then the heavy service nodes).
|
||||||
const pushError: Record<string, string> = {};
|
const pushError: Record<string, string> = {};
|
||||||
for (const node of ["shanks", "g14", "novox", "ace"]) {
|
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()
|
? (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(`\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"}`);
|
adr.push(` step-ca container: ${psMaps["novox"]?.get("step-ca") ?? "MISSING"}`);
|
||||||
console.log(adr.join("\n"));
|
console.log(adr.join("\n"));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user