Production readiness hardening and ops tooling
This commit is contained in:
108
scripts/provision-mqtt-device.mjs
Normal file
108
scripts/provision-mqtt-device.mjs
Normal file
@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync } from "node:child_process";
|
||||
import "dotenv/config";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function getArg(name) {
|
||||
const index = args.indexOf(name);
|
||||
return index >= 0 ? args[index + 1] : undefined;
|
||||
}
|
||||
|
||||
function hasFlag(name) {
|
||||
return args.includes(name);
|
||||
}
|
||||
|
||||
function usage() {
|
||||
console.log(`Usage:
|
||||
node scripts/provision-mqtt-device.mjs --device-id <device-id> [--base-url http://127.0.0.1:3000] [--apply-local]
|
||||
|
||||
Options:
|
||||
--device-id Device UUID from the platform.
|
||||
--base-url Running backend URL. Default: BASE_URL env or http://127.0.0.1:3000.
|
||||
--apply-local Run mosquitto_passwd locally. Use only on the broker host.
|
||||
|
||||
Environment:
|
||||
ADMIN_TOKEN Admin bearer token. Default: admin-dev-token.
|
||||
MOSQUITTO_PASSWD_FILE Default: /etc/mosquitto/passwd.
|
||||
MOSQUITTO_ACL_FILE Default: /etc/mosquitto/acl.
|
||||
`);
|
||||
}
|
||||
|
||||
const deviceId = getArg("--device-id");
|
||||
const baseUrl = getArg("--base-url") || process.env.BASE_URL || "http://127.0.0.1:3000";
|
||||
const adminToken = process.env.ADMIN_TOKEN || "admin-dev-token";
|
||||
const passwdFile = process.env.MOSQUITTO_PASSWD_FILE || "/etc/mosquitto/passwd";
|
||||
const aclFile = process.env.MOSQUITTO_ACL_FILE || "/etc/mosquitto/acl";
|
||||
const applyLocal = hasFlag("--apply-local");
|
||||
|
||||
if (!deviceId || hasFlag("--help") || hasFlag("-h")) {
|
||||
usage();
|
||||
process.exit(deviceId ? 0 : 1);
|
||||
}
|
||||
|
||||
function shellQuote(value) {
|
||||
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
|
||||
async function rotateCredential() {
|
||||
const response = await fetch(`${baseUrl}/admin/devices/${deviceId}/credentials/rotate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${adminToken}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: "{}"
|
||||
});
|
||||
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(payload?.message || payload?.code || `rotate failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
return payload.data;
|
||||
}
|
||||
|
||||
function applyMosquittoPassword(username, password) {
|
||||
const result = spawnSync("sudo", ["mosquitto_passwd", "-b", passwdFile, username, password], {
|
||||
stdio: "inherit"
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`mosquitto_passwd failed with status ${result.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
const data = await rotateCredential();
|
||||
const username = data.credential.mqtt_username;
|
||||
const password = data.credential.mqtt_password;
|
||||
|
||||
if (applyLocal) {
|
||||
applyMosquittoPassword(username, password);
|
||||
}
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
device_id: data.device.id,
|
||||
mqtt_username: username,
|
||||
mqtt_password: password,
|
||||
one_time_secret: true,
|
||||
applied_local: applyLocal,
|
||||
mosquitto_commands: [
|
||||
`sudo mosquitto_passwd -b ${shellQuote(passwdFile)} ${shellQuote(username)} ${shellQuote(password)}`,
|
||||
`node scripts/check-mqtt-acl.mjs --file ${shellQuote(aclFile)} || node scripts/check-mqtt-acl.mjs --print-template`,
|
||||
"sudo chown root:mosquitto /etc/mosquitto/passwd",
|
||||
"sudo chmod 640 /etc/mosquitto/passwd",
|
||||
"sudo systemctl reload mosquitto"
|
||||
],
|
||||
topic_scope: [
|
||||
`device publish/readwrite: devices/${username}/uplink/#`,
|
||||
`device subscribe/read: devices/${username}/downlink/#`,
|
||||
"backend read: devices/+/uplink/#",
|
||||
"backend write: devices/+/downlink/#"
|
||||
]
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user