Merge pull request #857 from Hornwitser/remote-tls-ca

Remove tls_ca config option
This commit is contained in:
Hornwitser authored and GitHub committed 2026-03-05 17:22:17 +01:00
commit ef0baaa30f
22 files changed
+62 -120

No files matched your search

-16
View File
@@ -182,14 +182,6 @@ You can generate an access token with `clusterioctl host generate-token --id <ho
Defaults to "enter token here".
### host.tls_ca
Path to certificate in PEM format to use for validating a TLS connection to the controller.
If you have a self signed certificate on the controller you will need to copy the certificate to your hosts and set it as the certificate authority with this option.
Defaults to null meaning use Node.js's default set of trusted certificate authorities.
### host.public_address
External address instances hosted on this host can be accessed on.
@@ -413,14 +405,6 @@ You can generate an access token with `clusteriocontroller bootstrap generate-us
Defaults to null meaning complain about it not being set and exit.
### control.tls_ca
Path to certificate in PEM format to use for validating a TLS connection to the controller.
If you have a self signed certificate on the controller you will need to copy the certificate to the computer you run clusterctl from and set it as the certificate authority with this option.
Defaults to null meaning use Node.js's default set of trusted certificate authorities.
### control.max_reconnect_delay
Max duration in seconds to wait before attempting to reconnect with the controller after the connection is dropped.
+19 -4
View File
@@ -63,10 +63,25 @@ This creates a private key file named `key.pem` and a public certificate file na
npx clusteriocontroller config set controller.tls_certificate cert.pem
npx clusteriocontroller config set controller.tls_private_key key.pem
Additionally you will need copy the `cert.pem` file to all of the computers you want to set up as hosts and configure it as the `host.tls_ca` option:
Additionally you will need copy the `cert.pem` file to all of the computers you want to set up as hosts and configure node to trust it by setting the `NODE_EXTRA_CA_CERTS` environment variable to a path to this file before the host is started. You can do this by passing `--self-signed-cert path/to/cert` to the installer when setting up a host, or manually by editing run-host.cmd, run-host.sh and/or clusteriohost.service by adding the following line next to the one setting `NODE_OPTIONS`:
npx clusteriohost config set host.tls_ca cert.pem
**For run-host.cmd**
You will also need to copy the `cert.pem` file to all of the computers you want to remotely manage the cluster via clusterctl and configure it as `control.tls_ca` option:
set "NODE_EXTRA_CA_CERTS=path\to\cert.pem"
npx clusterioctl control-config set control.tls_ca cert.pem
**For run-host.sh**
export "NODE_EXTRA_CA_CERTS=path/to/cert.pem"
**For clusteriohost.service**
Environment=NODE_EXTRA_CA_CERTS=path/to/cert.pem
You will also need to copy the `cert.pem` file to all of the computers you want to remotely manage the cluster via clusterctl and set the `NODE_EXTRA_CA_CERTS` environment variable as a path to that file before running clusterioctl. For example:
# On Windows
set "NODE_EXTRA_CA_CERTS=path\to\cert.pem"
# On Linux
export NODE_EXTRA_CA_CERTS="path/to/cert.pem"
# You can now use clusterioctl
npx clusterioctl instance list
+1 -1
View File
@@ -7,7 +7,7 @@
"build": "tsc --build",
"watch": "tsc --build --watch",
"build-mod": "pnpm run -r build-mod --output-dir ../../dist",
"test": "pnpm run build && mocha --enable-source-maps --check-leaks -R spec --recursive test \"plugins/*/test/**\" --exclude \"test/manual/**\"",
"test": "pnpm run build && NODE_EXTRA_CA_CERTS=$(pwd)/test/file/tls/cert.pem mocha --enable-source-maps --check-leaks -R spec --recursive test \"plugins/*/test/**\" --exclude \"test/manual/**\"",
"silent-test": "SILENT_TEST=y pnpm test",
"fast-test": "FAST_TEST=y pnpm test",
"cover": "nyc pnpm test",
+12 -6
View File
@@ -335,7 +335,7 @@ async function migrateRename(args) {
let mode = "standalone";
if (!hasRunSlave) { mode = "controller"; }
if (!hasRunMaster) { mode = "host"; }
await writeScripts(mode);
await writeScripts(mode, "");
}
logger.info(
@@ -420,7 +420,7 @@ async function groupIdToName(gid) {
}
}
async function writeScripts(mode) {
async function writeScripts(mode, selfSignedCert) {
if (["standalone", "controller"].includes(mode)) {
if (process.platform === "win32") {
await safeOutputFile(
@@ -474,12 +474,13 @@ WantedBy=multi-user.target
if (["standalone", "host"].includes(mode)) {
if (process.platform === "win32") {
let extraCaCertsLine = selfSignedCert ? `set "NODE_EXTRA_CA_CERTS=${selfSignedCert}"\n` : "";
await safeOutputFile(
"run-host.cmd",
`\
@echo off
set "NODE_OPTIONS=--enable-source-maps %NODE_OPTIONS%"
:restart
${extraCaCertsLine}:restart
call .\\node_modules\\.bin\\clusteriohost.cmd run --can-restart
if %errorlevel% equ 0 exit /b
if %errorlevel% equ 8 exit /b
@@ -487,18 +488,20 @@ goto restart
`
);
} else {
let extraCaCertsLine = selfSignedCert ? `export "NODE_EXTRA_CA_CERTS=${selfSignedCert}"\n` : "";
await safeOutputFile(
"run-host.sh",
`\
#!/bin/bash
export "NODE_OPTIONS=--enable-source-maps $NODE_OPTIONS"
while true; do
${extraCaCertsLine}while true; do
./node_modules/.bin/clusteriohost run --can-restart
if [[ $? -eq 0 || $? -eq 8 ]]; then exit $?; fi
done
`,
{ mode: 0o755 },
);
extraCaCertsLine = selfSignedCert ? `Environment=NODE_EXTRA_CA_CERTS=${selfSignedCert}\n` : "";
await safeOutputFile(
"systemd/clusteriohost.service",
`\
@@ -512,7 +515,7 @@ WorkingDirectory=${process.cwd()}
KillMode=mixed
KillSignal=SIGINT
Environment=NODE_OPTIONS=--enable-source-maps
ExecStart=${process.cwd()}/node_modules/.bin/clusteriohost run --log-level=warn --can-restart
${extraCaCertsLine}ExecStart=${process.cwd()}/node_modules/.bin/clusteriohost run --log-level=warn --can-restart
Restart=on-failure
RestartPreventExitStatus=8
@@ -839,6 +842,9 @@ async function main() {
.option("factorio-dir", {
nargs: 1, describe: "Path to Factorio installation [standalone/host]", type: "string",
})
.option("self-signed-cert", {
nargs: 1, describe: "Path to self-signed certificate of the controller [standalone/host]", type: "string",
})
.option("remote-npm", {
nargs: 0, description: "Allow remote updates via npm [standalone/controller/host]", type: "boolean",
})
@@ -947,7 +953,7 @@ async function main() {
if (!dev && ["standalone", "controller", "host"].includes(answers.mode)) {
logger.info("Writing run scripts");
await writeScripts(answers.mode);
await writeScripts(answers.mode, args.selfSignedCert);
}
if (answers.mode === "ctl") {
+3 -14
View File
@@ -27,8 +27,8 @@ import BaseCtlPlugin from "./src/BaseCtlPlugin";
class ControlConnector extends lib.WebSocketClientConnector {
private _token: string;
constructor(url: string, maxReconnectDelay: number, tlsCa: string | undefined, token: string) {
super(url, maxReconnectDelay, tlsCa);
constructor(url: string, maxReconnectDelay: number, token: string) {
super(url, maxReconnectDelay);
this._token = token;
}
@@ -54,8 +54,6 @@ class ControlConnector extends lib.WebSocketClientConnector {
export class Control extends lib.Link {
/** Control config used for connecting to the controller. */
config: lib.ControlConfig;
/** Certificate authority used to validate TLS connections to the controller. */
tlsCa?: string;
/** Mapping of plugin names to their instance for loaded plugins. */
plugins: Map<string, BaseCtlPlugin>;
/** Keep the control connection alive after the command completes. */
@@ -64,12 +62,10 @@ export class Control extends lib.Link {
constructor(
connector: ControlConnector,
controlConfig: lib.ControlConfig,
tlsCa: string | undefined,
ctlPlugins: Map<string, BaseCtlPlugin>
) {
super(connector);
this.config = controlConfig;
this.tlsCa = tlsCa;
this.plugins = ctlPlugins;
this.handle(lib.LogMessageEvent, this.handleLogMessageEvent.bind(this));
@@ -299,20 +295,13 @@ async function startControl() {
return;
}
let tlsCa: string | undefined;
let tlsCaPath = controlConfig.get("control.tls_ca");
if (tlsCaPath) {
tlsCa = await fs.readFile(tlsCaPath, "utf8");
}
let controlConnector = new ControlConnector(
controlConfig.get("control.controller_url")!,
controlConfig.get("control.max_reconnect_delay"),
tlsCa,
controlConfig.get("control.controller_token")!,
);
let control = new Control(controlConnector, controlConfig, tlsCa, ctlPlugins);
let control = new Control(controlConnector, controlConfig, ctlPlugins);
try {
await controlConnector.connect();
} catch (err) {
-1
View File
@@ -169,7 +169,6 @@ controllerPluginCommands.add(new lib.Command({
let response = await phin<[]>({
url,
parse: "json",
core: { ca: control.tlsCa } as object,
});
print(asTable(response.body));
},
-2
View File
@@ -345,7 +345,6 @@ instanceSaveCommands.add(new lib.Command({
"X-Access-Token": control.config.get("control.controller_token"),
"Content-Type": "application/zip",
},
core: { ca: control.tlsCa } as object,
data: content,
parse: "json",
});
@@ -420,7 +419,6 @@ instanceSaveCommands.add(new lib.Command({
url.pathname += `api/stream/${streamId}`;
let response = await phin({
url, method: "GET",
core: { ca: control.tlsCa } as object,
stream: true,
});
-2
View File
@@ -160,7 +160,6 @@ modCommands.add(new lib.Command({
"X-Access-Token": control.config.get("control.controller_token"),
"Content-Type": "application/zip",
},
core: { ca: control.tlsCa } as object,
data: content,
parse: "json",
});
@@ -199,7 +198,6 @@ modCommands.add(new lib.Command({
url.pathname += `api/stream/${streamId}`;
let response = await phin({
url, method: "GET",
core: { ca: control.tlsCa } as object,
stream: true,
});
+1 -10
View File
@@ -51,13 +51,11 @@ void new lib.Gauge(
export class HostConnector extends lib.WebSocketClientConnector {
constructor(
public hostConfig: lib.HostConfig,
tlsCa: string | undefined,
public pluginInfos: lib.PluginNodeEnvInfo[]
) {
super(
hostConfig.get("host.controller_url"),
hostConfig.get("host.max_reconnect_delay"),
tlsCa
);
}
@@ -230,17 +228,10 @@ async function startHost() {
return;
}
let tlsCa: string | undefined;
let tlsCaPath = hostConfig.get("host.tls_ca");
if (tlsCaPath) {
tlsCa = await fs.readFile(tlsCaPath, "utf8");
}
let hostConnector = new HostConnector(hostConfig, tlsCa, pluginInfos);
let hostConnector = new HostConnector(hostConfig, pluginInfos);
host = new Host(
hostConnector,
hostConfig,
tlsCa,
pluginInfos,
Boolean(args.canRestart),
Boolean(args.recovery),
-9
View File
@@ -259,10 +259,6 @@ export default class Host extends lib.Link {
declare "connector": HostConnector;
router = new HostRouter(this);
/**
* Certificate authority used to validate TLS connections to the controller.
*/
tlsCa?: string;
pluginInfos: lib.PluginNodeEnvInfo[];
config: lib.HostConfig;
@@ -304,7 +300,6 @@ export default class Host extends lib.Link {
constructor(
connector: HostConnector,
hostConfig: lib.HostConfig,
tlsCa: string | undefined,
pluginInfos: lib.PluginNodeEnvInfo[],
/**
* If true indicates that there is a process monitor present that
@@ -319,7 +314,6 @@ export default class Host extends lib.Link {
public modStore = new lib.ModStore(hostConfig.get("host.mods_directory"), new Map()),
) {
super(connector);
this.tlsCa = tlsCa;
this.pluginInfos = pluginInfos;
this.config = hostConfig;
@@ -593,7 +587,6 @@ export default class Host extends lib.Link {
url.pathname += `api/stream/${streamId}`;
let response = await phin({
url, method: "GET",
core: { ca: this.tlsCa } as object,
stream: true,
});
@@ -1027,7 +1020,6 @@ export default class Host extends lib.Link {
url.pathname += `api/stream/${streamId}`;
let response = await phin({
url, method: "GET",
core: { ca: this.tlsCa } as object,
stream: true,
});
@@ -1082,7 +1074,6 @@ export default class Host extends lib.Link {
url.pathname += `api/stream/${streamId}`;
phin({
url, method: "PUT",
core: { ca: this.tlsCa } as object,
data: content,
}).catch(err => {
logger.error(`Error pushing save to controller:\n${err.stack}`, this.instanceLogMeta(instanceId));
-1
View File
@@ -1295,7 +1295,6 @@ end`.replace(/\r?\n/g, " ");
let response = await phin({
url, method: "PUT",
data: content,
core: { ca: this._host.tlsCa } as object,
headers: {
"Content-Type": "application/zip",
"x-access-token": this._host.config.get("host.controller_token"),
-13
View File
@@ -259,7 +259,6 @@ export interface HostConfigFields {
"host.instances_directory": string;
"host.controller_url": string;
"host.controller_token": string;
"host.tls_ca": string | null;
"host.public_address": string;
"host.factorio_port_range": string;
"host.factorio_username": string | null,
@@ -327,12 +326,6 @@ export class HostConfig extends classes.Config<HostConfigFields> {
type: "string",
initialValue: "enter token here",
},
"host.tls_ca": {
description: "Path to Certificate Authority to validate TLS connection to controller against.",
restartRequired: true,
type: "string",
optional: true,
},
"host.public_address": {
description: "Public facing address players should connect to in order to join instances on this host",
type: "string",
@@ -624,7 +617,6 @@ export class InstanceConfig extends classes.Config<InstanceConfigFields> {
export interface ControlConfigFields {
"control.controller_url": string | null;
"control.controller_token": string | null;
"control.tls_ca": string | null;
"control.max_reconnect_delay": number;
}
@@ -647,11 +639,6 @@ export class ControlConfig extends classes.Config<ControlConfigFields> {
type: "string",
optional: true,
},
"control.tls_ca": {
description: "Path to Certificate Authority to validate TLS connection to controller against.",
type: "string",
optional: true,
},
"control.max_reconnect_delay": {
title: "Max Reconnect Delay",
description: "Maximum delay to wait before attempting to reconnect WebSocket",
+1 -12
View File
@@ -431,7 +431,6 @@ export abstract class WebSocketClientConnector extends WebSocketBaseConnector<We
constructor(
protected _url: string,
maxReconnectDelay: number,
protected _tlsCa: string | undefined,
) {
super(undefined as any, new libData.Address(libData.Address.controller, 0));
this._backoff = new ExponentialBackoff({ max: maxReconnectDelay });
@@ -543,17 +542,7 @@ export abstract class WebSocketClientConnector extends WebSocketBaseConnector<We
// Open WebSocket to controller
logger.verbose(`Connector | connecting to ${url}`);
if (process.env.APP_ENV === "browser") {
this._socket = new WebSocket(url) as WebSocketClusterio;
} else {
let options: { ca?: string } = {};
if (this._tlsCa) { options.ca = this._tlsCa; }
this._socket = new WebSocket(url, options) as WebSocketClusterio;
}
this._socket = new WebSocket(url) as WebSocketClusterio;
this._attachSocketHandlers();
}
+1 -1
View File
@@ -137,7 +137,7 @@ export default async function bootstrap() {
lib.addPluginConfigFields(pluginInfos);
let wsUrl = new URL(webRoot, document.location.href);
let controlConnector = new ControlConnector(wsUrl.href, 120, undefined);
let controlConnector = new ControlConnector(wsUrl.href, 120);
let control = new Control(controlConnector, new Map(pluginInfos.map(p => [p.name, p])));
control.plugins = await loadPlugins(pluginInfos, control);
control.inputComponents = inputComponentsFromPlugins(control.plugins);
+1
View File
@@ -38,3 +38,4 @@ catalog:
catalogMode: strict
linkWorkspacePackages: true
shellEmulator: true
+2 -2
View File
@@ -127,8 +127,8 @@ describe("Host testing", function() {
version: hostVersion,
}];
const hostConfig = new lib.HostConfig("host", { "host.version": hostVersion });
const hostConnector = new HostConnector(hostConfig, undefined, pluginInfos);
host = new Host(hostConnector, hostConfig, undefined, pluginInfos);
const hostConnector = new HostConnector(hostConfig, pluginInfos);
host = new Host(hostConnector, hostConfig, pluginInfos);
});
it("returns false when no changes are present", async function() {
host.pluginInfos[0].hostEntrypoint = true;
+4 -8
View File
@@ -41,7 +41,6 @@ async function startAltHost() {
await fs.remove(path.join("temp", "test", "alt-instances"));
await fs.remove(path.join("temp", "test", "alt-mods"));
await execCtl(`host create-config --id 5 --name alt-host --generate-token --output ${config}`);
await execHost(`--config ${config} config set host.tls_ca ../../test/file/tls/cert.pem`);
await execHost(`--config ${config} config set host.instances_directory alt-instances`);
const dir = path.isAbsolute(factorioDir) ? factorioDir : path.join("..", "..", factorioDir);
await execHost(`--config ${config} config set host.factorio_directory ${dir}`);
@@ -168,8 +167,7 @@ describe("Integration of Clusterio", function() {
describe("run", function() {
it("should handle resume of an active connection", async function() {
slowTest(this);
let tlsCa = await fs.readFile("test/file/tls/cert.pem");
let connectorA = new TestControlConnector(url, 2, tlsCa);
let connectorA = new TestControlConnector(url, 2);
connectorA.token = controlToken;
let controlA = new TestControl(connectorA);
await connectorA.connect();
@@ -177,7 +175,7 @@ describe("Integration of Clusterio", function() {
connectorA.stopHeartbeat();
connectorA.on("error", () => {});
let connectorB = new TestControlConnector(url, 2, tlsCa);
let connectorB = new TestControlConnector(url, 2);
connectorB.token = controlToken;
connectorB.src = connectorA.src;
let controlB = new TestControl(connectorB);
@@ -1679,8 +1677,7 @@ describe("Integration of Clusterio", function() {
it("should kick existing sessions for the user", async function() {
slowTest(this);
await getControl().send(new lib.UserCreateRequest("revokee"));
let tlsCa = await fs.readFile("test/file/tls/cert.pem");
let connector = new TestControlConnector(url, 2, tlsCa);
let connector = new TestControlConnector(url, 2);
connector.token = jwt.sign(
{ aud: "user", user: "revokee" }, Buffer.from("TestSecretDoNotUse", "base64")
);
@@ -1709,8 +1706,7 @@ describe("Integration of Clusterio", function() {
await execCtl("user set-roles temp Player");
let tempControl;
try {
let tlsCa = await fs.readFile("test/file/tls/cert.pem");
let connector = new TestControlConnector(url, 2, tlsCa);
let connector = new TestControlConnector(url, 2);
connector.token = jwt.sign(
{ aud: "user", user: "temp" }, Buffer.from("TestSecretDoNotUse", "base64")
);
+11 -8
View File
@@ -194,11 +194,18 @@ let controllerConfigPath = path.join("temp", "test", "config-controller.json");
let hostConfigPath = path.join("temp", "test", "config-host.json");
let controlConfigPath = path.join("temp", "test", "config-control.json");
function childOptions(options) {
return {
cwd: path.join("temp", "test"),
env: { ...process.env },
...options,
};
}
function exec(command, options = {}) {
// Uncomment to show commands run in tests
// console.log(command);
options = { cwd: path.join("temp", "test"), ...options };
return util.promisify(child_process.exec)(command, options);
return util.promisify(child_process.exec)(command, childOptions(options));
}
function execController(...args) {
@@ -264,7 +271,7 @@ function spawn(name, cmd, waitFor, options = {}) {
return new Promise((resolve, reject) => {
log(cmd);
let parts = cmd.split(" ");
let process = child_process.spawn(parts[0], parts.slice(1), { cwd: path.join("temp", "test"), ...options });
let process = child_process.spawn(parts[0], parts.slice(1), childOptions(options));
let stdout = new LineSplitter({ readableObjectMode: true });
let stderr = new LineSplitter({ readableObjectMode: true });
let onDataOut = line => {
@@ -354,7 +361,6 @@ before(async function() {
console.log("Bootstrapping");
await execController("bootstrap create-admin test");
await execController("bootstrap create-ctl-config test");
await execCtlProcess("control-config set control.tls_ca ../../test/file/tls/cert.pem");
controllerProcess = await spawnNode("controller:", "../../packages/controller run", /Started controller/);
@@ -362,12 +368,10 @@ before(async function() {
const relativeFactorioDir = path.isAbsolute(factorioDir) ? factorioDir : path.join("..", "..", factorioDir);
await execCtlProcess("host create-config --id 4 --name host --generate-token");
await execHost(`config set host.factorio_directory ${relativeFactorioDir}`);
await execHost("config set host.tls_ca ../../test/file/tls/cert.pem");
hostProcess = await spawnNode("host:", "../../packages/host run", /Started host/);
const tlsCa = await fs.readFile("test/file/tls/cert.pem");
const controlConnector = new TestControlConnector(url, 2, tlsCa);
const controlConnector = new TestControlConnector(url, 2);
controlConnector.token = controlToken;
control = new TestControl(controlConnector);
await controlConnector.connect();
@@ -375,7 +379,6 @@ before(async function() {
const initArgs = await initializeCtl(`--plugin-list ${pluginListPath} control-config list`, undefined, true);
control.config = initArgs.controlConfig;
control.plugins = initArgs.ctlPlugins;
control.tlsCa = tlsCa;
const testPack = lib.ModPack.fromJSON({});
testPack.id = 12;
+3 -7
View File
@@ -11,18 +11,14 @@ let token = jwt.sign({ aud: "user", user: "test" }, Buffer.from("TestSecretDoNot
let tokenHost = jwt.sign({ aud: "host", host: 0 }, Buffer.from("TestSecretDoNotUse", "base64"));
describe("Integration of lib/link", function() {
let tlsCa;
let control;
let controlConnector;
let hostConnector;
before(async function() {
tlsCa = await fs.readFile("test/file/tls/cert.pem");
});
beforeEach(async function() {
controlConnector = new TestControlConnector(url, 0.2, tlsCa);
controlConnector = new TestControlConnector(url, 0.2);
controlConnector.token = token;
control = new TestControl(controlConnector, false);
hostConnector = new TestHostConnector(url, 0.2, tlsCa);
hostConnector = new TestHostConnector(url, 0.2);
hostConnector.token = tokenHost;
hostConnector.hostId = 0;
});
@@ -123,7 +119,7 @@ describe("Integration of lib/link", function() {
it("should close old connections from the same address", async function() {
slowTest(this);
const hostConnectorFirst = new TestHostConnector(url, 0.2, tlsCa);
const hostConnectorFirst = new TestHostConnector(url, 0.2);
hostConnectorFirst.token = hostConnector.token;
hostConnectorFirst.hostId = hostConnector.hostId;
const onceClose = events.once(hostConnectorFirst, "close");
+1 -1
View File
@@ -23,7 +23,7 @@ function connectHost(controller, hostId, plugins) {
controller.wsServer.hostConnections.set(hostId, hostConnection);
const hostConfig = new lib.HostConfig("host");
hostConfig.set("host.id", hostId);
return new Host(hostSide, hostConfig, undefined, []);
return new Host(hostSide, hostConfig, []);
}
/**
+1 -1
View File
@@ -14,7 +14,7 @@ describe("messages/host", function() {
lib.Address.fromShorthand({ hostId: 1 }),
lib.Address.fromShorthand("controller"),
);
host = new Host(hostConnector, hostConfig, undefined, []);
host = new Host(hostConnector, hostConfig, []);
});
describe("HostUpdateRequest", function() {
+1 -1
View File
@@ -28,7 +28,7 @@ describe("messages/plugin", function() {
lib.Address.fromShorthand({ hostId: 1 }),
lib.Address.fromShorthand("controller"),
);
host = new Host(hostConnector, hostConfig, undefined, []);
host = new Host(hostConnector, hostConfig, []);
});
describe("PluginDetails", function() {