@studiometa/trafic-agent 0.1.25 → 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 +6 -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-D61uhYiJ.js → setup-4TI2lreq.js} +192 -149
- package/dist/setup-4TI2lreq.js.map +1 -0
- package/package.json +1 -1
- package/dist/setup-D61uhYiJ.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 gnupg ca-certificates", { silent: true });
|
|
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 });
|
|
347
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,7 +481,7 @@ 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
|
|
@@ -468,13 +497,13 @@ var NODESOURCE_KEYRING = "/etc/apt/keyrings/nodesource.gpg";
|
|
|
468
497
|
* nodejs.org points at NodeSource for apt installs. The Ubuntu 24.04
|
|
469
498
|
* `nodejs` package is 18.x, far below what the agent needs.
|
|
470
499
|
*/
|
|
471
|
-
function addNodeSourceAptRepo() {
|
|
472
|
-
exec("install -m 0755 -d /etc/apt/keyrings", { silent: true });
|
|
473
|
-
exec("curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /tmp/trafic-nodesource.key", { silent: true });
|
|
474
|
-
exec("gpg --batch --yes --dearmor -o /tmp/trafic-nodesource.gpg /tmp/trafic-nodesource.key", { silent: true });
|
|
475
|
-
exec(`install -m 0644 /tmp/trafic-nodesource.gpg ${NODESOURCE_KEYRING}`, { silent: true });
|
|
476
|
-
exec("rm -f /tmp/trafic-nodesource.key /tmp/trafic-nodesource.gpg", { silent: true });
|
|
477
|
-
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 });
|
|
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 });
|
|
478
507
|
}
|
|
479
508
|
/**
|
|
480
509
|
* Install Node.js from the NodeSource apt repository.
|
|
@@ -483,37 +512,37 @@ function addNodeSourceAptRepo() {
|
|
|
483
512
|
* trafic-agent` resolves for the systemd unit — and unattended-upgrades
|
|
484
513
|
* patches them, which a version manager install never gets.
|
|
485
514
|
*/
|
|
486
|
-
function installNode() {
|
|
515
|
+
function installNode(io = nodeIo) {
|
|
487
516
|
step("Install Node.js");
|
|
488
|
-
if (commandExists("node")) {
|
|
489
|
-
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()}`);
|
|
490
519
|
return;
|
|
491
520
|
}
|
|
492
521
|
info("Adding NodeSource apt repository...");
|
|
493
|
-
addNodeSourceAptRepo();
|
|
522
|
+
addNodeSourceAptRepo(io);
|
|
494
523
|
info("Installing Node.js...");
|
|
495
|
-
exec("apt-get update -qq", { silent: true });
|
|
496
|
-
exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y nodejs", { silent: true });
|
|
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 });
|
|
497
526
|
success(`Node.js ${NODE_MAJOR} installed from apt`);
|
|
498
527
|
}
|
|
499
528
|
/**
|
|
500
529
|
* Install the Trafic agent
|
|
501
530
|
*/
|
|
502
|
-
function installAgent() {
|
|
531
|
+
function installAgent(io = nodeIo) {
|
|
503
532
|
step("Install Trafic agent");
|
|
504
|
-
exec("npm install -g @studiometa/trafic-agent");
|
|
533
|
+
io.exec("npm install -g @studiometa/trafic-agent");
|
|
505
534
|
success("Trafic agent installed");
|
|
506
535
|
}
|
|
507
536
|
/**
|
|
508
537
|
* Create the agent configuration
|
|
509
538
|
*/
|
|
510
|
-
function createAgentConfig(tld, _email) {
|
|
539
|
+
function createAgentConfig(tld, _email, io = nodeIo) {
|
|
511
540
|
step("Create agent configuration");
|
|
512
|
-
exec("mkdir -p /etc/trafic");
|
|
513
|
-
exec("mkdir -p /var/lib/trafic");
|
|
514
|
-
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");
|
|
515
544
|
const configPath = "/etc/trafic/config.toml";
|
|
516
|
-
if (
|
|
545
|
+
if (io.fileExists(configPath)) {
|
|
517
546
|
info("Configuration already exists at /etc/trafic/config.toml — skipping");
|
|
518
547
|
info("Edit this file to configure authentication");
|
|
519
548
|
return;
|
|
@@ -544,16 +573,16 @@ tokens = []
|
|
|
544
573
|
basic_auth = []
|
|
545
574
|
rules = []
|
|
546
575
|
`;
|
|
547
|
-
|
|
548
|
-
exec("chmod 640 /etc/trafic/config.toml");
|
|
549
|
-
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");
|
|
550
579
|
success("Configuration created at /etc/trafic/config.toml");
|
|
551
580
|
info("Edit this file to configure authentication");
|
|
552
581
|
}
|
|
553
582
|
/**
|
|
554
583
|
* Create systemd service for the agent
|
|
555
584
|
*/
|
|
556
|
-
function createSystemdService() {
|
|
585
|
+
function createSystemdService(io = nodeIo) {
|
|
557
586
|
step("Create systemd service");
|
|
558
587
|
const serviceContent = `[Unit]
|
|
559
588
|
Description=Trafic Agent - DDEV preview environment manager
|
|
@@ -564,7 +593,7 @@ Requires=docker.service
|
|
|
564
593
|
Type=simple
|
|
565
594
|
User=ddev
|
|
566
595
|
Group=ddev
|
|
567
|
-
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
|
|
568
597
|
Restart=always
|
|
569
598
|
RestartSec=5
|
|
570
599
|
StandardOutput=journal
|
|
@@ -588,21 +617,35 @@ PrivateTmp=true
|
|
|
588
617
|
[Install]
|
|
589
618
|
WantedBy=multi-user.target
|
|
590
619
|
`;
|
|
591
|
-
|
|
592
|
-
exec("chmod 644 /etc/systemd/system/trafic-agent.service");
|
|
593
|
-
exec("systemctl daemon-reload");
|
|
594
|
-
exec("systemctl enable trafic-agent");
|
|
595
|
-
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");
|
|
596
625
|
success("Trafic agent service started");
|
|
597
626
|
info("View logs: journalctl -u trafic-agent -f");
|
|
598
627
|
}
|
|
599
628
|
//#endregion
|
|
600
629
|
//#region src/setup/hardening.ts
|
|
601
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
|
+
/**
|
|
602
644
|
* Configure SSH hardening
|
|
603
645
|
*/
|
|
604
|
-
function hardenSsh(allowedUsers = ["ddev"]) {
|
|
646
|
+
function hardenSsh(allowedUsers = ["ddev"], io = nodeIo) {
|
|
605
647
|
step("Harden SSH configuration");
|
|
648
|
+
const users = resolveAllowedUsers(allowedUsers, io.env.SUDO_USER);
|
|
606
649
|
const sshConfig = `# Trafic SSH hardening
|
|
607
650
|
# Generated by trafic setup
|
|
608
651
|
|
|
@@ -621,7 +664,7 @@ ClientAliveInterval 300
|
|
|
621
664
|
ClientAliveCountMax 2
|
|
622
665
|
|
|
623
666
|
# Allowed users (root kept for emergency access)
|
|
624
|
-
AllowUsers root ${
|
|
667
|
+
AllowUsers root ${users.join(" ")}
|
|
625
668
|
|
|
626
669
|
# Disable X11 forwarding
|
|
627
670
|
X11Forwarding no
|
|
@@ -634,40 +677,40 @@ KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256
|
|
|
634
677
|
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
|
|
635
678
|
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
|
|
636
679
|
`;
|
|
637
|
-
|
|
638
|
-
exec("chmod 644 /etc/ssh/sshd_config.d/trafic.conf");
|
|
639
|
-
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")) {
|
|
640
683
|
warn("SSH config test failed, reverting...");
|
|
641
|
-
exec("rm /etc/ssh/sshd_config.d/trafic.conf");
|
|
684
|
+
io.exec("rm /etc/ssh/sshd_config.d/trafic.conf");
|
|
642
685
|
return;
|
|
643
686
|
}
|
|
644
|
-
exec("systemctl reload ssh || systemctl reload sshd");
|
|
687
|
+
io.exec("systemctl reload ssh || systemctl reload sshd");
|
|
645
688
|
success("SSH hardened: password auth disabled, root key-only login retained");
|
|
646
|
-
info(`Allowed users: root, ${
|
|
689
|
+
info(`Allowed users: root, ${users.join(", ")}`);
|
|
647
690
|
}
|
|
648
691
|
/**
|
|
649
692
|
* Configure UFW firewall
|
|
650
693
|
*/
|
|
651
|
-
function configureFirewall() {
|
|
694
|
+
function configureFirewall(io = nodeIo) {
|
|
652
695
|
step("Configure firewall (UFW)");
|
|
653
|
-
if (!commandExists("ufw")) exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ufw", { silent: true });
|
|
654
|
-
exec("ufw --force reset", { silent: true });
|
|
655
|
-
exec("ufw default deny incoming");
|
|
656
|
-
exec("ufw default allow outgoing");
|
|
657
|
-
exec("ufw allow 22/tcp comment 'SSH'");
|
|
658
|
-
exec("ufw allow 80/tcp comment 'HTTP'");
|
|
659
|
-
exec("ufw allow 443/tcp comment 'HTTPS'");
|
|
660
|
-
exec("ufw allow from 172.16.0.0/12 to any port 9876 comment 'trafic-agent from Docker'");
|
|
661
|
-
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");
|
|
662
705
|
success("Firewall enabled: SSH (22), HTTP (80), HTTPS (443), trafic-agent (9876 from Docker) allowed");
|
|
663
706
|
}
|
|
664
707
|
/**
|
|
665
708
|
* Configure automatic security updates
|
|
666
709
|
*/
|
|
667
|
-
function configureUnattendedUpgrades() {
|
|
710
|
+
function configureUnattendedUpgrades(io = nodeIo) {
|
|
668
711
|
step("Configure automatic security updates");
|
|
669
|
-
exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y unattended-upgrades apt-listchanges", { silent: true });
|
|
670
|
-
|
|
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
|
|
671
714
|
Unattended-Upgrade::Allowed-Origins {
|
|
672
715
|
"\${distro_id}:\${distro_codename}";
|
|
673
716
|
"\${distro_id}:\${distro_codename}-security";
|
|
@@ -684,22 +727,22 @@ Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
|
|
684
727
|
// Email notifications (optional)
|
|
685
728
|
// Unattended-Upgrade::Mail "root";
|
|
686
729
|
`);
|
|
687
|
-
|
|
730
|
+
io.writeFile("/etc/apt/apt.conf.d/20auto-upgrades", `APT::Periodic::Update-Package-Lists "1";
|
|
688
731
|
APT::Periodic::Download-Upgradeable-Packages "1";
|
|
689
732
|
APT::Periodic::AutocleanInterval "7";
|
|
690
733
|
APT::Periodic::Unattended-Upgrade "1";
|
|
691
734
|
`);
|
|
692
|
-
exec("systemctl enable unattended-upgrades");
|
|
693
|
-
exec("systemctl start unattended-upgrades");
|
|
735
|
+
io.exec("systemctl enable unattended-upgrades");
|
|
736
|
+
io.exec("systemctl start unattended-upgrades");
|
|
694
737
|
success("Automatic security updates enabled");
|
|
695
738
|
}
|
|
696
739
|
/**
|
|
697
740
|
* Configure Fail2ban
|
|
698
741
|
*/
|
|
699
|
-
function configureFail2ban() {
|
|
742
|
+
function configureFail2ban(io = nodeIo) {
|
|
700
743
|
step("Configure Fail2ban");
|
|
701
|
-
if (!commandExists("fail2ban-client")) exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y fail2ban", { silent: true });
|
|
702
|
-
|
|
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
|
|
703
746
|
[DEFAULT]
|
|
704
747
|
bantime = 3600
|
|
705
748
|
findtime = 600
|
|
@@ -714,55 +757,55 @@ logpath = /var/log/auth.log
|
|
|
714
757
|
maxretry = 3
|
|
715
758
|
bantime = 7200
|
|
716
759
|
`);
|
|
717
|
-
exec("chmod 644 /etc/fail2ban/jail.local");
|
|
718
|
-
exec("systemctl enable fail2ban");
|
|
719
|
-
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");
|
|
720
763
|
success("Fail2ban enabled: SSH brute force protection active");
|
|
721
764
|
}
|
|
722
765
|
/**
|
|
723
766
|
* Configure system limits
|
|
724
767
|
*/
|
|
725
|
-
function configureSystemLimits() {
|
|
768
|
+
function configureSystemLimits(io = nodeIo) {
|
|
726
769
|
step("Configure system limits");
|
|
727
|
-
|
|
770
|
+
io.writeFile("/etc/security/limits.d/trafic.conf", `# Trafic: System limits for ddev user
|
|
728
771
|
ddev soft nofile 65536
|
|
729
772
|
ddev hard nofile 65536
|
|
730
773
|
ddev soft nproc 4096
|
|
731
774
|
ddev hard nproc 4096
|
|
732
775
|
`);
|
|
733
|
-
exec("chmod 644 /etc/security/limits.d/trafic.conf");
|
|
776
|
+
io.exec("chmod 644 /etc/security/limits.d/trafic.conf");
|
|
734
777
|
success("System limits configured for ddev user");
|
|
735
778
|
}
|
|
736
779
|
/**
|
|
737
780
|
* Set proper file permissions
|
|
738
781
|
*/
|
|
739
|
-
function configureFilePermissions() {
|
|
782
|
+
function configureFilePermissions(io = nodeIo) {
|
|
740
783
|
step("Configure file permissions");
|
|
741
|
-
exec("mkdir -p /etc/trafic");
|
|
742
|
-
exec("mkdir -p /var/lib/trafic");
|
|
743
|
-
exec("mkdir -p /var/backups/trafic");
|
|
744
|
-
exec("chmod 755 /etc/trafic");
|
|
745
|
-
exec("chmod 700 /var/lib/trafic");
|
|
746
|
-
exec("chmod 700 /var/backups/trafic");
|
|
747
|
-
exec("chown root:ddev /etc/trafic");
|
|
748
|
-
exec("chown ddev:ddev /var/lib/trafic");
|
|
749
|
-
exec("chown ddev:ddev /var/backups/trafic");
|
|
750
|
-
if (
|
|
751
|
-
exec("chmod 640 /etc/trafic/config.toml");
|
|
752
|
-
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");
|
|
753
796
|
}
|
|
754
797
|
success("File permissions configured");
|
|
755
798
|
}
|
|
756
799
|
/**
|
|
757
800
|
* Run all hardening steps
|
|
758
801
|
*/
|
|
759
|
-
function hardenServer(sshUsers = ["ddev"]) {
|
|
760
|
-
hardenSsh(sshUsers);
|
|
761
|
-
configureFirewall();
|
|
762
|
-
configureUnattendedUpgrades();
|
|
763
|
-
configureFail2ban();
|
|
764
|
-
configureSystemLimits();
|
|
765
|
-
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);
|
|
766
809
|
}
|
|
767
810
|
//#endregion
|
|
768
811
|
//#region src/setup/audit.ts
|
|
@@ -1340,4 +1383,4 @@ async function audit() {
|
|
|
1340
1383
|
//#endregion
|
|
1341
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 };
|
|
1342
1385
|
|
|
1343
|
-
//# sourceMappingURL=setup-
|
|
1386
|
+
//# sourceMappingURL=setup-4TI2lreq.js.map
|