@studiometa/trafic-agent 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +3 -3
- package/dist/index.js +1 -1
- package/dist/setup/agent.d.ts +18 -5
- package/dist/setup/agent.d.ts.map +1 -1
- package/dist/setup/ddev.d.ts +8 -7
- package/dist/setup/ddev.d.ts.map +1 -1
- package/dist/setup/docker.d.ts +4 -3
- package/dist/setup/docker.d.ts.map +1 -1
- package/dist/setup/hardening.d.ts +17 -7
- package/dist/setup/hardening.d.ts.map +1 -1
- package/dist/setup/io.d.ts +31 -0
- package/dist/setup/io.d.ts.map +1 -0
- package/dist/setup/user.d.ts +4 -3
- package/dist/setup/user.d.ts.map +1 -1
- package/dist/{setup-C-gTXd-K.js → setup-4TI2lreq.js} +213 -154
- package/dist/setup-4TI2lreq.js.map +1 -0
- package/package.json +1 -1
- package/dist/setup-C-gTXd-K.js.map +0 -1
|
@@ -237,39 +237,68 @@ function commandExists(command) {
|
|
|
237
237
|
function isRoot() {
|
|
238
238
|
return process.getuid?.() === 0;
|
|
239
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Write a file (or show in dry-run mode)
|
|
242
|
+
*/
|
|
243
|
+
function writeFile(path, content) {
|
|
244
|
+
if (dryRun) {
|
|
245
|
+
console.log(` \x1b[90mWrite ${path}:\x1b[0m`);
|
|
246
|
+
const lines = content.split("\n").slice(0, 5);
|
|
247
|
+
for (const line of lines) console.log(` \x1b[90m ${line}\x1b[0m`);
|
|
248
|
+
if (content.split("\n").length > 5) console.log(` \x1b[90m ...\x1b[0m`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
writeFileSync(path, content);
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/setup/io.ts
|
|
255
|
+
/**
|
|
256
|
+
* The real implementation, used when no io is injected.
|
|
257
|
+
*
|
|
258
|
+
* `writeFile` comes from steps.js rather than `node:fs` so writes are
|
|
259
|
+
* skipped in dry-run mode. Reads stay direct: they change nothing.
|
|
260
|
+
*/
|
|
261
|
+
var nodeIo = {
|
|
262
|
+
exec,
|
|
263
|
+
commandExists,
|
|
264
|
+
writeFile,
|
|
265
|
+
fileExists: existsSync,
|
|
266
|
+
readFile: (path) => readFileSync(path, "utf-8"),
|
|
267
|
+
env: process.env
|
|
268
|
+
};
|
|
240
269
|
//#endregion
|
|
241
270
|
//#region src/setup/user.ts
|
|
242
271
|
/**
|
|
243
272
|
* Write /etc/sudoers.d/trafic-ddev allowing the ddev user to run
|
|
244
273
|
* ddev-hostname without a password. Idempotent — safe to call multiple times.
|
|
245
274
|
*/
|
|
246
|
-
function configureDdevSudoers() {
|
|
247
|
-
|
|
275
|
+
function configureDdevSudoers(io = nodeIo) {
|
|
276
|
+
io.writeFile("/etc/sudoers.d/trafic-ddev", `# Trafic: allow ddev user to run ddev-hostname without a password.
|
|
248
277
|
# ddev-hostname manages /etc/hosts and always calls sudo internally.
|
|
249
278
|
ddev ALL=(ALL) NOPASSWD: /usr/bin/ddev-hostname
|
|
250
279
|
`);
|
|
251
|
-
exec("chmod 440 /etc/sudoers.d/trafic-ddev", { silent: true });
|
|
280
|
+
io.exec("chmod 440 /etc/sudoers.d/trafic-ddev", { silent: true });
|
|
252
281
|
success("Configured passwordless sudo for ddev-hostname");
|
|
253
282
|
}
|
|
254
283
|
/**
|
|
255
284
|
* Create the ddev user with proper permissions
|
|
256
285
|
*/
|
|
257
|
-
function createDdevUser() {
|
|
286
|
+
function createDdevUser(io = nodeIo) {
|
|
258
287
|
step("Create ddev user");
|
|
259
|
-
if (
|
|
288
|
+
if (io.exec("id -u ddev 2>/dev/null", { silent: true })) info("User 'ddev' already exists");
|
|
260
289
|
else {
|
|
261
|
-
exec("useradd -m -s /bin/bash ddev");
|
|
290
|
+
io.exec("useradd -m -s /bin/bash ddev");
|
|
262
291
|
success("Created user 'ddev'");
|
|
263
292
|
}
|
|
264
|
-
exec("usermod -aG docker ddev 2>/dev/null || true", { silent: true });
|
|
265
|
-
exec("mkdir -p /home/ddev/www");
|
|
266
|
-
exec("chown ddev:ddev /home/ddev/www");
|
|
267
|
-
exec("chmod 750 /home/ddev/www");
|
|
293
|
+
io.exec("usermod -aG docker ddev 2>/dev/null || true", { silent: true });
|
|
294
|
+
io.exec("mkdir -p /home/ddev/www");
|
|
295
|
+
io.exec("chown ddev:ddev /home/ddev/www");
|
|
296
|
+
io.exec("chmod 750 /home/ddev/www");
|
|
268
297
|
success("Created /home/ddev/www");
|
|
269
|
-
exec("mkdir -p /home/ddev/.ssh");
|
|
270
|
-
exec("chmod 700 /home/ddev/.ssh");
|
|
271
|
-
exec("chown ddev:ddev /home/ddev/.ssh");
|
|
272
|
-
configureDdevSudoers();
|
|
298
|
+
io.exec("mkdir -p /home/ddev/.ssh");
|
|
299
|
+
io.exec("chmod 700 /home/ddev/.ssh");
|
|
300
|
+
io.exec("chown ddev:ddev /home/ddev/.ssh");
|
|
301
|
+
configureDdevSudoers(io);
|
|
273
302
|
success("User ddev configured");
|
|
274
303
|
}
|
|
275
304
|
//#endregion
|
|
@@ -277,26 +306,26 @@ function createDdevUser() {
|
|
|
277
306
|
/**
|
|
278
307
|
* Install Docker using the official script
|
|
279
308
|
*/
|
|
280
|
-
function installDocker() {
|
|
309
|
+
function installDocker(io = nodeIo) {
|
|
281
310
|
step("Install Docker");
|
|
282
|
-
if (commandExists("docker")) {
|
|
283
|
-
info(`Docker already installed: ${exec("docker --version", { silent: true })?.trim() ?? "unknown version"}`);
|
|
311
|
+
if (io.commandExists("docker")) {
|
|
312
|
+
info(`Docker already installed: ${io.exec("docker --version", { silent: true })?.trim() ?? "unknown version"}`);
|
|
284
313
|
return;
|
|
285
314
|
}
|
|
286
315
|
info("Downloading and running Docker install script...");
|
|
287
|
-
exec("curl -fsSL https://get.docker.com -o /tmp/get-docker.sh");
|
|
288
|
-
exec("sh /tmp/get-docker.sh", { silent: true });
|
|
289
|
-
exec("rm /tmp/get-docker.sh");
|
|
290
|
-
exec("systemctl enable docker", { silent: true });
|
|
291
|
-
exec("systemctl start docker", { silent: true });
|
|
316
|
+
io.exec("curl -fsSL https://get.docker.com -o /tmp/get-docker.sh");
|
|
317
|
+
io.exec("sh /tmp/get-docker.sh", { silent: true });
|
|
318
|
+
io.exec("rm /tmp/get-docker.sh");
|
|
319
|
+
io.exec("systemctl enable docker", { silent: true });
|
|
320
|
+
io.exec("systemctl start docker", { silent: true });
|
|
292
321
|
success("Docker installed and started");
|
|
293
|
-
exec("usermod -aG docker ddev", { silent: true });
|
|
322
|
+
io.exec("usermod -aG docker ddev", { silent: true });
|
|
294
323
|
success("Added ddev user to docker group");
|
|
295
324
|
}
|
|
296
325
|
/**
|
|
297
326
|
* Configure Docker for production use
|
|
298
327
|
*/
|
|
299
|
-
function configureDocker() {
|
|
328
|
+
function configureDocker(io = nodeIo) {
|
|
300
329
|
step("Configure Docker");
|
|
301
330
|
const daemonConfig = {
|
|
302
331
|
"log-driver": "json-file",
|
|
@@ -307,32 +336,32 @@ function configureDocker() {
|
|
|
307
336
|
"storage-driver": "overlay2",
|
|
308
337
|
"live-restore": true
|
|
309
338
|
};
|
|
310
|
-
exec("mkdir -p /etc/docker");
|
|
339
|
+
io.exec("mkdir -p /etc/docker");
|
|
311
340
|
const configPath = "/etc/docker/daemon.json";
|
|
312
|
-
if (
|
|
341
|
+
if (io.fileExists(configPath)) try {
|
|
313
342
|
const merged = {
|
|
314
|
-
...JSON.parse(
|
|
343
|
+
...JSON.parse(io.readFile(configPath)),
|
|
315
344
|
...daemonConfig
|
|
316
345
|
};
|
|
317
|
-
|
|
346
|
+
io.writeFile(configPath, JSON.stringify(merged, null, 2));
|
|
318
347
|
info("Merged with existing Docker config");
|
|
319
348
|
} catch {
|
|
320
349
|
warn("Could not parse existing Docker config, skipping");
|
|
321
350
|
return;
|
|
322
351
|
}
|
|
323
|
-
else
|
|
324
|
-
exec("systemctl reload docker || systemctl restart docker");
|
|
352
|
+
else io.writeFile(configPath, JSON.stringify(daemonConfig, null, 2));
|
|
353
|
+
io.exec("systemctl reload docker || systemctl restart docker");
|
|
325
354
|
success("Docker configured with production settings");
|
|
326
355
|
}
|
|
327
356
|
/**
|
|
328
357
|
* Setup Docker system prune cron job
|
|
329
358
|
*/
|
|
330
|
-
function setupDockerPrune() {
|
|
359
|
+
function setupDockerPrune(io = nodeIo) {
|
|
331
360
|
step("Setup Docker cleanup cron");
|
|
332
|
-
|
|
361
|
+
io.writeFile("/etc/cron.d/trafic-docker-prune", `# Trafic: Clean up Docker resources weekly
|
|
333
362
|
0 3 * * 0 root docker system prune -af --volumes 2>&1 | logger -t docker-prune
|
|
334
363
|
`);
|
|
335
|
-
exec("chmod 644 /etc/cron.d/trafic-docker-prune");
|
|
364
|
+
io.exec("chmod 644 /etc/cron.d/trafic-docker-prune");
|
|
336
365
|
success("Docker prune scheduled weekly at 3am on Sundays");
|
|
337
366
|
}
|
|
338
367
|
//#endregion
|
|
@@ -340,39 +369,39 @@ function setupDockerPrune() {
|
|
|
340
369
|
/**
|
|
341
370
|
* Install system dependencies required by Trafic
|
|
342
371
|
*/
|
|
343
|
-
function installSystemDeps() {
|
|
372
|
+
function installSystemDeps(io = nodeIo) {
|
|
344
373
|
step("Install system dependencies");
|
|
345
|
-
exec("apt-get update -qq", { silent: true });
|
|
346
|
-
exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y jq curl rsync", { silent: true });
|
|
347
|
-
success("System dependencies installed: jq, curl, rsync");
|
|
374
|
+
io.exec("apt-get update -qq", { silent: true });
|
|
375
|
+
io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y jq curl rsync gnupg ca-certificates", { silent: true });
|
|
376
|
+
success("System dependencies installed: jq, curl, rsync, gnupg, ca-certificates");
|
|
348
377
|
}
|
|
349
378
|
/**
|
|
350
379
|
* Add the official DDEV apt repository and GPG key.
|
|
351
380
|
* Idempotent: safe to call even if the repository is already configured.
|
|
352
381
|
*/
|
|
353
|
-
function addDdevAptRepo() {
|
|
354
|
-
exec("install -m 0755 -d /etc/apt/keyrings", { silent: true });
|
|
355
|
-
exec("curl -fsSL https://pkg.ddev.com/apt/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/ddev.gpg > /dev/null", { silent: true });
|
|
356
|
-
exec("chmod a+r /etc/apt/keyrings/ddev.gpg", { silent: true });
|
|
357
|
-
exec("echo \"deb [signed-by=/etc/apt/keyrings/ddev.gpg] https://pkg.ddev.com/apt/ * *\" | tee /etc/apt/sources.list.d/ddev.list > /dev/null", { silent: true });
|
|
382
|
+
function addDdevAptRepo(io = nodeIo) {
|
|
383
|
+
io.exec("install -m 0755 -d /etc/apt/keyrings", { silent: true });
|
|
384
|
+
io.exec("curl -fsSL https://pkg.ddev.com/apt/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/ddev.gpg > /dev/null", { silent: true });
|
|
385
|
+
io.exec("chmod a+r /etc/apt/keyrings/ddev.gpg", { silent: true });
|
|
386
|
+
io.exec("echo \"deb [signed-by=/etc/apt/keyrings/ddev.gpg] https://pkg.ddev.com/apt/ * *\" | tee /etc/apt/sources.list.d/ddev.list > /dev/null", { silent: true });
|
|
358
387
|
}
|
|
359
388
|
/**
|
|
360
389
|
* Install DDEV
|
|
361
390
|
*/
|
|
362
|
-
function installDdev() {
|
|
391
|
+
function installDdev(io = nodeIo) {
|
|
363
392
|
step("Install DDEV");
|
|
364
|
-
if (commandExists("ddev")) {
|
|
365
|
-
info(`DDEV already installed: ${exec("ddev --version 2>/dev/null | head -1", { silent: true })?.trim() ?? "unknown version"}`);
|
|
393
|
+
if (io.commandExists("ddev")) {
|
|
394
|
+
info(`DDEV already installed: ${io.exec("ddev --version 2>/dev/null | head -1", { silent: true })?.trim() ?? "unknown version"}`);
|
|
366
395
|
return;
|
|
367
396
|
}
|
|
368
397
|
info("Adding DDEV apt repository...");
|
|
369
|
-
addDdevAptRepo();
|
|
398
|
+
addDdevAptRepo(io);
|
|
370
399
|
info("Installing DDEV...");
|
|
371
|
-
exec("apt-get update -qq", { silent: true });
|
|
372
|
-
exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ddev", { silent: true });
|
|
373
|
-
exec("HOME=/home/ddev mkcert -install", { silent: true });
|
|
374
|
-
exec("chown -R ddev:ddev /home/ddev/.local/share/mkcert", { silent: true });
|
|
375
|
-
success(`DDEV installed: ${exec("ddev --version 2>/dev/null | head -1", { silent: true })?.trim() ?? "unknown version"}`);
|
|
400
|
+
io.exec("apt-get update -qq", { silent: true });
|
|
401
|
+
io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ddev", { silent: true });
|
|
402
|
+
io.exec("HOME=/home/ddev mkcert -install", { silent: true });
|
|
403
|
+
io.exec("chown -R ddev:ddev /home/ddev/.local/share/mkcert", { silent: true });
|
|
404
|
+
success(`DDEV installed: ${io.exec("ddev --version 2>/dev/null | head -1", { silent: true })?.trim() ?? "unknown version"}`);
|
|
376
405
|
}
|
|
377
406
|
/**
|
|
378
407
|
* Configure DDEV global settings for production
|
|
@@ -385,10 +414,10 @@ function installDdev() {
|
|
|
385
414
|
*
|
|
386
415
|
* With proper DNS, DDEV won't need sudo for hostname management.
|
|
387
416
|
*/
|
|
388
|
-
function configureDdev(tld, email) {
|
|
417
|
+
function configureDdev(tld, email, io = nodeIo) {
|
|
389
418
|
step("Configure DDEV");
|
|
390
419
|
const ddevCmd = (cmd) => `su - ddev -c '${cmd}'`;
|
|
391
|
-
exec(ddevCmd(`ddev config global --project-tld=${tld} --router-http-port=80 --router-https-port=443 --router-bind-all-interfaces=true --use-letsencrypt=${email ? "true" : "false"} ${email ? `--letsencrypt-email=${email}` : ""} --instrumentation-opt-in=false`), { silent: true });
|
|
420
|
+
io.exec(ddevCmd(`ddev config global --project-tld=${tld} --router-http-port=80 --router-https-port=443 --router-bind-all-interfaces=true --use-letsencrypt=${email ? "true" : "false"} ${email ? `--letsencrypt-email=${email}` : ""} --instrumentation-opt-in=false`), { silent: true });
|
|
392
421
|
success(`DDEV configured with TLD: ${tld}`);
|
|
393
422
|
info(`Ensure DNS is configured: *.${tld} → server IP`);
|
|
394
423
|
if (email) success(`Let's Encrypt enabled with email: ${email}`);
|
|
@@ -400,19 +429,19 @@ function configureDdev(tld, email) {
|
|
|
400
429
|
* Falls back to the default `bridge` network, then to 172.17.0.1.
|
|
401
430
|
* `host.docker.internal` is not available on Linux without extra configuration.
|
|
402
431
|
*/
|
|
403
|
-
function getDockerGatewayIp() {
|
|
404
|
-
const ddevIp = exec("docker network inspect ddev_default --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null", { silent: true })?.trim();
|
|
432
|
+
function getDockerGatewayIp(io = nodeIo) {
|
|
433
|
+
const ddevIp = io.exec("docker network inspect ddev_default --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null", { silent: true })?.trim();
|
|
405
434
|
if (ddevIp) return ddevIp;
|
|
406
|
-
return exec("docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null", { silent: true })?.trim() || "172.17.0.1";
|
|
435
|
+
return io.exec("docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null", { silent: true })?.trim() || "172.17.0.1";
|
|
407
436
|
}
|
|
408
437
|
/**
|
|
409
438
|
* Configure Traefik for forward auth
|
|
410
439
|
*/
|
|
411
|
-
function configureTraefik() {
|
|
440
|
+
function configureTraefik(io = nodeIo) {
|
|
412
441
|
step("Configure Traefik for forward auth");
|
|
413
|
-
exec("mkdir -p /home/ddev/.ddev/traefik/custom-global-config", { silent: true });
|
|
414
|
-
exec("chown -R ddev:ddev /home/ddev/.ddev", { silent: true });
|
|
415
|
-
const gatewayIp = getDockerGatewayIp();
|
|
442
|
+
io.exec("mkdir -p /home/ddev/.ddev/traefik/custom-global-config", { silent: true });
|
|
443
|
+
io.exec("chown -R ddev:ddev /home/ddev/.ddev", { silent: true });
|
|
444
|
+
const gatewayIp = getDockerGatewayIp(io);
|
|
416
445
|
const dynamicConfig = `# Trafic: Forward auth middleware definition
|
|
417
446
|
http:
|
|
418
447
|
middlewares:
|
|
@@ -436,11 +465,11 @@ http:
|
|
|
436
465
|
servers:
|
|
437
466
|
- url: "http://${gatewayIp}:9876"
|
|
438
467
|
`;
|
|
439
|
-
|
|
440
|
-
exec("chown ddev:ddev /home/ddev/.ddev/traefik/trafic.yaml", { silent: true });
|
|
441
|
-
|
|
442
|
-
exec("chown ddev:ddev /home/ddev/.ddev/traefik/custom-global-config/trafic.yaml", { silent: true });
|
|
443
|
-
|
|
468
|
+
io.writeFile("/home/ddev/.ddev/traefik/trafic.yaml", dynamicConfig);
|
|
469
|
+
io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/trafic.yaml", { silent: true });
|
|
470
|
+
io.writeFile("/home/ddev/.ddev/traefik/custom-global-config/trafic.yaml", dynamicConfig);
|
|
471
|
+
io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/custom-global-config/trafic.yaml", { silent: true });
|
|
472
|
+
io.writeFile("/home/ddev/.ddev/traefik/static_config.trafic.yaml", `entryPoints:
|
|
444
473
|
http-80:
|
|
445
474
|
http:
|
|
446
475
|
middlewares:
|
|
@@ -452,52 +481,68 @@ http:
|
|
|
452
481
|
- trafic-auth@file
|
|
453
482
|
- trafic-errors@file
|
|
454
483
|
`);
|
|
455
|
-
exec("chown ddev:ddev /home/ddev/.ddev/traefik/static_config.trafic.yaml", { silent: true });
|
|
484
|
+
io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/static_config.trafic.yaml", { silent: true });
|
|
456
485
|
success("Traefik configured with Trafic middleware");
|
|
457
486
|
}
|
|
458
487
|
//#endregion
|
|
459
488
|
//#region src/setup/agent.ts
|
|
489
|
+
/** Node.js major version required by the agent */
|
|
490
|
+
var NODE_MAJOR = 24;
|
|
491
|
+
/** NodeSource keyring path */
|
|
492
|
+
var NODESOURCE_KEYRING = "/etc/apt/keyrings/nodesource.gpg";
|
|
460
493
|
/**
|
|
461
|
-
*
|
|
494
|
+
* Add the NodeSource apt repository and GPG key.
|
|
495
|
+
* Idempotent: safe to call even if the repository is already configured.
|
|
496
|
+
*
|
|
497
|
+
* nodejs.org points at NodeSource for apt installs. The Ubuntu 24.04
|
|
498
|
+
* `nodejs` package is 18.x, far below what the agent needs.
|
|
499
|
+
*/
|
|
500
|
+
function addNodeSourceAptRepo(io = nodeIo) {
|
|
501
|
+
io.exec("install -m 0755 -d /etc/apt/keyrings", { silent: true });
|
|
502
|
+
io.exec("curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /tmp/trafic-nodesource.key", { silent: true });
|
|
503
|
+
io.exec("gpg --batch --yes --dearmor -o /tmp/trafic-nodesource.gpg /tmp/trafic-nodesource.key", { silent: true });
|
|
504
|
+
io.exec(`install -m 0644 /tmp/trafic-nodesource.gpg ${NODESOURCE_KEYRING}`, { silent: true });
|
|
505
|
+
io.exec("rm -f /tmp/trafic-nodesource.key /tmp/trafic-nodesource.gpg", { silent: true });
|
|
506
|
+
io.exec(`echo "deb [signed-by=${NODESOURCE_KEYRING}] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list > /dev/null`, { silent: true });
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Install Node.js from the NodeSource apt repository.
|
|
510
|
+
*
|
|
511
|
+
* apt keeps node and npm in /usr/bin — on root's PATH, so `which
|
|
512
|
+
* trafic-agent` resolves for the systemd unit — and unattended-upgrades
|
|
513
|
+
* patches them, which a version manager install never gets.
|
|
462
514
|
*/
|
|
463
|
-
function installNode() {
|
|
515
|
+
function installNode(io = nodeIo) {
|
|
464
516
|
step("Install Node.js");
|
|
465
|
-
if (commandExists("node")) {
|
|
466
|
-
info(`Node.js already installed: ${exec("node --version", { silent: true })?.trim()}`);
|
|
517
|
+
if (io.commandExists("node")) {
|
|
518
|
+
info(`Node.js already installed: ${io.exec("node --version", { silent: true })?.trim()}`);
|
|
467
519
|
return;
|
|
468
520
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
`);
|
|
475
|
-
exec("chmod 644 /etc/profile.d/fnm.sh");
|
|
476
|
-
exec("export FNM_DIR=/opt/fnm && /opt/fnm/fnm install 24");
|
|
477
|
-
exec("export FNM_DIR=/opt/fnm && /opt/fnm/fnm default 24");
|
|
478
|
-
exec("ln -sf /opt/fnm/aliases/default/bin/node /usr/local/bin/node || true");
|
|
479
|
-
exec("ln -sf /opt/fnm/aliases/default/bin/npm /usr/local/bin/npm || true");
|
|
480
|
-
exec("ln -sf /opt/fnm/aliases/default/bin/npx /usr/local/bin/npx || true");
|
|
481
|
-
success("Node.js 24 installed via fnm");
|
|
521
|
+
info("Adding NodeSource apt repository...");
|
|
522
|
+
addNodeSourceAptRepo(io);
|
|
523
|
+
info("Installing Node.js...");
|
|
524
|
+
io.exec("apt-get update -qq", { silent: true });
|
|
525
|
+
io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y nodejs", { silent: true });
|
|
526
|
+
success(`Node.js ${NODE_MAJOR} installed from apt`);
|
|
482
527
|
}
|
|
483
528
|
/**
|
|
484
529
|
* Install the Trafic agent
|
|
485
530
|
*/
|
|
486
|
-
function installAgent() {
|
|
531
|
+
function installAgent(io = nodeIo) {
|
|
487
532
|
step("Install Trafic agent");
|
|
488
|
-
exec("npm install -g @studiometa/trafic-agent");
|
|
533
|
+
io.exec("npm install -g @studiometa/trafic-agent");
|
|
489
534
|
success("Trafic agent installed");
|
|
490
535
|
}
|
|
491
536
|
/**
|
|
492
537
|
* Create the agent configuration
|
|
493
538
|
*/
|
|
494
|
-
function createAgentConfig(tld, _email) {
|
|
539
|
+
function createAgentConfig(tld, _email, io = nodeIo) {
|
|
495
540
|
step("Create agent configuration");
|
|
496
|
-
exec("mkdir -p /etc/trafic");
|
|
497
|
-
exec("mkdir -p /var/lib/trafic");
|
|
498
|
-
exec("chown ddev:ddev /var/lib/trafic");
|
|
541
|
+
io.exec("mkdir -p /etc/trafic");
|
|
542
|
+
io.exec("mkdir -p /var/lib/trafic");
|
|
543
|
+
io.exec("chown ddev:ddev /var/lib/trafic");
|
|
499
544
|
const configPath = "/etc/trafic/config.toml";
|
|
500
|
-
if (
|
|
545
|
+
if (io.fileExists(configPath)) {
|
|
501
546
|
info("Configuration already exists at /etc/trafic/config.toml — skipping");
|
|
502
547
|
info("Edit this file to configure authentication");
|
|
503
548
|
return;
|
|
@@ -528,16 +573,16 @@ tokens = []
|
|
|
528
573
|
basic_auth = []
|
|
529
574
|
rules = []
|
|
530
575
|
`;
|
|
531
|
-
|
|
532
|
-
exec("chmod 640 /etc/trafic/config.toml");
|
|
533
|
-
exec("chown root:ddev /etc/trafic/config.toml");
|
|
576
|
+
io.writeFile(configPath, config);
|
|
577
|
+
io.exec("chmod 640 /etc/trafic/config.toml");
|
|
578
|
+
io.exec("chown root:ddev /etc/trafic/config.toml");
|
|
534
579
|
success("Configuration created at /etc/trafic/config.toml");
|
|
535
580
|
info("Edit this file to configure authentication");
|
|
536
581
|
}
|
|
537
582
|
/**
|
|
538
583
|
* Create systemd service for the agent
|
|
539
584
|
*/
|
|
540
|
-
function createSystemdService() {
|
|
585
|
+
function createSystemdService(io = nodeIo) {
|
|
541
586
|
step("Create systemd service");
|
|
542
587
|
const serviceContent = `[Unit]
|
|
543
588
|
Description=Trafic Agent - DDEV preview environment manager
|
|
@@ -548,7 +593,7 @@ Requires=docker.service
|
|
|
548
593
|
Type=simple
|
|
549
594
|
User=ddev
|
|
550
595
|
Group=ddev
|
|
551
|
-
ExecStart=${exec("which trafic-agent", { silent: true })?.trim() || "/usr/bin/trafic-agent"} start --config /etc/trafic/config.toml
|
|
596
|
+
ExecStart=${io.exec("which trafic-agent", { silent: true })?.trim() || "/usr/bin/trafic-agent"} start --config /etc/trafic/config.toml
|
|
552
597
|
Restart=always
|
|
553
598
|
RestartSec=5
|
|
554
599
|
StandardOutput=journal
|
|
@@ -572,21 +617,35 @@ PrivateTmp=true
|
|
|
572
617
|
[Install]
|
|
573
618
|
WantedBy=multi-user.target
|
|
574
619
|
`;
|
|
575
|
-
|
|
576
|
-
exec("chmod 644 /etc/systemd/system/trafic-agent.service");
|
|
577
|
-
exec("systemctl daemon-reload");
|
|
578
|
-
exec("systemctl enable trafic-agent");
|
|
579
|
-
exec("systemctl start trafic-agent");
|
|
620
|
+
io.writeFile("/etc/systemd/system/trafic-agent.service", serviceContent);
|
|
621
|
+
io.exec("chmod 644 /etc/systemd/system/trafic-agent.service");
|
|
622
|
+
io.exec("systemctl daemon-reload");
|
|
623
|
+
io.exec("systemctl enable trafic-agent");
|
|
624
|
+
io.exec("systemctl start trafic-agent");
|
|
580
625
|
success("Trafic agent service started");
|
|
581
626
|
info("View logs: journalctl -u trafic-agent -f");
|
|
582
627
|
}
|
|
583
628
|
//#endregion
|
|
584
629
|
//#region src/setup/hardening.ts
|
|
585
630
|
/**
|
|
631
|
+
* Resolve the users listed in AllowUsers.
|
|
632
|
+
*
|
|
633
|
+
* The user who invoked sudo is always included. Without them, the
|
|
634
|
+
* AllowUsers directive locks them out on their next connection, and
|
|
635
|
+
* reloading sshd keeps the current session alive so nothing looks wrong
|
|
636
|
+
* until it is too late. root needs no entry — it is always listed.
|
|
637
|
+
*/
|
|
638
|
+
function resolveAllowedUsers(allowedUsers, sudoUser) {
|
|
639
|
+
const users = allowedUsers.map((user) => user.trim()).filter(Boolean);
|
|
640
|
+
if (sudoUser && sudoUser !== "root" && !users.includes(sudoUser)) users.push(sudoUser);
|
|
641
|
+
return users;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
586
644
|
* Configure SSH hardening
|
|
587
645
|
*/
|
|
588
|
-
function hardenSsh(allowedUsers = ["ddev"]) {
|
|
646
|
+
function hardenSsh(allowedUsers = ["ddev"], io = nodeIo) {
|
|
589
647
|
step("Harden SSH configuration");
|
|
648
|
+
const users = resolveAllowedUsers(allowedUsers, io.env.SUDO_USER);
|
|
590
649
|
const sshConfig = `# Trafic SSH hardening
|
|
591
650
|
# Generated by trafic setup
|
|
592
651
|
|
|
@@ -605,7 +664,7 @@ ClientAliveInterval 300
|
|
|
605
664
|
ClientAliveCountMax 2
|
|
606
665
|
|
|
607
666
|
# Allowed users (root kept for emergency access)
|
|
608
|
-
AllowUsers root ${
|
|
667
|
+
AllowUsers root ${users.join(" ")}
|
|
609
668
|
|
|
610
669
|
# Disable X11 forwarding
|
|
611
670
|
X11Forwarding no
|
|
@@ -618,40 +677,40 @@ KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256
|
|
|
618
677
|
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
|
|
619
678
|
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
|
|
620
679
|
`;
|
|
621
|
-
|
|
622
|
-
exec("chmod 644 /etc/ssh/sshd_config.d/trafic.conf");
|
|
623
|
-
if (exec("sshd -t 2>&1 || echo 'error'", { silent: true })?.includes("error")) {
|
|
680
|
+
io.writeFile("/etc/ssh/sshd_config.d/trafic.conf", sshConfig);
|
|
681
|
+
io.exec("chmod 644 /etc/ssh/sshd_config.d/trafic.conf");
|
|
682
|
+
if (io.exec("sshd -t 2>&1 || echo 'error'", { silent: true })?.includes("error")) {
|
|
624
683
|
warn("SSH config test failed, reverting...");
|
|
625
|
-
exec("rm /etc/ssh/sshd_config.d/trafic.conf");
|
|
684
|
+
io.exec("rm /etc/ssh/sshd_config.d/trafic.conf");
|
|
626
685
|
return;
|
|
627
686
|
}
|
|
628
|
-
exec("systemctl reload ssh || systemctl reload sshd");
|
|
687
|
+
io.exec("systemctl reload ssh || systemctl reload sshd");
|
|
629
688
|
success("SSH hardened: password auth disabled, root key-only login retained");
|
|
630
|
-
info(`Allowed users: root, ${
|
|
689
|
+
info(`Allowed users: root, ${users.join(", ")}`);
|
|
631
690
|
}
|
|
632
691
|
/**
|
|
633
692
|
* Configure UFW firewall
|
|
634
693
|
*/
|
|
635
|
-
function configureFirewall() {
|
|
694
|
+
function configureFirewall(io = nodeIo) {
|
|
636
695
|
step("Configure firewall (UFW)");
|
|
637
|
-
if (!commandExists("ufw")) exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ufw", { silent: true });
|
|
638
|
-
exec("ufw --force reset", { silent: true });
|
|
639
|
-
exec("ufw default deny incoming");
|
|
640
|
-
exec("ufw default allow outgoing");
|
|
641
|
-
exec("ufw allow 22/tcp comment 'SSH'");
|
|
642
|
-
exec("ufw allow 80/tcp comment 'HTTP'");
|
|
643
|
-
exec("ufw allow 443/tcp comment 'HTTPS'");
|
|
644
|
-
exec("ufw allow from 172.16.0.0/12 to any port 9876 comment 'trafic-agent from Docker'");
|
|
645
|
-
exec("ufw --force enable");
|
|
696
|
+
if (!io.commandExists("ufw")) io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ufw", { silent: true });
|
|
697
|
+
io.exec("ufw --force reset", { silent: true });
|
|
698
|
+
io.exec("ufw default deny incoming");
|
|
699
|
+
io.exec("ufw default allow outgoing");
|
|
700
|
+
io.exec("ufw allow 22/tcp comment 'SSH'");
|
|
701
|
+
io.exec("ufw allow 80/tcp comment 'HTTP'");
|
|
702
|
+
io.exec("ufw allow 443/tcp comment 'HTTPS'");
|
|
703
|
+
io.exec("ufw allow from 172.16.0.0/12 to any port 9876 comment 'trafic-agent from Docker'");
|
|
704
|
+
io.exec("ufw --force enable");
|
|
646
705
|
success("Firewall enabled: SSH (22), HTTP (80), HTTPS (443), trafic-agent (9876 from Docker) allowed");
|
|
647
706
|
}
|
|
648
707
|
/**
|
|
649
708
|
* Configure automatic security updates
|
|
650
709
|
*/
|
|
651
|
-
function configureUnattendedUpgrades() {
|
|
710
|
+
function configureUnattendedUpgrades(io = nodeIo) {
|
|
652
711
|
step("Configure automatic security updates");
|
|
653
|
-
exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y unattended-upgrades apt-listchanges", { silent: true });
|
|
654
|
-
|
|
712
|
+
io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y unattended-upgrades apt-listchanges", { silent: true });
|
|
713
|
+
io.writeFile("/etc/apt/apt.conf.d/50unattended-upgrades", `// Trafic: Automatic security updates
|
|
655
714
|
Unattended-Upgrade::Allowed-Origins {
|
|
656
715
|
"\${distro_id}:\${distro_codename}";
|
|
657
716
|
"\${distro_id}:\${distro_codename}-security";
|
|
@@ -668,22 +727,22 @@ Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
|
|
668
727
|
// Email notifications (optional)
|
|
669
728
|
// Unattended-Upgrade::Mail "root";
|
|
670
729
|
`);
|
|
671
|
-
|
|
730
|
+
io.writeFile("/etc/apt/apt.conf.d/20auto-upgrades", `APT::Periodic::Update-Package-Lists "1";
|
|
672
731
|
APT::Periodic::Download-Upgradeable-Packages "1";
|
|
673
732
|
APT::Periodic::AutocleanInterval "7";
|
|
674
733
|
APT::Periodic::Unattended-Upgrade "1";
|
|
675
734
|
`);
|
|
676
|
-
exec("systemctl enable unattended-upgrades");
|
|
677
|
-
exec("systemctl start unattended-upgrades");
|
|
735
|
+
io.exec("systemctl enable unattended-upgrades");
|
|
736
|
+
io.exec("systemctl start unattended-upgrades");
|
|
678
737
|
success("Automatic security updates enabled");
|
|
679
738
|
}
|
|
680
739
|
/**
|
|
681
740
|
* Configure Fail2ban
|
|
682
741
|
*/
|
|
683
|
-
function configureFail2ban() {
|
|
742
|
+
function configureFail2ban(io = nodeIo) {
|
|
684
743
|
step("Configure Fail2ban");
|
|
685
|
-
if (!commandExists("fail2ban-client")) exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y fail2ban", { silent: true });
|
|
686
|
-
|
|
744
|
+
if (!io.commandExists("fail2ban-client")) io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y fail2ban", { silent: true });
|
|
745
|
+
io.writeFile("/etc/fail2ban/jail.local", `# Trafic: Fail2ban configuration
|
|
687
746
|
[DEFAULT]
|
|
688
747
|
bantime = 3600
|
|
689
748
|
findtime = 600
|
|
@@ -698,55 +757,55 @@ logpath = /var/log/auth.log
|
|
|
698
757
|
maxretry = 3
|
|
699
758
|
bantime = 7200
|
|
700
759
|
`);
|
|
701
|
-
exec("chmod 644 /etc/fail2ban/jail.local");
|
|
702
|
-
exec("systemctl enable fail2ban");
|
|
703
|
-
exec("systemctl restart fail2ban");
|
|
760
|
+
io.exec("chmod 644 /etc/fail2ban/jail.local");
|
|
761
|
+
io.exec("systemctl enable fail2ban");
|
|
762
|
+
io.exec("systemctl restart fail2ban");
|
|
704
763
|
success("Fail2ban enabled: SSH brute force protection active");
|
|
705
764
|
}
|
|
706
765
|
/**
|
|
707
766
|
* Configure system limits
|
|
708
767
|
*/
|
|
709
|
-
function configureSystemLimits() {
|
|
768
|
+
function configureSystemLimits(io = nodeIo) {
|
|
710
769
|
step("Configure system limits");
|
|
711
|
-
|
|
770
|
+
io.writeFile("/etc/security/limits.d/trafic.conf", `# Trafic: System limits for ddev user
|
|
712
771
|
ddev soft nofile 65536
|
|
713
772
|
ddev hard nofile 65536
|
|
714
773
|
ddev soft nproc 4096
|
|
715
774
|
ddev hard nproc 4096
|
|
716
775
|
`);
|
|
717
|
-
exec("chmod 644 /etc/security/limits.d/trafic.conf");
|
|
776
|
+
io.exec("chmod 644 /etc/security/limits.d/trafic.conf");
|
|
718
777
|
success("System limits configured for ddev user");
|
|
719
778
|
}
|
|
720
779
|
/**
|
|
721
780
|
* Set proper file permissions
|
|
722
781
|
*/
|
|
723
|
-
function configureFilePermissions() {
|
|
782
|
+
function configureFilePermissions(io = nodeIo) {
|
|
724
783
|
step("Configure file permissions");
|
|
725
|
-
exec("mkdir -p /etc/trafic");
|
|
726
|
-
exec("mkdir -p /var/lib/trafic");
|
|
727
|
-
exec("mkdir -p /var/backups/trafic");
|
|
728
|
-
exec("chmod 755 /etc/trafic");
|
|
729
|
-
exec("chmod 700 /var/lib/trafic");
|
|
730
|
-
exec("chmod 700 /var/backups/trafic");
|
|
731
|
-
exec("chown root:ddev /etc/trafic");
|
|
732
|
-
exec("chown ddev:ddev /var/lib/trafic");
|
|
733
|
-
exec("chown ddev:ddev /var/backups/trafic");
|
|
734
|
-
if (
|
|
735
|
-
exec("chmod 640 /etc/trafic/config.toml");
|
|
736
|
-
exec("chown root:ddev /etc/trafic/config.toml");
|
|
784
|
+
io.exec("mkdir -p /etc/trafic");
|
|
785
|
+
io.exec("mkdir -p /var/lib/trafic");
|
|
786
|
+
io.exec("mkdir -p /var/backups/trafic");
|
|
787
|
+
io.exec("chmod 755 /etc/trafic");
|
|
788
|
+
io.exec("chmod 700 /var/lib/trafic");
|
|
789
|
+
io.exec("chmod 700 /var/backups/trafic");
|
|
790
|
+
io.exec("chown root:ddev /etc/trafic");
|
|
791
|
+
io.exec("chown ddev:ddev /var/lib/trafic");
|
|
792
|
+
io.exec("chown ddev:ddev /var/backups/trafic");
|
|
793
|
+
if (io.fileExists("/etc/trafic/config.toml")) {
|
|
794
|
+
io.exec("chmod 640 /etc/trafic/config.toml");
|
|
795
|
+
io.exec("chown root:ddev /etc/trafic/config.toml");
|
|
737
796
|
}
|
|
738
797
|
success("File permissions configured");
|
|
739
798
|
}
|
|
740
799
|
/**
|
|
741
800
|
* Run all hardening steps
|
|
742
801
|
*/
|
|
743
|
-
function hardenServer(sshUsers = ["ddev"]) {
|
|
744
|
-
hardenSsh(sshUsers);
|
|
745
|
-
configureFirewall();
|
|
746
|
-
configureUnattendedUpgrades();
|
|
747
|
-
configureFail2ban();
|
|
748
|
-
configureSystemLimits();
|
|
749
|
-
configureFilePermissions();
|
|
802
|
+
function hardenServer(sshUsers = ["ddev"], io = nodeIo) {
|
|
803
|
+
hardenSsh(sshUsers, io);
|
|
804
|
+
configureFirewall(io);
|
|
805
|
+
configureUnattendedUpgrades(io);
|
|
806
|
+
configureFail2ban(io);
|
|
807
|
+
configureSystemLimits(io);
|
|
808
|
+
configureFilePermissions(io);
|
|
750
809
|
}
|
|
751
810
|
//#endregion
|
|
752
811
|
//#region src/setup/audit.ts
|
|
@@ -1324,4 +1383,4 @@ async function audit() {
|
|
|
1324
1383
|
//#endregion
|
|
1325
1384
|
export { validateConfig as _, printAuditResults as a, info as c, success as d, warn as f, parseDuration as g, loadConfig as h, runPendingMigrations as i, isRoot as l, stopIdleProjects as m, setup as n, runAudit as o, startIdleScheduler as p, listMigrations as r, exec as s, audit as t, step as u };
|
|
1326
1385
|
|
|
1327
|
-
//# sourceMappingURL=setup-
|
|
1386
|
+
//# sourceMappingURL=setup-4TI2lreq.js.map
|