mosquitto: run-once dynsec bootstrap + fix the exit-0-on-failure provisioner #8

Merged
jschoubben merged 2 commits from feat/mosquitto-bootstrap into main 2026-09-05 23:06:08 +00:00
3 changed files with 69 additions and 1 deletions
Showing only changes of commit ec1e7189a3 - Show all commits
+47
View File
@@ -0,0 +1,47 @@
// mosquitto's run-once bootstrap — mosquitto's own code (novox/hq ADR 0039), run once before the
// broker first starts (ADR 0052). The Dynamic Security plugin refuses to bring the broker up unless
// `dynamic-security.json` already holds an admin client, and nothing in a reconcile loop ever seeds
// that file — a state declaration describes what should exist, not a step that runs. This is that
// step: it writes the seed offline, exactly once, and exits. The host runs it to completion and only
// then starts the broker container the manifest places after it.
//
// It runs in the module's own runtime image, under the module's own account, as `mesh-tools run`
// imports it — no broker connection, because seeding is an offline file operation and there is no
// broker to reach yet. `mosquitto_ctrl dynsec init` writes the file; nothing here talks to a server.
//
// **Two disciplines make the seed safe to live beside a file the plugin then grows:**
//
// - It writes only when the file is absent, and never reconciles it. Once the broker is up, the
// dynsec plugin owns that file and rewrites it on every client it creates; re-seeding would wipe
// every provisioned client (novox/hq 04-ISSUES/035). The host's completion marker keeps the step
// from re-running; this absent-check keeps the one pass it does run from clobbering.
// - It hands the file to the broker's user. The broker runs as uid 1883 and must both READ the
// seed at startup and PERSIST to it as clients come and go; this container runs as root and would
// otherwise leave a root-owned file the broker at 1883 can neither read (if 0600) nor rewrite.
// So after writing, it chowns the file to 1883:1883 and sets 0600 — the broker's to read and to
// grow, and no one else's. This is the ownership question ADR 0052 left for the lab to settle.
import { chownSync, chmodSync, existsSync } from "node:fs";
import { MosquittoClient } from "../client.js";
// The broker (eclipse-mosquitto) runs as this uid/gid; the seeded store must be its to read and
// rewrite. Overridable for a broker image that runs as a different user.
const BROKER_UID = Number(process.env.MESH_MQTT_BROKER_UID ?? "1883") || 1883;
const BROKER_GID = Number(process.env.MESH_MQTT_BROKER_GID ?? "1883") || 1883;
// The same path the broker's `plugin_opt_config_file` names, reached through the shared data volume.
const configFile = process.env.MESH_DYNSEC_FILE ?? "/mosquitto/data/dynamic-security.json";
if (existsSync(configFile)) {
// Already seeded — and possibly grown by the running plugin since. Leave it exactly as it is.
console.log(`[mosquitto:bootstrap] ${configFile} already exists; leaving it untouched`);
} else {
const mosquitto = MosquittoClient.fromEnv();
await mosquitto.initBootstrapFile(configFile);
// Hand the store to the broker's user so it can read the seed and persist to it (see the header).
chownSync(configFile, BROKER_UID, BROKER_GID);
chmodSync(configFile, 0o600);
console.log(
`[mosquitto:bootstrap] seeded ${configFile} with the dynsec admin client, owned by ${BROKER_UID}:${BROKER_GID}`,
);
}
+21
View File
@@ -84,6 +84,27 @@
"type": "network",
"name": "mosquitto"
},
{
"id": "bootstrap",
"type": "container",
"name": "mosquitto-bootstrap",
"image": "mesh-runtime-mosquitto@sha256:0000000000000000000000000000000000000000000000000000000000000000",
"run-once": true,
"volumes": [
"/services/mosquitto/data:/mosquitto/data",
"/var/lib/mosquitto-module/admin.secret:/run/secrets/admin:ro"
],
"env": {
"MESH_PROVISION_MQTT": "mosquitto:1883",
"MESH_PROVISION_ADMIN_USER": "mesh-admin",
"MESH_PROVISION_PASSWORD_FILE": "/run/secrets/admin",
"MESH_DYNSEC_FILE": "/mosquitto/data/dynamic-security.json"
},
"args": [
"run",
"/app/modules/mosquitto/dist/bootstrap/index.js"
]
},
{
"id": "server",
"type": "container",
+1 -1
View File
@@ -8,5 +8,5 @@
"skipLibCheck": true,
"noEmit": true
},
"include": ["client.ts", "index.ts", "provisioner/index.ts", "tools/index.ts"]
"include": ["client.ts", "index.ts", "provisioner/index.ts", "tools/index.ts", "bootstrap/index.ts"]
}