@studiometa/trafic-agent 0.1.7 → 0.1.9
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 +92 -89
- package/dist/cli.js.map +1 -1
- package/dist/index.js +3 -41
- package/dist/server-D0N_6y4c.js +687 -0
- package/dist/server-D0N_6y4c.js.map +1 -0
- package/dist/server.js +2 -9
- package/dist/setup/ddev.d.ts.map +1 -1
- package/dist/setup-CVf8YfPW.js +941 -0
- package/dist/setup-CVf8YfPW.js.map +1 -0
- package/package.json +6 -6
- package/dist/index-Dip9RQ_T.js +0 -904
- package/dist/index-Dip9RQ_T.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/server-CHT47EXr.js +0 -618
- package/dist/server-CHT47EXr.js.map +0 -1
- package/dist/server.js.map +0 -1
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
import { _ as getProjectInfo, i as shouldNeverStop, l as getIdleProjects, n as getIdleTimeoutMs, p as setProjectStatus, r as loadProjectConfig, x as stopProject, y as loadProjectList } from "./server-D0N_6y4c.js";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { parse } from "smol-toml";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
var DEFAULT_CONFIG_PATHS = [
|
|
6
|
+
"/etc/trafic/config.toml",
|
|
7
|
+
"./config.toml",
|
|
8
|
+
"./trafic.toml"
|
|
9
|
+
];
|
|
10
|
+
var DEFAULT_CONFIG = {
|
|
11
|
+
tld: "",
|
|
12
|
+
port: 9876,
|
|
13
|
+
dbPath: "/var/lib/trafic/db.sqlite",
|
|
14
|
+
projectListPath: "/home/ddev/.ddev/project_list.yaml",
|
|
15
|
+
projectsDir: "/home/ddev/www",
|
|
16
|
+
idleTimeout: "30m",
|
|
17
|
+
idleCheckInterval: "5m",
|
|
18
|
+
auth: {
|
|
19
|
+
defaultPolicy: "basic",
|
|
20
|
+
allowedIps: [],
|
|
21
|
+
tokens: [],
|
|
22
|
+
basicAuth: [],
|
|
23
|
+
rules: []
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Parse a duration string (e.g., "30m", "1h", "2h30m") to milliseconds
|
|
28
|
+
*/
|
|
29
|
+
function parseDuration(duration) {
|
|
30
|
+
const match = duration.match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/);
|
|
31
|
+
if (!match) return 0;
|
|
32
|
+
const hours = parseInt(match[1] ?? "0", 10);
|
|
33
|
+
const minutes = parseInt(match[2] ?? "0", 10);
|
|
34
|
+
const seconds = parseInt(match[3] ?? "0", 10);
|
|
35
|
+
return (hours * 3600 + minutes * 60 + seconds) * 1e3;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Load configuration from TOML file
|
|
39
|
+
*/
|
|
40
|
+
function loadConfig(configPath) {
|
|
41
|
+
let path = configPath;
|
|
42
|
+
if (!path) path = DEFAULT_CONFIG_PATHS.find((p) => existsSync(p));
|
|
43
|
+
if (!path || !existsSync(path)) {
|
|
44
|
+
console.warn("No config file found, using defaults");
|
|
45
|
+
return DEFAULT_CONFIG;
|
|
46
|
+
}
|
|
47
|
+
const parsed = parse(readFileSync(path, "utf-8"));
|
|
48
|
+
const auth = mergeAuthConfig(parsed.auth);
|
|
49
|
+
return {
|
|
50
|
+
tld: parsed.tld ?? DEFAULT_CONFIG.tld,
|
|
51
|
+
port: parsed.port ?? DEFAULT_CONFIG.port,
|
|
52
|
+
dbPath: parsed.db_path ?? DEFAULT_CONFIG.dbPath,
|
|
53
|
+
projectListPath: parsed.project_list_path ?? DEFAULT_CONFIG.projectListPath,
|
|
54
|
+
projectsDir: parsed.projects_dir ?? DEFAULT_CONFIG.projectsDir,
|
|
55
|
+
idleTimeout: parsed.idle_timeout ?? DEFAULT_CONFIG.idleTimeout,
|
|
56
|
+
idleCheckInterval: parsed.idle_check_interval ?? DEFAULT_CONFIG.idleCheckInterval,
|
|
57
|
+
auth
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function mergeAuthConfig(raw) {
|
|
61
|
+
if (!raw) return DEFAULT_CONFIG.auth;
|
|
62
|
+
const rules = raw.rules ?? [];
|
|
63
|
+
return {
|
|
64
|
+
defaultPolicy: raw.default_policy ?? DEFAULT_CONFIG.auth.defaultPolicy,
|
|
65
|
+
allowedIps: raw.allowed_ips ?? DEFAULT_CONFIG.auth.allowedIps,
|
|
66
|
+
tokens: raw.tokens ?? DEFAULT_CONFIG.auth.tokens,
|
|
67
|
+
basicAuth: raw.basic_auth ?? DEFAULT_CONFIG.auth.basicAuth,
|
|
68
|
+
rules: rules.map((r) => ({
|
|
69
|
+
match: r.match,
|
|
70
|
+
policy: r.policy,
|
|
71
|
+
tokens: r.tokens,
|
|
72
|
+
allowedIps: r.allowed_ips
|
|
73
|
+
}))
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Validate configuration
|
|
78
|
+
*/
|
|
79
|
+
function validateConfig(config) {
|
|
80
|
+
const errors = [];
|
|
81
|
+
if (!config.tld) errors.push("tld is required");
|
|
82
|
+
if (config.port < 1 || config.port > 65535) errors.push("port must be between 1 and 65535");
|
|
83
|
+
if (config.auth.defaultPolicy !== "allow" && config.auth.defaultPolicy !== "deny" && config.auth.defaultPolicy !== "basic" && config.auth.defaultPolicy !== "token") errors.push("auth.default_policy must be \"allow\", \"deny\", \"basic\", or \"token\"");
|
|
84
|
+
return errors;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Stop idle projects that haven't been accessed recently
|
|
88
|
+
* Respects per-project idle_timeout settings from .ddev/config.trafic.yaml
|
|
89
|
+
*/
|
|
90
|
+
function stopIdleProjects(config) {
|
|
91
|
+
const globalThresholdMs = parseDuration(config.idleTimeout);
|
|
92
|
+
const projects = loadProjectList(config.projectListPath);
|
|
93
|
+
const idleProjects = getIdleProjects(globalThresholdMs);
|
|
94
|
+
for (const project of idleProjects) {
|
|
95
|
+
const projectDir = projects.get(project.name);
|
|
96
|
+
if (projectDir) {
|
|
97
|
+
const projectConfig = loadProjectConfig(projectDir);
|
|
98
|
+
if (shouldNeverStop(projectConfig)) continue;
|
|
99
|
+
const projectThresholdMs = getIdleTimeoutMs(projectConfig);
|
|
100
|
+
if (projectThresholdMs !== void 0) {
|
|
101
|
+
if (Date.now() - project.lastAccess < projectThresholdMs) continue;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (getProjectInfo(project.name)?.status !== "running") {
|
|
105
|
+
setProjectStatus(project.name, "stopped");
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
console.log(`Stopping idle project: ${project.name}`);
|
|
109
|
+
if (stopProject(project.name)) {
|
|
110
|
+
setProjectStatus(project.name, "stopped");
|
|
111
|
+
console.log(`Stopped: ${project.name}`);
|
|
112
|
+
} else console.error(`Failed to stop: ${project.name}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Start the idle check scheduler
|
|
117
|
+
*/
|
|
118
|
+
function startIdleScheduler(config) {
|
|
119
|
+
const intervalMs = parseDuration(config.idleCheckInterval);
|
|
120
|
+
console.log(`Idle scheduler started: checking every ${config.idleCheckInterval}, timeout ${config.idleTimeout}`);
|
|
121
|
+
stopIdleProjects(config);
|
|
122
|
+
return setInterval(() => {
|
|
123
|
+
stopIdleProjects(config);
|
|
124
|
+
}, intervalMs);
|
|
125
|
+
}
|
|
126
|
+
var stepNumber = 0;
|
|
127
|
+
var dryRun = false;
|
|
128
|
+
/**
|
|
129
|
+
* Set dry-run mode
|
|
130
|
+
*/
|
|
131
|
+
function setDryRun(value) {
|
|
132
|
+
dryRun = value;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Reset step counter
|
|
136
|
+
*/
|
|
137
|
+
function resetSteps() {
|
|
138
|
+
stepNumber = 0;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Log a step
|
|
142
|
+
*/
|
|
143
|
+
function step(message) {
|
|
144
|
+
stepNumber++;
|
|
145
|
+
console.log(`\n\x1b[36m[${stepNumber}]\x1b[0m ${message}`);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Log success
|
|
149
|
+
*/
|
|
150
|
+
function success(message) {
|
|
151
|
+
console.log(` \x1b[32m✓\x1b[0m ${message}`);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Log warning
|
|
155
|
+
*/
|
|
156
|
+
function warn(message) {
|
|
157
|
+
console.log(` \x1b[33m⚠\x1b[0m ${message}`);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Log error
|
|
161
|
+
*/
|
|
162
|
+
function error(message) {
|
|
163
|
+
console.log(` \x1b[31m✗\x1b[0m ${message}`);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Log info
|
|
167
|
+
*/
|
|
168
|
+
function info(message) {
|
|
169
|
+
console.log(` ${message}`);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Execute a command (or show in dry-run mode)
|
|
173
|
+
*/
|
|
174
|
+
function exec(command, options) {
|
|
175
|
+
if (dryRun) {
|
|
176
|
+
if (!options?.silent) console.log(` \x1b[90m$ ${command}\x1b[0m`);
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
return execSync(command, {
|
|
181
|
+
encoding: "utf-8",
|
|
182
|
+
stdio: options?.silent ? "pipe" : "inherit"
|
|
183
|
+
});
|
|
184
|
+
} catch (err) {
|
|
185
|
+
const e = err;
|
|
186
|
+
throw new Error(`Command failed: ${command}\n${e.stderr ?? ""}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Execute a command and return output (silent)
|
|
191
|
+
*/
|
|
192
|
+
function execSilent$1(command) {
|
|
193
|
+
if (dryRun) return "";
|
|
194
|
+
try {
|
|
195
|
+
return execSync(command, {
|
|
196
|
+
encoding: "utf-8",
|
|
197
|
+
stdio: "pipe"
|
|
198
|
+
}).trim();
|
|
199
|
+
} catch {
|
|
200
|
+
return "";
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Check if a command exists
|
|
205
|
+
*/
|
|
206
|
+
function commandExists(command) {
|
|
207
|
+
try {
|
|
208
|
+
execSync(`which ${command}`, { stdio: "pipe" });
|
|
209
|
+
return true;
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Check if running as root
|
|
216
|
+
*/
|
|
217
|
+
function isRoot() {
|
|
218
|
+
return process.getuid?.() === 0;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Create the ddev user with proper permissions
|
|
222
|
+
*/
|
|
223
|
+
function createDdevUser() {
|
|
224
|
+
step("Create ddev user");
|
|
225
|
+
if (execSilent$1("id -u ddev 2>/dev/null")) info("User 'ddev' already exists");
|
|
226
|
+
else {
|
|
227
|
+
exec("useradd -m -s /bin/bash ddev");
|
|
228
|
+
success("Created user 'ddev'");
|
|
229
|
+
}
|
|
230
|
+
exec("usermod -aG docker ddev 2>/dev/null || true", { silent: true });
|
|
231
|
+
exec("mkdir -p /home/ddev/www");
|
|
232
|
+
exec("chown ddev:ddev /home/ddev/www");
|
|
233
|
+
exec("chmod 750 /home/ddev/www");
|
|
234
|
+
success("Created /home/ddev/www");
|
|
235
|
+
exec("mkdir -p /home/ddev/.ssh");
|
|
236
|
+
exec("chmod 700 /home/ddev/.ssh");
|
|
237
|
+
exec("chown ddev:ddev /home/ddev/.ssh");
|
|
238
|
+
success("User ddev configured (no sudo needed with proper DNS)");
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Install Docker using the official script
|
|
242
|
+
*/
|
|
243
|
+
function installDocker() {
|
|
244
|
+
step("Install Docker");
|
|
245
|
+
if (commandExists("docker")) {
|
|
246
|
+
info(`Docker already installed: ${exec("docker --version", { silent: true })?.trim() ?? "unknown version"}`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
info("Downloading and running Docker install script...");
|
|
250
|
+
exec("curl -fsSL https://get.docker.com -o /tmp/get-docker.sh");
|
|
251
|
+
exec("sh /tmp/get-docker.sh", { silent: true });
|
|
252
|
+
exec("rm /tmp/get-docker.sh");
|
|
253
|
+
exec("systemctl enable docker", { silent: true });
|
|
254
|
+
exec("systemctl start docker", { silent: true });
|
|
255
|
+
success("Docker installed and started");
|
|
256
|
+
exec("usermod -aG docker ddev", { silent: true });
|
|
257
|
+
success("Added ddev user to docker group");
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Configure Docker for production use
|
|
261
|
+
*/
|
|
262
|
+
function configureDocker() {
|
|
263
|
+
step("Configure Docker");
|
|
264
|
+
const daemonConfig = {
|
|
265
|
+
"log-driver": "json-file",
|
|
266
|
+
"log-opts": {
|
|
267
|
+
"max-size": "10m",
|
|
268
|
+
"max-file": "3"
|
|
269
|
+
},
|
|
270
|
+
"storage-driver": "overlay2",
|
|
271
|
+
"live-restore": true
|
|
272
|
+
};
|
|
273
|
+
exec("mkdir -p /etc/docker");
|
|
274
|
+
const configPath = "/etc/docker/daemon.json";
|
|
275
|
+
if (existsSync(configPath)) try {
|
|
276
|
+
const merged = {
|
|
277
|
+
...JSON.parse(readFileSync(configPath, "utf-8")),
|
|
278
|
+
...daemonConfig
|
|
279
|
+
};
|
|
280
|
+
writeFileSync(configPath, JSON.stringify(merged, null, 2));
|
|
281
|
+
info("Merged with existing Docker config");
|
|
282
|
+
} catch {
|
|
283
|
+
warn("Could not parse existing Docker config, skipping");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
else writeFileSync(configPath, JSON.stringify(daemonConfig, null, 2));
|
|
287
|
+
exec("systemctl reload docker || systemctl restart docker");
|
|
288
|
+
success("Docker configured with production settings");
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Setup Docker system prune cron job
|
|
292
|
+
*/
|
|
293
|
+
function setupDockerPrune() {
|
|
294
|
+
step("Setup Docker cleanup cron");
|
|
295
|
+
writeFileSync("/etc/cron.d/trafic-docker-prune", `# Trafic: Clean up Docker resources weekly
|
|
296
|
+
0 3 * * 0 root docker system prune -af --volumes 2>&1 | logger -t docker-prune
|
|
297
|
+
`);
|
|
298
|
+
exec("chmod 644 /etc/cron.d/trafic-docker-prune");
|
|
299
|
+
success("Docker prune scheduled weekly at 3am on Sundays");
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Install system dependencies required by Trafic
|
|
303
|
+
*/
|
|
304
|
+
function installSystemDeps() {
|
|
305
|
+
step("Install system dependencies");
|
|
306
|
+
exec("apt-get update -qq", { silent: true });
|
|
307
|
+
exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y jq curl rsync", { silent: true });
|
|
308
|
+
success("System dependencies installed: jq, curl, rsync");
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Install DDEV
|
|
312
|
+
*/
|
|
313
|
+
function installDdev() {
|
|
314
|
+
step("Install DDEV");
|
|
315
|
+
if (commandExists("ddev")) {
|
|
316
|
+
info(`DDEV already installed: ${exec("ddev --version 2>/dev/null | head -1", { silent: true })?.trim() ?? "unknown version"}`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
info("Fetching latest DDEV version...");
|
|
320
|
+
const latestVersion = exec("curl -fsSL https://api.github.com/repos/ddev/ddev/releases/latest | grep '\"tag_name\"' | sed -E 's/.*\"([^\"]+)\".*/\\1/'", { silent: true })?.trim();
|
|
321
|
+
if (!latestVersion) throw new Error("Could not determine latest DDEV version");
|
|
322
|
+
info(`Installing DDEV ${latestVersion}...`);
|
|
323
|
+
const arch = exec("uname -m", { silent: true })?.trim() === "x86_64" ? "amd64" : "arm64";
|
|
324
|
+
const tarball = `ddev_linux-${arch}.${latestVersion}.tar.gz`;
|
|
325
|
+
exec(`curl -fsSL -o /tmp/${tarball} ${`https://github.com/ddev/ddev/releases/download/${latestVersion}/${tarball}`}`, { silent: true });
|
|
326
|
+
exec(`tar -xzf /tmp/${tarball} -C /tmp ddev`, { silent: true });
|
|
327
|
+
exec("mv /tmp/ddev /usr/local/bin/ddev", { silent: true });
|
|
328
|
+
exec("chmod +x /usr/local/bin/ddev", { silent: true });
|
|
329
|
+
exec(`rm -f /tmp/${tarball}`, { silent: true });
|
|
330
|
+
info("Installing mkcert...");
|
|
331
|
+
const mkcertVersion = exec("curl -fsSL https://api.github.com/repos/FiloSottile/mkcert/releases/latest | grep '\"tag_name\"' | sed -E 's/.*\"([^\"]+)\".*/\\1/'", { silent: true })?.trim();
|
|
332
|
+
if (mkcertVersion) {
|
|
333
|
+
exec(`curl -fsSL -o /usr/local/bin/mkcert https://github.com/FiloSottile/mkcert/releases/download/${mkcertVersion}/${`mkcert-${mkcertVersion}-linux-${arch}`}`, { silent: true });
|
|
334
|
+
exec("chmod +x /usr/local/bin/mkcert", { silent: true });
|
|
335
|
+
}
|
|
336
|
+
success(`DDEV ${latestVersion} installed`);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Configure DDEV global settings for production
|
|
340
|
+
*
|
|
341
|
+
* Note on DNS and /etc/hosts:
|
|
342
|
+
* When using a custom TLD (not ddev.site), DDEV may try to edit /etc/hosts
|
|
343
|
+
* if it can't resolve project hostnames via DNS. To avoid this:
|
|
344
|
+
* 1. Configure DNS so *.{tld} resolves to the server's IP (recommended)
|
|
345
|
+
* 2. Or install dnsmasq for local wildcard DNS resolution
|
|
346
|
+
*
|
|
347
|
+
* With proper DNS, DDEV won't need sudo for hostname management.
|
|
348
|
+
*/
|
|
349
|
+
function configureDdev(tld, email) {
|
|
350
|
+
step("Configure DDEV");
|
|
351
|
+
const ddevCmd = (cmd) => `su - ddev -c '${cmd}'`;
|
|
352
|
+
exec(ddevCmd(`ddev config global --project-tld=${tld} --router-http-port=80 --router-https-port=443 --use-letsencrypt=${email ? "true" : "false"} ${email ? `--letsencrypt-email=${email}` : ""} --instrumentation-opt-in=false`), { silent: true });
|
|
353
|
+
success(`DDEV configured with TLD: ${tld}`);
|
|
354
|
+
info(`Ensure DNS is configured: *.${tld} → server IP`);
|
|
355
|
+
if (email) success(`Let's Encrypt enabled with email: ${email}`);
|
|
356
|
+
success("DDEV global settings configured");
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Configure Traefik for forward auth
|
|
360
|
+
*/
|
|
361
|
+
function configureTraefik() {
|
|
362
|
+
step("Configure Traefik for forward auth");
|
|
363
|
+
exec("mkdir -p /home/ddev/.ddev/traefik", { silent: true });
|
|
364
|
+
exec("chown -R ddev:ddev /home/ddev/.ddev", { silent: true });
|
|
365
|
+
writeFileSync("/home/ddev/.ddev/traefik/trafic.yaml", `# Trafic: Forward auth middleware
|
|
366
|
+
http:
|
|
367
|
+
middlewares:
|
|
368
|
+
trafic-auth:
|
|
369
|
+
forwardAuth:
|
|
370
|
+
address: "http://host.docker.internal:9876/__auth__"
|
|
371
|
+
authResponseHeaders:
|
|
372
|
+
- "X-Trafic-Project"
|
|
373
|
+
|
|
374
|
+
trafic-errors:
|
|
375
|
+
errors:
|
|
376
|
+
status:
|
|
377
|
+
- "502"
|
|
378
|
+
- "503"
|
|
379
|
+
service: trafic-service
|
|
380
|
+
query: "/"
|
|
381
|
+
|
|
382
|
+
services:
|
|
383
|
+
trafic-service:
|
|
384
|
+
loadBalancer:
|
|
385
|
+
servers:
|
|
386
|
+
- url: "http://host.docker.internal:9876"
|
|
387
|
+
`);
|
|
388
|
+
exec("chown ddev:ddev /home/ddev/.ddev/traefik/trafic.yaml", { silent: true });
|
|
389
|
+
writeFileSync("/home/ddev/.ddev/traefik/dynamic.yaml", `# Trafic: Default router configuration
|
|
390
|
+
# This file makes all DDEV projects use the Trafic auth middleware
|
|
391
|
+
http:
|
|
392
|
+
routers:
|
|
393
|
+
trafic-catch-all:
|
|
394
|
+
rule: "HostRegexp(\`.+\`)"
|
|
395
|
+
priority: 1
|
|
396
|
+
middlewares:
|
|
397
|
+
- trafic-auth
|
|
398
|
+
- trafic-errors
|
|
399
|
+
service: api@internal
|
|
400
|
+
`);
|
|
401
|
+
exec("chown ddev:ddev /home/ddev/.ddev/traefik/dynamic.yaml", { silent: true });
|
|
402
|
+
success("Traefik configured with Trafic middleware");
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Install Node.js using fnm
|
|
406
|
+
*/
|
|
407
|
+
function installNode() {
|
|
408
|
+
step("Install Node.js");
|
|
409
|
+
if (commandExists("node")) {
|
|
410
|
+
info(`Node.js already installed: ${exec("node --version", { silent: true })?.trim()}`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
exec("curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-dir \"/opt/fnm\" --skip-shell");
|
|
414
|
+
writeFileSync("/etc/profile.d/fnm.sh", `# Trafic: fnm (Node.js version manager)
|
|
415
|
+
export FNM_DIR="/opt/fnm"
|
|
416
|
+
export PATH="/opt/fnm:$PATH"
|
|
417
|
+
eval "$(fnm env)"
|
|
418
|
+
`);
|
|
419
|
+
exec("chmod 644 /etc/profile.d/fnm.sh");
|
|
420
|
+
exec("export FNM_DIR=/opt/fnm && /opt/fnm/fnm install 24");
|
|
421
|
+
exec("export FNM_DIR=/opt/fnm && /opt/fnm/fnm default 24");
|
|
422
|
+
exec("ln -sf /opt/fnm/aliases/default/bin/node /usr/local/bin/node || true");
|
|
423
|
+
exec("ln -sf /opt/fnm/aliases/default/bin/npm /usr/local/bin/npm || true");
|
|
424
|
+
exec("ln -sf /opt/fnm/aliases/default/bin/npx /usr/local/bin/npx || true");
|
|
425
|
+
success("Node.js 24 installed via fnm");
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Install the Trafic agent
|
|
429
|
+
*/
|
|
430
|
+
function installAgent() {
|
|
431
|
+
step("Install Trafic agent");
|
|
432
|
+
exec("npm install -g @studiometa/trafic-agent");
|
|
433
|
+
success("Trafic agent installed");
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Create the agent configuration
|
|
437
|
+
*/
|
|
438
|
+
function createAgentConfig(tld, _email) {
|
|
439
|
+
step("Create agent configuration");
|
|
440
|
+
exec("mkdir -p /etc/trafic");
|
|
441
|
+
exec("mkdir -p /var/lib/trafic");
|
|
442
|
+
exec("chown ddev:ddev /var/lib/trafic");
|
|
443
|
+
const configPath = "/etc/trafic/config.toml";
|
|
444
|
+
if (existsSync(configPath)) {
|
|
445
|
+
info("Configuration already exists at /etc/trafic/config.toml — skipping");
|
|
446
|
+
info("Edit this file to configure authentication");
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
writeFileSync(configPath, `# Trafic agent configuration
|
|
450
|
+
# Generated by trafic setup
|
|
451
|
+
|
|
452
|
+
# Required: TLD for DDEV projects
|
|
453
|
+
tld = "${tld}"
|
|
454
|
+
|
|
455
|
+
# Agent HTTP server port
|
|
456
|
+
port = 9876
|
|
457
|
+
|
|
458
|
+
# Database and paths
|
|
459
|
+
db_path = "/var/lib/trafic/db.sqlite"
|
|
460
|
+
project_list_path = "/home/ddev/.ddev/project_list.yaml"
|
|
461
|
+
projects_dir = "/home/ddev/www"
|
|
462
|
+
|
|
463
|
+
# Scale-to-zero settings
|
|
464
|
+
idle_timeout = "4h"
|
|
465
|
+
idle_check_interval = "5m"
|
|
466
|
+
|
|
467
|
+
# Authentication
|
|
468
|
+
[auth]
|
|
469
|
+
default_policy = "basic"
|
|
470
|
+
allowed_ips = []
|
|
471
|
+
tokens = []
|
|
472
|
+
basic_auth = []
|
|
473
|
+
rules = []
|
|
474
|
+
`);
|
|
475
|
+
exec("chmod 640 /etc/trafic/config.toml");
|
|
476
|
+
exec("chown root:ddev /etc/trafic/config.toml");
|
|
477
|
+
success("Configuration created at /etc/trafic/config.toml");
|
|
478
|
+
info("Edit this file to configure authentication");
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Create systemd service for the agent
|
|
482
|
+
*/
|
|
483
|
+
function createSystemdService() {
|
|
484
|
+
step("Create systemd service");
|
|
485
|
+
writeFileSync("/etc/systemd/system/trafic-agent.service", `[Unit]
|
|
486
|
+
Description=Trafic Agent - DDEV preview environment manager
|
|
487
|
+
After=network.target docker.service
|
|
488
|
+
Requires=docker.service
|
|
489
|
+
|
|
490
|
+
[Service]
|
|
491
|
+
Type=simple
|
|
492
|
+
User=ddev
|
|
493
|
+
Group=ddev
|
|
494
|
+
ExecStart=${exec("which trafic-agent", { silent: true })?.trim() || "/usr/bin/trafic-agent"} start --config /etc/trafic/config.toml
|
|
495
|
+
Restart=always
|
|
496
|
+
RestartSec=5
|
|
497
|
+
StandardOutput=journal
|
|
498
|
+
StandardError=journal
|
|
499
|
+
SyslogIdentifier=trafic-agent
|
|
500
|
+
|
|
501
|
+
# Environment
|
|
502
|
+
Environment=NODE_ENV=production
|
|
503
|
+
# Skip ddev-hostname /etc/hosts management — on a server, hostnames resolve
|
|
504
|
+
# to the public IP (not localhost), which would trigger ddev-hostname on every
|
|
505
|
+
# ddev start. DDEV_NONINTERACTIVE=true skips that entirely.
|
|
506
|
+
Environment=DDEV_NONINTERACTIVE=true
|
|
507
|
+
|
|
508
|
+
# Security hardening
|
|
509
|
+
NoNewPrivileges=true
|
|
510
|
+
ProtectSystem=strict
|
|
511
|
+
ProtectHome=read-only
|
|
512
|
+
ReadWritePaths=/var/lib/trafic /home/ddev/.ddev
|
|
513
|
+
PrivateTmp=true
|
|
514
|
+
|
|
515
|
+
[Install]
|
|
516
|
+
WantedBy=multi-user.target
|
|
517
|
+
`);
|
|
518
|
+
exec("chmod 644 /etc/systemd/system/trafic-agent.service");
|
|
519
|
+
exec("systemctl daemon-reload");
|
|
520
|
+
exec("systemctl enable trafic-agent");
|
|
521
|
+
exec("systemctl start trafic-agent");
|
|
522
|
+
success("Trafic agent service started");
|
|
523
|
+
info("View logs: journalctl -u trafic-agent -f");
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Configure SSH hardening
|
|
527
|
+
*/
|
|
528
|
+
function hardenSsh(allowedUsers = ["ddev"]) {
|
|
529
|
+
step("Harden SSH configuration");
|
|
530
|
+
writeFileSync("/etc/ssh/sshd_config.d/trafic.conf", `# Trafic SSH hardening
|
|
531
|
+
# Generated by trafic setup
|
|
532
|
+
|
|
533
|
+
# Allow root login via SSH key only (no password) — required for server management
|
|
534
|
+
PermitRootLogin prohibit-password
|
|
535
|
+
|
|
536
|
+
# Disable password authentication (keys only)
|
|
537
|
+
PasswordAuthentication no
|
|
538
|
+
PubkeyAuthentication yes
|
|
539
|
+
|
|
540
|
+
# Limit authentication attempts
|
|
541
|
+
MaxAuthTries 3
|
|
542
|
+
|
|
543
|
+
# Idle timeout (5 minutes)
|
|
544
|
+
ClientAliveInterval 300
|
|
545
|
+
ClientAliveCountMax 2
|
|
546
|
+
|
|
547
|
+
# Allowed users (root kept for emergency access)
|
|
548
|
+
AllowUsers root ${allowedUsers.join(" ")}
|
|
549
|
+
|
|
550
|
+
# Disable X11 forwarding
|
|
551
|
+
X11Forwarding no
|
|
552
|
+
|
|
553
|
+
# Disable TCP forwarding
|
|
554
|
+
AllowTcpForwarding no
|
|
555
|
+
|
|
556
|
+
# Use strong algorithms only
|
|
557
|
+
KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256
|
|
558
|
+
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
|
|
559
|
+
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
|
|
560
|
+
`);
|
|
561
|
+
exec("chmod 644 /etc/ssh/sshd_config.d/trafic.conf");
|
|
562
|
+
if (exec("sshd -t 2>&1 || echo 'error'", { silent: true })?.includes("error")) {
|
|
563
|
+
warn("SSH config test failed, reverting...");
|
|
564
|
+
exec("rm /etc/ssh/sshd_config.d/trafic.conf");
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
exec("systemctl reload ssh || systemctl reload sshd");
|
|
568
|
+
success("SSH hardened: password auth disabled, root key-only login retained");
|
|
569
|
+
info(`Allowed users: root, ${allowedUsers.join(", ")}`);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Configure UFW firewall
|
|
573
|
+
*/
|
|
574
|
+
function configureFirewall() {
|
|
575
|
+
step("Configure firewall (UFW)");
|
|
576
|
+
if (!commandExists("ufw")) exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ufw", { silent: true });
|
|
577
|
+
exec("ufw --force reset", { silent: true });
|
|
578
|
+
exec("ufw default deny incoming");
|
|
579
|
+
exec("ufw default allow outgoing");
|
|
580
|
+
exec("ufw allow 22/tcp comment 'SSH'");
|
|
581
|
+
exec("ufw allow 80/tcp comment 'HTTP'");
|
|
582
|
+
exec("ufw allow 443/tcp comment 'HTTPS'");
|
|
583
|
+
exec("ufw --force enable");
|
|
584
|
+
success("Firewall enabled: SSH (22), HTTP (80), HTTPS (443) allowed");
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Configure automatic security updates
|
|
588
|
+
*/
|
|
589
|
+
function configureUnattendedUpgrades() {
|
|
590
|
+
step("Configure automatic security updates");
|
|
591
|
+
exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y unattended-upgrades apt-listchanges", { silent: true });
|
|
592
|
+
writeFileSync("/etc/apt/apt.conf.d/50unattended-upgrades", `// Trafic: Automatic security updates
|
|
593
|
+
Unattended-Upgrade::Allowed-Origins {
|
|
594
|
+
"\${distro_id}:\${distro_codename}";
|
|
595
|
+
"\${distro_id}:\${distro_codename}-security";
|
|
596
|
+
"\${distro_id}ESMApps:\${distro_codename}-apps-security";
|
|
597
|
+
"\${distro_id}ESM:\${distro_codename}-infra-security";
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// Do not automatically reboot
|
|
601
|
+
Unattended-Upgrade::Automatic-Reboot "false";
|
|
602
|
+
|
|
603
|
+
// Remove unused dependencies
|
|
604
|
+
Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
|
605
|
+
|
|
606
|
+
// Email notifications (optional)
|
|
607
|
+
// Unattended-Upgrade::Mail "root";
|
|
608
|
+
`);
|
|
609
|
+
writeFileSync("/etc/apt/apt.conf.d/20auto-upgrades", `APT::Periodic::Update-Package-Lists "1";
|
|
610
|
+
APT::Periodic::Download-Upgradeable-Packages "1";
|
|
611
|
+
APT::Periodic::AutocleanInterval "7";
|
|
612
|
+
APT::Periodic::Unattended-Upgrade "1";
|
|
613
|
+
`);
|
|
614
|
+
exec("systemctl enable unattended-upgrades");
|
|
615
|
+
exec("systemctl start unattended-upgrades");
|
|
616
|
+
success("Automatic security updates enabled");
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Configure Fail2ban
|
|
620
|
+
*/
|
|
621
|
+
function configureFail2ban() {
|
|
622
|
+
step("Configure Fail2ban");
|
|
623
|
+
if (!commandExists("fail2ban-client")) exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y fail2ban", { silent: true });
|
|
624
|
+
writeFileSync("/etc/fail2ban/jail.local", `# Trafic: Fail2ban configuration
|
|
625
|
+
[DEFAULT]
|
|
626
|
+
bantime = 3600
|
|
627
|
+
findtime = 600
|
|
628
|
+
maxretry = 5
|
|
629
|
+
backend = auto
|
|
630
|
+
|
|
631
|
+
[sshd]
|
|
632
|
+
enabled = true
|
|
633
|
+
port = ssh
|
|
634
|
+
filter = sshd
|
|
635
|
+
logpath = /var/log/auth.log
|
|
636
|
+
maxretry = 3
|
|
637
|
+
bantime = 7200
|
|
638
|
+
`);
|
|
639
|
+
exec("chmod 644 /etc/fail2ban/jail.local");
|
|
640
|
+
exec("systemctl enable fail2ban");
|
|
641
|
+
exec("systemctl restart fail2ban");
|
|
642
|
+
success("Fail2ban enabled: SSH brute force protection active");
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Configure system limits
|
|
646
|
+
*/
|
|
647
|
+
function configureSystemLimits() {
|
|
648
|
+
step("Configure system limits");
|
|
649
|
+
writeFileSync("/etc/security/limits.d/trafic.conf", `# Trafic: System limits for ddev user
|
|
650
|
+
ddev soft nofile 65536
|
|
651
|
+
ddev hard nofile 65536
|
|
652
|
+
ddev soft nproc 4096
|
|
653
|
+
ddev hard nproc 4096
|
|
654
|
+
`);
|
|
655
|
+
exec("chmod 644 /etc/security/limits.d/trafic.conf");
|
|
656
|
+
success("System limits configured for ddev user");
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Set proper file permissions
|
|
660
|
+
*/
|
|
661
|
+
function configureFilePermissions() {
|
|
662
|
+
step("Configure file permissions");
|
|
663
|
+
exec("mkdir -p /etc/trafic");
|
|
664
|
+
exec("mkdir -p /var/lib/trafic");
|
|
665
|
+
exec("mkdir -p /var/backups/trafic");
|
|
666
|
+
exec("chmod 755 /etc/trafic");
|
|
667
|
+
exec("chmod 700 /var/lib/trafic");
|
|
668
|
+
exec("chmod 700 /var/backups/trafic");
|
|
669
|
+
exec("chown root:ddev /etc/trafic");
|
|
670
|
+
exec("chown ddev:ddev /var/lib/trafic");
|
|
671
|
+
exec("chown ddev:ddev /var/backups/trafic");
|
|
672
|
+
if (existsSync("/etc/trafic/config.toml")) {
|
|
673
|
+
exec("chmod 640 /etc/trafic/config.toml");
|
|
674
|
+
exec("chown root:ddev /etc/trafic/config.toml");
|
|
675
|
+
}
|
|
676
|
+
success("File permissions configured");
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Run all hardening steps
|
|
680
|
+
*/
|
|
681
|
+
function hardenServer(sshUsers = ["ddev"]) {
|
|
682
|
+
hardenSsh(sshUsers);
|
|
683
|
+
configureFirewall();
|
|
684
|
+
configureUnattendedUpgrades();
|
|
685
|
+
configureFail2ban();
|
|
686
|
+
configureSystemLimits();
|
|
687
|
+
configureFilePermissions();
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Run a command silently and return output
|
|
691
|
+
*/
|
|
692
|
+
function execSilent(command) {
|
|
693
|
+
try {
|
|
694
|
+
return execSync(command, {
|
|
695
|
+
encoding: "utf-8",
|
|
696
|
+
stdio: "pipe"
|
|
697
|
+
}).trim();
|
|
698
|
+
} catch {
|
|
699
|
+
return "";
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Check if a service is active
|
|
704
|
+
*/
|
|
705
|
+
function isServiceActive(service) {
|
|
706
|
+
return execSilent(`systemctl is-active ${service}`) === "active";
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Audit SSH configuration
|
|
710
|
+
*/
|
|
711
|
+
function auditSsh() {
|
|
712
|
+
const checks = [];
|
|
713
|
+
const sshdConfig = existsSync("/etc/ssh/sshd_config") ? readFileSync("/etc/ssh/sshd_config", "utf-8") : "";
|
|
714
|
+
const traficConfig = existsSync("/etc/ssh/sshd_config.d/trafic.conf") ? readFileSync("/etc/ssh/sshd_config.d/trafic.conf", "utf-8") : "";
|
|
715
|
+
const config = sshdConfig + "\n" + traficConfig;
|
|
716
|
+
const rootLoginDisabled = config.includes("PermitRootLogin no") || config.includes("PermitRootLogin prohibit-password");
|
|
717
|
+
checks.push({
|
|
718
|
+
name: "SSH root login",
|
|
719
|
+
status: rootLoginDisabled ? "pass" : "fail",
|
|
720
|
+
message: rootLoginDisabled ? "Root login disabled" : "Root login may be enabled",
|
|
721
|
+
fix: "Add 'PermitRootLogin no' to /etc/ssh/sshd_config.d/trafic.conf"
|
|
722
|
+
});
|
|
723
|
+
const passwordAuthDisabled = config.includes("PasswordAuthentication no");
|
|
724
|
+
checks.push({
|
|
725
|
+
name: "SSH password auth",
|
|
726
|
+
status: passwordAuthDisabled ? "pass" : "warn",
|
|
727
|
+
message: passwordAuthDisabled ? "Password authentication disabled" : "Password authentication may be enabled",
|
|
728
|
+
fix: "Add 'PasswordAuthentication no' to /etc/ssh/sshd_config.d/trafic.conf"
|
|
729
|
+
});
|
|
730
|
+
return checks;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Audit firewall configuration
|
|
734
|
+
*/
|
|
735
|
+
function auditFirewall() {
|
|
736
|
+
const checks = [];
|
|
737
|
+
const ufwStatus = execSilent("ufw status");
|
|
738
|
+
const isActive = ufwStatus.includes("Status: active");
|
|
739
|
+
checks.push({
|
|
740
|
+
name: "Firewall (UFW)",
|
|
741
|
+
status: isActive ? "pass" : "fail",
|
|
742
|
+
message: isActive ? "UFW is active" : "UFW is not active",
|
|
743
|
+
fix: "Run: ufw enable"
|
|
744
|
+
});
|
|
745
|
+
if (isActive) {
|
|
746
|
+
const allows22 = ufwStatus.includes("22/tcp");
|
|
747
|
+
const allows80 = ufwStatus.includes("80/tcp");
|
|
748
|
+
const allows443 = ufwStatus.includes("443/tcp");
|
|
749
|
+
if (allows22 && allows80 && allows443) checks.push({
|
|
750
|
+
name: "Firewall ports",
|
|
751
|
+
status: "pass",
|
|
752
|
+
message: "Required ports (22, 80, 443) are open"
|
|
753
|
+
});
|
|
754
|
+
else checks.push({
|
|
755
|
+
name: "Firewall ports",
|
|
756
|
+
status: "warn",
|
|
757
|
+
message: "Some required ports may be blocked",
|
|
758
|
+
fix: "Run: ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp"
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
return checks;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Audit security services
|
|
765
|
+
*/
|
|
766
|
+
function auditServices() {
|
|
767
|
+
const checks = [];
|
|
768
|
+
const fail2banActive = isServiceActive("fail2ban");
|
|
769
|
+
checks.push({
|
|
770
|
+
name: "Fail2ban",
|
|
771
|
+
status: fail2banActive ? "pass" : "warn",
|
|
772
|
+
message: fail2banActive ? "Fail2ban is active" : "Fail2ban is not running",
|
|
773
|
+
fix: "Run: apt install fail2ban && systemctl enable fail2ban --now"
|
|
774
|
+
});
|
|
775
|
+
const unattendedActive = isServiceActive("unattended-upgrades");
|
|
776
|
+
checks.push({
|
|
777
|
+
name: "Automatic updates",
|
|
778
|
+
status: unattendedActive ? "pass" : "warn",
|
|
779
|
+
message: unattendedActive ? "Unattended upgrades enabled" : "Automatic updates not configured",
|
|
780
|
+
fix: "Run: apt install unattended-upgrades && dpkg-reconfigure unattended-upgrades"
|
|
781
|
+
});
|
|
782
|
+
const agentActive = isServiceActive("trafic-agent");
|
|
783
|
+
checks.push({
|
|
784
|
+
name: "Trafic agent",
|
|
785
|
+
status: agentActive ? "pass" : "fail",
|
|
786
|
+
message: agentActive ? "Trafic agent is running" : "Trafic agent is not running",
|
|
787
|
+
fix: "Run: systemctl start trafic-agent"
|
|
788
|
+
});
|
|
789
|
+
return checks;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Audit Docker
|
|
793
|
+
*/
|
|
794
|
+
function auditDocker() {
|
|
795
|
+
const checks = [];
|
|
796
|
+
const dockerActive = isServiceActive("docker");
|
|
797
|
+
checks.push({
|
|
798
|
+
name: "Docker",
|
|
799
|
+
status: dockerActive ? "pass" : "fail",
|
|
800
|
+
message: dockerActive ? "Docker is running" : "Docker is not running",
|
|
801
|
+
fix: "Run: systemctl start docker"
|
|
802
|
+
});
|
|
803
|
+
const danglingImages = execSilent("docker images -f dangling=true -q | wc -l");
|
|
804
|
+
const count = parseInt(danglingImages, 10) || 0;
|
|
805
|
+
if (count > 0) checks.push({
|
|
806
|
+
name: "Docker cleanup",
|
|
807
|
+
status: "warn",
|
|
808
|
+
message: `${count} dangling Docker images`,
|
|
809
|
+
fix: "Run: docker system prune -f"
|
|
810
|
+
});
|
|
811
|
+
const diskUsage = execSilent("df -h / | awk 'NR==2{print $5}'");
|
|
812
|
+
const usagePercent = parseInt(diskUsage, 10) || 0;
|
|
813
|
+
checks.push({
|
|
814
|
+
name: "Disk usage",
|
|
815
|
+
status: usagePercent > 90 ? "fail" : usagePercent > 80 ? "warn" : "pass",
|
|
816
|
+
message: `Disk usage: ${diskUsage}`,
|
|
817
|
+
fix: usagePercent > 80 ? "Consider cleaning up old projects or Docker resources" : void 0
|
|
818
|
+
});
|
|
819
|
+
return checks;
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Audit file permissions
|
|
823
|
+
*/
|
|
824
|
+
function auditPermissions() {
|
|
825
|
+
const checks = [];
|
|
826
|
+
if (existsSync("/etc/trafic/config.toml")) {
|
|
827
|
+
const stats = execSilent("stat -c '%a' /etc/trafic/config.toml");
|
|
828
|
+
const isSecure = stats === "640" || stats === "600";
|
|
829
|
+
checks.push({
|
|
830
|
+
name: "Config permissions",
|
|
831
|
+
status: isSecure ? "pass" : "warn",
|
|
832
|
+
message: isSecure ? "Config file has secure permissions" : `Config file permissions: ${stats} (should be 640)`,
|
|
833
|
+
fix: "Run: chmod 640 /etc/trafic/config.toml"
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
if (existsSync("/home/ddev/www")) {
|
|
837
|
+
const owner = execSilent("stat -c '%U:%G' /home/ddev/www");
|
|
838
|
+
const isCorrect = owner === "ddev:ddev";
|
|
839
|
+
checks.push({
|
|
840
|
+
name: "Projects directory",
|
|
841
|
+
status: isCorrect ? "pass" : "warn",
|
|
842
|
+
message: isCorrect ? "Projects directory owned by ddev" : `Projects directory owner: ${owner}`,
|
|
843
|
+
fix: "Run: chown ddev:ddev /home/ddev/www"
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
return checks;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Run all audit checks
|
|
850
|
+
*/
|
|
851
|
+
function runAudit() {
|
|
852
|
+
return [
|
|
853
|
+
...auditSsh(),
|
|
854
|
+
...auditFirewall(),
|
|
855
|
+
...auditServices(),
|
|
856
|
+
...auditDocker(),
|
|
857
|
+
...auditPermissions()
|
|
858
|
+
];
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Print audit results
|
|
862
|
+
*/
|
|
863
|
+
function printAuditResults(checks) {
|
|
864
|
+
console.log("\n\x1B[1mSecurity Audit Results\x1B[0m\n");
|
|
865
|
+
for (const check of checks) {
|
|
866
|
+
const icon = check.status === "pass" ? "\x1B[32m✓\x1B[0m" : check.status === "warn" ? "\x1B[33m⚠\x1B[0m" : "\x1B[31m✗\x1B[0m";
|
|
867
|
+
console.log(`${icon} \x1b[1m${check.name}\x1b[0m: ${check.message}`);
|
|
868
|
+
if (check.fix && check.status !== "pass") console.log(` \x1b[90m${check.fix}\x1b[0m`);
|
|
869
|
+
}
|
|
870
|
+
const passed = checks.filter((c) => c.status === "pass").length;
|
|
871
|
+
const warnings = checks.filter((c) => c.status === "warn").length;
|
|
872
|
+
const failed = checks.filter((c) => c.status === "fail").length;
|
|
873
|
+
console.log(`\n\x1b[1mSummary:\x1b[0m ${passed} passed, ${warnings} warnings, ${failed} failed`);
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Run the full server setup
|
|
877
|
+
*/
|
|
878
|
+
async function setup(options) {
|
|
879
|
+
resetSteps();
|
|
880
|
+
setDryRun(options.dryRun ?? false);
|
|
881
|
+
console.log("\n\x1B[1m🚦 Trafic Server Setup\x1B[0m\n");
|
|
882
|
+
if (options.dryRun) console.log("\x1B[33m⚠ Dry-run mode: no changes will be made\x1B[0m\n");
|
|
883
|
+
if (!isRoot() && !options.dryRun) {
|
|
884
|
+
error("This command must be run as root");
|
|
885
|
+
console.log(" Run: sudo trafic-agent setup ...");
|
|
886
|
+
process.exit(1);
|
|
887
|
+
}
|
|
888
|
+
info(`TLD: ${options.tld}`);
|
|
889
|
+
if (options.email) info(`Let's Encrypt email: ${options.email}`);
|
|
890
|
+
if (options.noHardening) info("Hardening: disabled");
|
|
891
|
+
try {
|
|
892
|
+
installSystemDeps();
|
|
893
|
+
createDdevUser();
|
|
894
|
+
if (!options.noDocker) {
|
|
895
|
+
installDocker();
|
|
896
|
+
configureDocker();
|
|
897
|
+
setupDockerPrune();
|
|
898
|
+
} else {
|
|
899
|
+
step("Install Docker");
|
|
900
|
+
info("Skipped (--no-docker)");
|
|
901
|
+
}
|
|
902
|
+
if (!options.noDdev) {
|
|
903
|
+
installDdev();
|
|
904
|
+
configureDdev(options.tld, options.email);
|
|
905
|
+
configureTraefik();
|
|
906
|
+
} else {
|
|
907
|
+
step("Install DDEV");
|
|
908
|
+
info("Skipped (--no-ddev)");
|
|
909
|
+
}
|
|
910
|
+
installNode();
|
|
911
|
+
installAgent();
|
|
912
|
+
createAgentConfig(options.tld, options.email);
|
|
913
|
+
createSystemdService();
|
|
914
|
+
if (!options.noHardening) hardenServer(options.sshUsers ?? ["ddev"]);
|
|
915
|
+
else {
|
|
916
|
+
step("Server hardening");
|
|
917
|
+
info("Skipped (--no-hardening)");
|
|
918
|
+
}
|
|
919
|
+
console.log("\n\x1B[32m✓ Setup complete!\x1B[0m\n");
|
|
920
|
+
console.log("Next steps:");
|
|
921
|
+
console.log(` 1. Edit /etc/trafic/config.toml to configure authentication`);
|
|
922
|
+
console.log(` 2. Add SSH public keys to /home/ddev/.ssh/authorized_keys`);
|
|
923
|
+
console.log(` 3. Deploy your first project:`);
|
|
924
|
+
console.log(` npx @studiometa/trafic-cli deploy --host=<this-server> --name=my-app`);
|
|
925
|
+
console.log(`\nYour projects will be available at https://<name>.${options.tld}`);
|
|
926
|
+
} catch (err) {
|
|
927
|
+
error(`Setup failed: ${err instanceof Error ? err.message : err}`);
|
|
928
|
+
process.exit(1);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Run security audit
|
|
933
|
+
*/
|
|
934
|
+
async function audit() {
|
|
935
|
+
const checks = runAudit();
|
|
936
|
+
printAuditResults(checks);
|
|
937
|
+
if (checks.filter((c) => c.status === "fail").length > 0) process.exit(1);
|
|
938
|
+
}
|
|
939
|
+
export { startIdleScheduler as a, parseDuration as c, runAudit as i, validateConfig as l, setup as n, stopIdleProjects as o, printAuditResults as r, loadConfig as s, audit as t };
|
|
940
|
+
|
|
941
|
+
//# sourceMappingURL=setup-CVf8YfPW.js.map
|