Replace host tokens with host pairing

Hosts join the cluster by running the installer, which shows a code and
waits until the request is approved under Hosts > Add host in the web
interface or with clusterioctl host approve. An admin can pass their
user token to the installer to approve the host right away. The token
is only used for that and is not saved.

The controller only accepts hosts that have a record, and only
approving a host creates one. Revoking a host deletes its record, so
its token can no longer be used to reconnect. Hosts that have connected
before keep working.

Removes host generate-token, revoke-token and create-config from
clusterioctl, bootstrap generate-host-token, the host token messages and
the core.host.generate_token, revoke_token and create_config
permissions, which are also stripped from stored roles. Adds
clusterioctl host pending, approve, reject and revoke, and the
core.host.approve and core.host.revoke permissions.

Standalone installs add the host to the controller database directly,
since the controller is not running during the install.
This commit is contained in:
Danielv123 committed 2026-10-30 06:11:32 +01:00
1 parent fa2883d74e
commit 87d4f5c64e
49 files changed
+4855 -473

No files matched your search

+46 -4
View File
@@ -32,6 +32,8 @@ import readline from "node:readline";
import util from "node:util";
import { loadBreaks, matchBreaks, selectBreaks } from "./version-compat-breaks.js";
// Only uses Node builtins, so it does not load a second copy of the lib.
import { pairHost, PairingError } from "../packages/create/pair.js";
const execFile = util.promisify(child_process.execFile);
@@ -279,6 +281,44 @@ async function pollCtl(ctl, cliArgs, expectRegex, description, timeoutMs = 30e3)
}
}
/**
* Pair the host with the running controller and write its config
*
* The pairing API is probed instead of checking which side is local, as releases that support
* pairing no longer have ctl host create-config to fall back on.
*
* @returns true if the host was paired, false if the controller predates host pairing.
*/
async function pairHostWithController() {
// Written by bootstrap create-ctl-config, holds the admin token which approves the host directly.
const ctlConfig = JSON.parse(await fs.readFile(path.join(controllerDir, "config-ctl.json"), "utf8"));
const controllerUrl = ctlConfig["ctl.controller_url"];
console.log(`| pairing host with ${controllerUrl}`);
let pairing;
try {
pairing = await pairHost({ controllerUrl, name: "host", token: ctlConfig["ctl.controller_token"] });
} catch (err) {
if (err instanceof PairingError && err.status === 404) {
console.log("| controller does not support host pairing");
return false;
}
throw err;
}
console.log(`| paired host ${pairing.name} with id ${pairing.hostId}`);
// The same fields the installer sets, which every host version reads.
const hostConfig = {
"host.id": pairing.hostId,
"host.name": pairing.name,
"host.controller_url": controllerUrl,
"host.controller_token": pairing.token,
};
await fs.writeFile(
path.join(hostDir, "config-host.json"), JSON.stringify(hostConfig, null, "\t"), { flag: "wx", mode: 0o600 }
);
return true;
}
async function main() {
const options = parseArgs();
const breaks = await loadBreaks();
@@ -337,10 +377,12 @@ async function main() {
);
try {
await run("ctl", ctl, [
"host", "create-config", "--id", "1", "--name", "host", "--generate-token",
"--output", path.join(hostDir, "config-host.json"),
], controllerDir);
if (!await pairHostWithController()) {
await run("ctl", ctl, [
"host", "create-config", "--id", "1", "--name", "host", "--generate-token",
"--output", path.join(hostDir, "config-host.json"),
], controllerDir);
}
// Started host is only logged after the connection to the controller is established.
const hostProc = await spawnDaemon("host", host, ["run"], hostDir, /Started host/);