@studiometa/trafic-agent 0.1.0 → 0.1.2
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/README.md +131 -0
- package/dist/cli.js +2 -2
- package/dist/{index-C7OaqqbZ.js → index-CO8UzDfr.js} +11 -13
- package/dist/index-CO8UzDfr.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/setup/ddev.d.ts.map +1 -1
- package/dist/setup/docker.d.ts.map +1 -1
- package/dist/setup/steps.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/index-C7OaqqbZ.js.map +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# @studiometa/trafic-agent
|
|
2
|
+
|
|
3
|
+
Server agent for DDEV preview environments — auth, scale-to-zero, and auto-start.
|
|
4
|
+
|
|
5
|
+
Part of [Trafic](https://github.com/studiometa/trafic), a tool for managing DDEV preview environments on Linux servers.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Forward auth** — Traefik middleware for IP whitelist, basic auth, and token auth
|
|
10
|
+
- **Scale-to-zero** — Automatically stop idle DDEV projects to save RAM
|
|
11
|
+
- **Auto-start** — Show a waiting page and restart stopped projects on request
|
|
12
|
+
- **Per-project config** — Override auth and idle timeout per project
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g @studiometa/trafic-agent
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
### `trafic-agent serve`
|
|
23
|
+
|
|
24
|
+
Start the agent server.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
trafic-agent serve
|
|
28
|
+
# or with custom config
|
|
29
|
+
trafic-agent serve --config /etc/trafic/config.toml
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### `trafic-agent setup`
|
|
33
|
+
|
|
34
|
+
Interactive server setup — installs Docker, DDEV, configures DNS, and sets up the agent as a systemd service.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
trafic-agent setup --tld previews.example.com
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Requirements:**
|
|
41
|
+
- Ubuntu 24.04 LTS
|
|
42
|
+
- Root access (for initial setup)
|
|
43
|
+
- Wildcard DNS pointing to the server
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
|
|
47
|
+
Create `/etc/trafic/config.toml`:
|
|
48
|
+
|
|
49
|
+
```toml
|
|
50
|
+
# Required: TLD for DDEV projects
|
|
51
|
+
tld = "previews.example.com"
|
|
52
|
+
|
|
53
|
+
# Agent HTTP server port (default: 9876)
|
|
54
|
+
port = 9876
|
|
55
|
+
|
|
56
|
+
# Scale-to-zero: stop idle projects after this duration
|
|
57
|
+
idle_timeout = "4h"
|
|
58
|
+
|
|
59
|
+
# Authentication
|
|
60
|
+
[auth]
|
|
61
|
+
default_policy = "basic" # allow, deny, basic, or token
|
|
62
|
+
|
|
63
|
+
# IP whitelist (bypasses auth)
|
|
64
|
+
allowed_ips = ["192.168.1.0/24", "10.0.0.0/8"]
|
|
65
|
+
|
|
66
|
+
# Bearer tokens for CI/API access
|
|
67
|
+
tokens = ["your-ci-token"]
|
|
68
|
+
|
|
69
|
+
# Basic auth credentials
|
|
70
|
+
basic_auth = ["user:password"]
|
|
71
|
+
|
|
72
|
+
# Per-hostname rules
|
|
73
|
+
[[auth.rules]]
|
|
74
|
+
match = "*.public.*"
|
|
75
|
+
policy = "allow"
|
|
76
|
+
|
|
77
|
+
[[auth.rules]]
|
|
78
|
+
match = "admin.*"
|
|
79
|
+
policy = "basic"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Per-project configuration
|
|
83
|
+
|
|
84
|
+
Create `.ddev/config.trafic.yaml` in your project:
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
# Override auth policy for this project
|
|
88
|
+
auth_policy: allow # allow, deny, basic, or token
|
|
89
|
+
|
|
90
|
+
# Override idle timeout (or disable with "never")
|
|
91
|
+
idle_timeout: never
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## How it works
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
HTTPS request
|
|
98
|
+
│
|
|
99
|
+
▼
|
|
100
|
+
Traefik (DDEV router)
|
|
101
|
+
│
|
|
102
|
+
├─► forwardAuth → trafic-agent
|
|
103
|
+
│ │
|
|
104
|
+
│ ├─► 200 OK → DDEV project
|
|
105
|
+
│ └─► 401 → Basic auth prompt
|
|
106
|
+
│
|
|
107
|
+
└─► 502 error → errors middleware → trafic-agent
|
|
108
|
+
│
|
|
109
|
+
├─► known project → Waiting page + auto-start
|
|
110
|
+
└─► unknown → Error page
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The agent:
|
|
114
|
+
1. Handles forward auth requests from Traefik
|
|
115
|
+
2. Checks IP whitelist, tokens, or basic auth
|
|
116
|
+
3. Tracks project activity for scale-to-zero
|
|
117
|
+
4. Starts stopped projects when requested
|
|
118
|
+
5. Shows a waiting page while projects start
|
|
119
|
+
|
|
120
|
+
## Endpoints
|
|
121
|
+
|
|
122
|
+
| Endpoint | Description |
|
|
123
|
+
|----------|-------------|
|
|
124
|
+
| `GET /auth` | Forward auth for Traefik |
|
|
125
|
+
| `GET /errors` | Error handler for stopped projects |
|
|
126
|
+
| `GET /status` | Health check |
|
|
127
|
+
| `GET /projects` | List all projects (JSON) |
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT — see [LICENSE](https://github.com/studiometa/trafic/blob/main/LICENSE)
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
|
-
import { a as audit, s as setup, l as loadConfig, v as validateConfig, c as startIdleScheduler } from "./index-
|
|
3
|
+
import { a as audit, s as setup, l as loadConfig, v as validateConfig, c as startIdleScheduler } from "./index-CO8UzDfr.js";
|
|
4
4
|
import { x as startServer, d as closeDb } from "./server-CHT47EXr.js";
|
|
5
5
|
const HELP = `
|
|
6
6
|
trafic-agent — Server agent for DDEV preview environments
|
|
@@ -45,7 +45,7 @@ function printHelp() {
|
|
|
45
45
|
console.log(HELP.trim());
|
|
46
46
|
}
|
|
47
47
|
function printVersion() {
|
|
48
|
-
console.log("0.1.
|
|
48
|
+
console.log("0.1.2");
|
|
49
49
|
}
|
|
50
50
|
async function runStart(values) {
|
|
51
51
|
const config = loadConfig(values.config);
|
|
@@ -246,19 +246,18 @@ function configureDocker() {
|
|
|
246
246
|
};
|
|
247
247
|
exec("mkdir -p /etc/docker");
|
|
248
248
|
const configPath = "/etc/docker/daemon.json";
|
|
249
|
-
|
|
250
|
-
if (existsSync2(configPath)) {
|
|
249
|
+
if (existsSync(configPath)) {
|
|
251
250
|
try {
|
|
252
|
-
const existing = JSON.parse(
|
|
251
|
+
const existing = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
253
252
|
const merged = { ...existing, ...daemonConfig };
|
|
254
|
-
|
|
253
|
+
writeFileSync(configPath, JSON.stringify(merged, null, 2));
|
|
255
254
|
info("Merged with existing Docker config");
|
|
256
255
|
} catch {
|
|
257
256
|
warn("Could not parse existing Docker config, skipping");
|
|
258
257
|
return;
|
|
259
258
|
}
|
|
260
259
|
} else {
|
|
261
|
-
|
|
260
|
+
writeFileSync(configPath, JSON.stringify(daemonConfig, null, 2));
|
|
262
261
|
}
|
|
263
262
|
exec("systemctl reload docker || systemctl restart docker");
|
|
264
263
|
success("Docker configured with production settings");
|
|
@@ -268,8 +267,7 @@ function setupDockerPrune() {
|
|
|
268
267
|
const cronContent = `# Trafic: Clean up Docker resources weekly
|
|
269
268
|
0 3 * * 0 root docker system prune -af --volumes 2>&1 | logger -t docker-prune
|
|
270
269
|
`;
|
|
271
|
-
|
|
272
|
-
writeFileSync2("/etc/cron.d/trafic-docker-prune", cronContent);
|
|
270
|
+
writeFileSync("/etc/cron.d/trafic-docker-prune", cronContent);
|
|
273
271
|
exec("chmod 644 /etc/cron.d/trafic-docker-prune");
|
|
274
272
|
success("Docker prune scheduled weekly at 3am on Sundays");
|
|
275
273
|
}
|
|
@@ -280,10 +278,11 @@ function installDdev() {
|
|
|
280
278
|
info(`DDEV already installed: ${version?.trim() ?? "unknown version"}`);
|
|
281
279
|
return;
|
|
282
280
|
}
|
|
283
|
-
info("Installing DDEV...");
|
|
281
|
+
info("Installing DDEV as user 'ddev'...");
|
|
284
282
|
exec(
|
|
285
|
-
"curl -fsSL https://ddev.com/install.sh | bash -s -- --version latest"
|
|
283
|
+
"su - ddev -c 'curl -fsSL https://ddev.com/install.sh | bash -s -- --version latest'"
|
|
286
284
|
);
|
|
285
|
+
exec("ln -sf /home/ddev/.ddev/bin/ddev /usr/local/bin/ddev");
|
|
287
286
|
success("DDEV installed");
|
|
288
287
|
}
|
|
289
288
|
function configureDdev(tld, email) {
|
|
@@ -331,8 +330,7 @@ http:
|
|
|
331
330
|
servers:
|
|
332
331
|
- url: "http://host.docker.internal:9876"
|
|
333
332
|
`;
|
|
334
|
-
|
|
335
|
-
writeFileSync2("/home/ddev/.ddev/traefik/trafic.yaml", staticConfig);
|
|
333
|
+
writeFileSync("/home/ddev/.ddev/traefik/trafic.yaml", staticConfig);
|
|
336
334
|
exec("chown ddev:ddev /home/ddev/.ddev/traefik/trafic.yaml");
|
|
337
335
|
const routerConfig = `# Trafic: Default router configuration
|
|
338
336
|
# This file makes all DDEV projects use the Trafic auth middleware
|
|
@@ -346,7 +344,7 @@ http:
|
|
|
346
344
|
- trafic-errors
|
|
347
345
|
service: api@internal
|
|
348
346
|
`;
|
|
349
|
-
|
|
347
|
+
writeFileSync("/home/ddev/.ddev/traefik/dynamic.yaml", routerConfig);
|
|
350
348
|
exec("chown ddev:ddev /home/ddev/.ddev/traefik/dynamic.yaml");
|
|
351
349
|
success("Traefik configured with Trafic middleware");
|
|
352
350
|
}
|
|
@@ -862,4 +860,4 @@ export {
|
|
|
862
860
|
setup as s,
|
|
863
861
|
validateConfig as v
|
|
864
862
|
};
|
|
865
|
-
//# sourceMappingURL=index-
|
|
863
|
+
//# sourceMappingURL=index-CO8UzDfr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-CO8UzDfr.js","sources":["../src/utils/config.ts","../src/tasks/stop-idle.ts","../src/setup/steps.ts","../src/setup/user.ts","../src/setup/docker.ts","../src/setup/ddev.ts","../src/setup/agent.ts","../src/setup/hardening.ts","../src/setup/audit.ts","../src/setup/index.ts"],"sourcesContent":["import { readFileSync, existsSync } from \"node:fs\";\nimport { parse } from \"smol-toml\";\nimport type { AgentConfig, AuthConfig, AuthRule } from \"../types.js\";\n\nconst DEFAULT_CONFIG_PATHS = [\n \"/etc/trafic/config.toml\",\n \"./config.toml\",\n \"./trafic.toml\",\n];\n\nconst DEFAULT_CONFIG: AgentConfig = {\n tld: \"\",\n port: 9876,\n dbPath: \"/var/lib/trafic/db.sqlite\",\n projectListPath: \"/home/ddev/.ddev/project_list.yaml\",\n projectsDir: \"/home/ddev/www\",\n idleTimeout: \"30m\",\n idleCheckInterval: \"5m\",\n auth: {\n defaultPolicy: \"basic\",\n allowedIps: [],\n tokens: [],\n basicAuth: [],\n rules: [],\n },\n};\n\n/**\n * Parse a duration string (e.g., \"30m\", \"1h\", \"2h30m\") to milliseconds\n */\nexport function parseDuration(duration: string): number {\n const regex = /(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?/;\n const match = duration.match(regex);\n if (!match) return 0;\n\n const hours = parseInt(match[1] ?? \"0\", 10);\n const minutes = parseInt(match[2] ?? \"0\", 10);\n const seconds = parseInt(match[3] ?? \"0\", 10);\n\n return (hours * 3600 + minutes * 60 + seconds) * 1000;\n}\n\n/**\n * Load configuration from TOML file\n */\nexport function loadConfig(configPath?: string): AgentConfig {\n // Find config file\n let path = configPath;\n if (!path) {\n path = DEFAULT_CONFIG_PATHS.find((p) => existsSync(p));\n }\n\n if (!path || !existsSync(path)) {\n console.warn(\"No config file found, using defaults\");\n return DEFAULT_CONFIG;\n }\n\n const content = readFileSync(path, \"utf-8\");\n const parsed = parse(content) as Record<string, unknown>;\n\n // Merge with defaults\n const auth = mergeAuthConfig(parsed.auth as Record<string, unknown>);\n\n return {\n tld: (parsed.tld as string) ?? DEFAULT_CONFIG.tld,\n port: (parsed.port as number) ?? DEFAULT_CONFIG.port,\n dbPath: (parsed.db_path as string) ?? DEFAULT_CONFIG.dbPath,\n projectListPath:\n (parsed.project_list_path as string) ?? DEFAULT_CONFIG.projectListPath,\n projectsDir: (parsed.projects_dir as string) ?? DEFAULT_CONFIG.projectsDir,\n idleTimeout:\n (parsed.idle_timeout as string) ?? DEFAULT_CONFIG.idleTimeout,\n idleCheckInterval:\n (parsed.idle_check_interval as string) ??\n DEFAULT_CONFIG.idleCheckInterval,\n auth,\n };\n}\n\nfunction mergeAuthConfig(raw?: Record<string, unknown>): AuthConfig {\n if (!raw) return DEFAULT_CONFIG.auth;\n\n const rules = (raw.rules as Array<Record<string, unknown>>) ?? [];\n\n return {\n defaultPolicy:\n (raw.default_policy as AuthConfig[\"defaultPolicy\"]) ??\n DEFAULT_CONFIG.auth.defaultPolicy,\n allowedIps:\n (raw.allowed_ips as string[]) ?? DEFAULT_CONFIG.auth.allowedIps,\n tokens: (raw.tokens as string[]) ?? DEFAULT_CONFIG.auth.tokens,\n basicAuth: (raw.basic_auth as string[]) ?? DEFAULT_CONFIG.auth.basicAuth,\n rules: rules.map(\n (r): AuthRule => ({\n match: r.match as string,\n policy: r.policy as AuthRule[\"policy\"],\n tokens: r.tokens as string[] | undefined,\n allowedIps: r.allowed_ips as string[] | undefined,\n }),\n ),\n };\n}\n\n/**\n * Validate configuration\n */\nexport function validateConfig(config: AgentConfig): string[] {\n const errors: string[] = [];\n\n if (!config.tld) {\n errors.push(\"tld is required\");\n }\n\n if (config.port < 1 || config.port > 65535) {\n errors.push(\"port must be between 1 and 65535\");\n }\n\n if (\n config.auth.defaultPolicy !== \"allow\" &&\n config.auth.defaultPolicy !== \"deny\" &&\n config.auth.defaultPolicy !== \"basic\" &&\n config.auth.defaultPolicy !== \"token\"\n ) {\n errors.push('auth.default_policy must be \"allow\", \"deny\", \"basic\", or \"token\"');\n }\n\n return errors;\n}\n","import { getIdleProjects, setProjectStatus } from \"../utils/db.js\";\nimport { stopProject, getProjectInfo, loadProjectList } from \"../utils/ddev.js\";\nimport { parseDuration } from \"../utils/config.js\";\nimport { loadProjectConfig, shouldNeverStop, getIdleTimeoutMs } from \"../utils/project-config.js\";\nimport type { AgentConfig } from \"../types.js\";\n\n/**\n * Stop idle projects that haven't been accessed recently\n * Respects per-project idle_timeout settings from .ddev/config.trafic.yaml\n */\nexport function stopIdleProjects(config: AgentConfig): void {\n const globalThresholdMs = parseDuration(config.idleTimeout);\n\n // Get all projects to check their individual configs\n const projects = loadProjectList(config.projectListPath);\n\n // Get projects idle according to global threshold\n const idleProjects = getIdleProjects(globalThresholdMs);\n\n for (const project of idleProjects) {\n const projectDir = projects.get(project.name);\n\n // Load per-project config if available\n if (projectDir) {\n const projectConfig = loadProjectConfig(projectDir);\n\n // Skip if project is configured to never stop\n if (shouldNeverStop(projectConfig)) {\n continue;\n }\n\n // Check project-specific idle timeout\n const projectThresholdMs = getIdleTimeoutMs(projectConfig);\n if (projectThresholdMs !== undefined) {\n const idleDuration = Date.now() - project.lastAccess;\n if (idleDuration < projectThresholdMs) {\n // Not idle according to project-specific timeout\n continue;\n }\n }\n }\n\n // Double-check project is actually running via DDEV\n const info = getProjectInfo(project.name);\n if (info?.status !== \"running\") {\n // Already stopped, just update our record\n setProjectStatus(project.name, \"stopped\");\n continue;\n }\n\n console.log(`Stopping idle project: ${project.name}`);\n const success = stopProject(project.name);\n\n if (success) {\n setProjectStatus(project.name, \"stopped\");\n console.log(`Stopped: ${project.name}`);\n } else {\n console.error(`Failed to stop: ${project.name}`);\n }\n }\n}\n\n/**\n * Start the idle check scheduler\n */\nexport function startIdleScheduler(config: AgentConfig): NodeJS.Timeout {\n const intervalMs = parseDuration(config.idleCheckInterval);\n\n console.log(\n `Idle scheduler started: checking every ${config.idleCheckInterval}, timeout ${config.idleTimeout}`,\n );\n\n // Run immediately on start\n stopIdleProjects(config);\n\n // Then run on interval\n return setInterval(() => {\n stopIdleProjects(config);\n }, intervalMs);\n}\n","import { execSync } from \"node:child_process\";\nimport { writeFileSync as fsWriteFile } from \"node:fs\";\n\nlet stepNumber = 0;\nlet dryRun = false;\n\n/**\n * Set dry-run mode\n */\nexport function setDryRun(value: boolean): void {\n dryRun = value;\n}\n\n/**\n * Reset step counter\n */\nexport function resetSteps(): void {\n stepNumber = 0;\n}\n\n/**\n * Log a step\n */\nexport function step(message: string): void {\n stepNumber++;\n console.log(`\\n\\x1b[36m[${stepNumber}]\\x1b[0m ${message}`);\n}\n\n/**\n * Log success\n */\nexport function success(message: string): void {\n console.log(` \\x1b[32m✓\\x1b[0m ${message}`);\n}\n\n/**\n * Log warning\n */\nexport function warn(message: string): void {\n console.log(` \\x1b[33m⚠\\x1b[0m ${message}`);\n}\n\n/**\n * Log error\n */\nexport function error(message: string): void {\n console.log(` \\x1b[31m✗\\x1b[0m ${message}`);\n}\n\n/**\n * Log info\n */\nexport function info(message: string): void {\n console.log(` ${message}`);\n}\n\n/**\n * Execute a command (or show in dry-run mode)\n */\nexport function exec(command: string, options?: { silent?: boolean }): string {\n if (dryRun) {\n if (!options?.silent) {\n console.log(` \\x1b[90m$ ${command}\\x1b[0m`);\n }\n return \"\";\n }\n\n try {\n return execSync(command, {\n encoding: \"utf-8\",\n stdio: options?.silent ? \"pipe\" : \"inherit\",\n });\n } catch (err) {\n const e = err as { status?: number; stderr?: string };\n throw new Error(`Command failed: ${command}\\n${e.stderr ?? \"\"}`);\n }\n}\n\n/**\n * Execute a command and return output (silent)\n */\nexport function execSilent(command: string): string {\n if (dryRun) {\n return \"\";\n }\n\n try {\n return execSync(command, {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n } catch {\n return \"\";\n }\n}\n\n/**\n * Check if a command exists\n */\nexport function commandExists(command: string): boolean {\n try {\n execSync(`which ${command}`, { stdio: \"pipe\" });\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if running as root\n */\nexport function isRoot(): boolean {\n return process.getuid?.() === 0;\n}\n\n/**\n * Write a file (or show in dry-run mode)\n */\nexport function writeFile(path: string, content: string): void {\n if (dryRun) {\n console.log(` \\x1b[90mWrite ${path}:\\x1b[0m`);\n const lines = content.split(\"\\n\").slice(0, 5);\n for (const line of lines) {\n console.log(` \\x1b[90m ${line}\\x1b[0m`);\n }\n if (content.split(\"\\n\").length > 5) {\n console.log(` \\x1b[90m ...\\x1b[0m`);\n }\n return;\n }\n\n fsWriteFile(path, content);\n}\n","import { step, success, info, exec, execSilent, warn } from \"./steps.js\";\n\n/**\n * Create the ddev user with proper permissions\n */\nexport function createDdevUser(): void {\n step(\"Create ddev user\");\n\n // Check if user exists\n const userExists = execSilent(\"id -u ddev 2>/dev/null\");\n if (userExists) {\n info(\"User 'ddev' already exists\");\n } else {\n exec(\"useradd -m -s /bin/bash ddev\");\n success(\"Created user 'ddev'\");\n }\n\n // Add to docker group (will be created by Docker install)\n exec(\"usermod -aG docker ddev 2>/dev/null || true\", { silent: true });\n\n // Create www directory\n exec(\"mkdir -p /home/ddev/www\");\n exec(\"chown ddev:ddev /home/ddev/www\");\n exec(\"chmod 750 /home/ddev/www\");\n success(\"Created /home/ddev/www\");\n\n // Create .ssh directory if needed\n exec(\"mkdir -p /home/ddev/.ssh\");\n exec(\"chmod 700 /home/ddev/.ssh\");\n exec(\"chown ddev:ddev /home/ddev/.ssh\");\n\n // Note: ddev user doesn't need sudo when DNS is properly configured\n // - Docker access: via docker group membership\n // - DDEV: runs as regular user, no sudo needed\n // - Agent: runs as systemd service (root manages it)\n // - Hostname resolution: via DNS (*.tld → server IP), not /etc/hosts\n //\n // If DNS is NOT configured for the custom TLD, DDEV will try to edit\n // /etc/hosts which requires sudo. In that case, you'd need:\n // echo \"ddev ALL=(ALL) NOPASSWD: /usr/local/bin/ddev-hostname\" >> /etc/sudoers.d/ddev\n // But it's better to just configure DNS properly.\n success(\"User ddev configured (no sudo needed with proper DNS)\");\n}\n\n/**\n * Setup authorized_keys for ddev user\n */\nexport function setupAuthorizedKeys(publicKey?: string): void {\n if (!publicKey) {\n warn(\"No SSH public key provided, skipping authorized_keys setup\");\n return;\n }\n\n step(\"Setup SSH authorized_keys\");\n\n const authKeysPath = \"/home/ddev/.ssh/authorized_keys\";\n exec(`touch ${authKeysPath}`);\n exec(`chmod 600 ${authKeysPath}`);\n exec(`chown ddev:ddev ${authKeysPath}`);\n\n // Check if key already exists\n const existing = execSilent(`cat ${authKeysPath}`);\n if (existing.includes(publicKey.trim())) {\n info(\"SSH key already authorized\");\n } else {\n exec(`echo '${publicKey}' >> ${authKeysPath}`);\n success(\"Added SSH public key\");\n }\n}\n","import { writeFileSync, existsSync, readFileSync } from \"node:fs\";\nimport { step, success, info, exec, commandExists, warn } from \"./steps.js\";\n\n/**\n * Install Docker using the official script\n */\nexport function installDocker(): void {\n step(\"Install Docker\");\n\n if (commandExists(\"docker\")) {\n const version = exec(\"docker --version\", { silent: true });\n info(`Docker already installed: ${version?.trim() ?? \"unknown version\"}`);\n return;\n }\n\n // Install Docker using official script\n info(\"Downloading Docker install script...\");\n exec(\"curl -fsSL https://get.docker.com -o /tmp/get-docker.sh\");\n exec(\"sh /tmp/get-docker.sh\");\n exec(\"rm /tmp/get-docker.sh\");\n\n // Enable and start Docker\n exec(\"systemctl enable docker\");\n exec(\"systemctl start docker\");\n\n success(\"Docker installed and started\");\n\n // Add ddev user to docker group\n exec(\"usermod -aG docker ddev\");\n success(\"Added ddev user to docker group\");\n}\n\n/**\n * Configure Docker for production use\n */\nexport function configureDocker(): void {\n step(\"Configure Docker\");\n\n // Create daemon.json with production settings\n const daemonConfig = {\n \"log-driver\": \"json-file\",\n \"log-opts\": {\n \"max-size\": \"10m\",\n \"max-file\": \"3\",\n },\n \"storage-driver\": \"overlay2\",\n \"live-restore\": true,\n };\n\n exec(\"mkdir -p /etc/docker\");\n\n const configPath = \"/etc/docker/daemon.json\";\n\n // Check if config already exists\n if (existsSync(configPath)) {\n try {\n const existing = JSON.parse(readFileSync(configPath, \"utf-8\"));\n // Merge with existing config\n const merged = { ...existing, ...daemonConfig };\n writeFileSync(configPath, JSON.stringify(merged, null, 2));\n info(\"Merged with existing Docker config\");\n } catch {\n warn(\"Could not parse existing Docker config, skipping\");\n return;\n }\n } else {\n writeFileSync(configPath, JSON.stringify(daemonConfig, null, 2));\n }\n\n // Reload Docker to apply config\n exec(\"systemctl reload docker || systemctl restart docker\");\n success(\"Docker configured with production settings\");\n}\n\n/**\n * Setup Docker system prune cron job\n */\nexport function setupDockerPrune(): void {\n step(\"Setup Docker cleanup cron\");\n\n const cronContent = `# Trafic: Clean up Docker resources weekly\n0 3 * * 0 root docker system prune -af --volumes 2>&1 | logger -t docker-prune\n`;\n\n writeFileSync(\"/etc/cron.d/trafic-docker-prune\", cronContent);\n exec(\"chmod 644 /etc/cron.d/trafic-docker-prune\");\n\n success(\"Docker prune scheduled weekly at 3am on Sundays\");\n}\n","import { writeFileSync } from \"node:fs\";\nimport { step, success, info, exec, commandExists } from \"./steps.js\";\n\n/**\n * Install DDEV\n */\nexport function installDdev(): void {\n step(\"Install DDEV\");\n\n if (commandExists(\"ddev\")) {\n const version = exec(\"ddev --version 2>/dev/null | head -1\", { silent: true });\n info(`DDEV already installed: ${version?.trim() ?? \"unknown version\"}`);\n return;\n }\n\n // Install DDEV using the official install script\n // Must run as ddev user (not root) per DDEV requirements\n info(\"Installing DDEV as user 'ddev'...\");\n exec(\n \"su - ddev -c 'curl -fsSL https://ddev.com/install.sh | bash -s -- --version latest'\",\n );\n\n // Symlink ddev binary to /usr/local/bin so it's available system-wide\n exec(\"ln -sf /home/ddev/.ddev/bin/ddev /usr/local/bin/ddev\");\n\n success(\"DDEV installed\");\n}\n\n/**\n * Configure DDEV global settings for production\n *\n * Note on DNS and /etc/hosts:\n * When using a custom TLD (not ddev.site), DDEV may try to edit /etc/hosts\n * if it can't resolve project hostnames via DNS. To avoid this:\n * 1. Configure DNS so *.{tld} resolves to the server's IP (recommended)\n * 2. Or install dnsmasq for local wildcard DNS resolution\n *\n * With proper DNS, DDEV won't need sudo for hostname management.\n */\nexport function configureDdev(tld: string, email?: string): void {\n step(\"Configure DDEV\");\n\n // Run as ddev user\n const ddevCmd = (cmd: string) => `su - ddev -c '${cmd}'`;\n\n // Configure global settings\n // use_dns_when_possible=true (default) means DDEV uses DNS resolution\n // and only falls back to /etc/hosts if DNS fails\n exec(\n ddevCmd(\n `ddev config global --project-tld=${tld} --router-http-port=80 --router-https-port=443 --use-letsencrypt=${email ? \"true\" : \"false\"} ${email ? `--letsencrypt-email=${email}` : \"\"}`,\n ),\n );\n\n success(`DDEV configured with TLD: ${tld}`);\n info(`Ensure DNS is configured: *.${tld} → server IP`);\n\n if (email) {\n success(`Let's Encrypt enabled with email: ${email}`);\n }\n\n // Disable instrumentation (telemetry)\n exec(ddevCmd(\"ddev config global --instrumentation-opt-in=false\"));\n\n // Start the router\n exec(ddevCmd(\"ddev poweroff || true\"));\n exec(ddevCmd(\"ddev start\"));\n\n success(\"DDEV router started\");\n}\n\n/**\n * Install dnsmasq for local DNS resolution (optional)\n * This allows the server itself to resolve *.{tld} hostnames\n * without needing to edit /etc/hosts for each project\n */\nexport function installDnsmasq(tld: string): void {\n step(\"Install dnsmasq for local DNS\");\n\n exec(\"apt-get update && apt-get install -y dnsmasq\", { silent: true });\n\n // Get server's public IP\n const serverIp = exec(\n \"curl -4 -s ifconfig.me || hostname -I | awk '{print $1}'\",\n { silent: true },\n )?.trim() || \"127.0.0.1\";\n\n // Configure dnsmasq to resolve *.{tld} to server IP\n const dnsmasqConfig = `# Trafic: Local DNS for DDEV projects\n# Resolve all *.${tld} to this server\naddress=/${tld}/${serverIp}\n\n# Don't read /etc/resolv.conf\nno-resolv\n\n# Use upstream DNS for everything else\nserver=1.1.1.1\nserver=8.8.8.8\n\n# Listen on localhost only\nlisten-address=127.0.0.1\nbind-interfaces\n`;\n\n writeFileSync(\"/etc/dnsmasq.d/trafic.conf\", dnsmasqConfig);\n\n // Configure system to use local dnsmasq\n exec(\"systemctl restart dnsmasq\");\n exec(\"systemctl enable dnsmasq\");\n\n // Update resolv.conf to use local DNS first\n info(\"Configure /etc/resolv.conf to use 127.0.0.1 as primary DNS\");\n\n success(`dnsmasq configured: *.${tld} → ${serverIp}`);\n}\n\n/**\n * Configure Traefik for forward auth\n */\nexport function configureTraefik(): void {\n step(\"Configure Traefik for forward auth\");\n\n // Create custom Traefik config directory\n exec(\"mkdir -p /home/ddev/.ddev/traefik\");\n exec(\"chown -R ddev:ddev /home/ddev/.ddev\");\n\n // Static configuration for Trafic middleware\n const staticConfig = `# Trafic: Forward auth middleware\nhttp:\n middlewares:\n trafic-auth:\n forwardAuth:\n address: \"http://host.docker.internal:9876/__auth__\"\n authResponseHeaders:\n - \"X-Trafic-Project\"\n\n trafic-errors:\n errors:\n status:\n - \"502\"\n - \"503\"\n service: trafic-service\n query: \"/\"\n\n services:\n trafic-service:\n loadBalancer:\n servers:\n - url: \"http://host.docker.internal:9876\"\n`;\n\n writeFileSync(\"/home/ddev/.ddev/traefik/trafic.yaml\", staticConfig);\n exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/trafic.yaml\");\n\n // Create default router config that applies middleware to all projects\n const routerConfig = `# Trafic: Default router configuration\n# This file makes all DDEV projects use the Trafic auth middleware\nhttp:\n routers:\n trafic-catch-all:\n rule: \"HostRegexp(\\`.+\\`)\"\n priority: 1\n middlewares:\n - trafic-auth\n - trafic-errors\n service: api@internal\n`;\n\n writeFileSync(\"/home/ddev/.ddev/traefik/dynamic.yaml\", routerConfig);\n exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/dynamic.yaml\");\n\n success(\"Traefik configured with Trafic middleware\");\n}\n","import { step, success, info, exec, commandExists } from \"./steps.js\";\nimport { writeFileSync } from \"node:fs\";\n\n/**\n * Install Node.js using fnm\n */\nexport function installNode(): void {\n step(\"Install Node.js\");\n\n // Check if Node is already installed\n if (commandExists(\"node\")) {\n const version = exec(\"node --version\", { silent: true });\n info(`Node.js already installed: ${version?.trim()}`);\n return;\n }\n\n // Install fnm (Fast Node Manager)\n exec(\n 'curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-dir \"/opt/fnm\" --skip-shell',\n );\n\n // Add fnm to system path\n const profileContent = `# Trafic: fnm (Node.js version manager)\nexport FNM_DIR=\"/opt/fnm\"\nexport PATH=\"/opt/fnm:$PATH\"\neval \"$(fnm env)\"\n`;\n\n writeFileSync(\"/etc/profile.d/fnm.sh\", profileContent);\n exec(\"chmod 644 /etc/profile.d/fnm.sh\");\n\n // Install Node.js 24\n exec(\"export FNM_DIR=/opt/fnm && /opt/fnm/fnm install 24\");\n exec(\"export FNM_DIR=/opt/fnm && /opt/fnm/fnm default 24\");\n\n // Create symlinks for system-wide access\n exec(\n \"ln -sf /opt/fnm/aliases/default/bin/node /usr/local/bin/node || true\",\n );\n exec(\"ln -sf /opt/fnm/aliases/default/bin/npm /usr/local/bin/npm || true\");\n exec(\"ln -sf /opt/fnm/aliases/default/bin/npx /usr/local/bin/npx || true\");\n\n success(\"Node.js 24 installed via fnm\");\n}\n\n/**\n * Install the Trafic agent\n */\nexport function installAgent(): void {\n step(\"Install Trafic agent\");\n\n // Install the agent globally\n exec(\"npm install -g @studiometa/trafic-agent\");\n\n success(\"Trafic agent installed\");\n}\n\n/**\n * Create the agent configuration\n */\nexport function createAgentConfig(tld: string, _email?: string): void {\n step(\"Create agent configuration\");\n\n const config = `# Trafic agent configuration\n# Generated by trafic setup\n\n# Required: TLD for DDEV projects\ntld = \"${tld}\"\n\n# Agent HTTP server port\nport = 9876\n\n# Database and paths\ndb_path = \"/var/lib/trafic/db.sqlite\"\nproject_list_path = \"/home/ddev/.ddev/project_list.yaml\"\nprojects_dir = \"/home/ddev/www\"\n\n# Scale-to-zero settings\nidle_timeout = \"4h\"\nidle_check_interval = \"5m\"\n\n# Authentication\n[auth]\ndefault_policy = \"basic\"\nallowed_ips = []\ntokens = []\nbasic_auth = []\nrules = []\n`;\n\n writeFileSync(\"/etc/trafic/config.toml\", config);\n exec(\"chmod 640 /etc/trafic/config.toml\");\n exec(\"chown root:ddev /etc/trafic/config.toml\");\n\n success(\"Configuration created at /etc/trafic/config.toml\");\n info(\"Edit this file to configure authentication\");\n}\n\n/**\n * Create systemd service for the agent\n */\nexport function createSystemdService(): void {\n step(\"Create systemd service\");\n\n const serviceContent = `[Unit]\nDescription=Trafic Agent - DDEV preview environment manager\nAfter=network.target docker.service\nRequires=docker.service\n\n[Service]\nType=simple\nUser=ddev\nGroup=ddev\nExecStart=/usr/local/bin/npx trafic-agent start --config /etc/trafic/config.toml\nRestart=always\nRestartSec=5\nStandardOutput=journal\nStandardError=journal\nSyslogIdentifier=trafic-agent\n\n# Environment\nEnvironment=NODE_ENV=production\n\n# Security hardening\nNoNewPrivileges=true\nProtectSystem=strict\nProtectHome=read-only\nReadWritePaths=/var/lib/trafic /home/ddev/.ddev\nPrivateTmp=true\n\n[Install]\nWantedBy=multi-user.target\n`;\n\n writeFileSync(\"/etc/systemd/system/trafic-agent.service\", serviceContent);\n exec(\"chmod 644 /etc/systemd/system/trafic-agent.service\");\n\n // Reload systemd and enable service\n exec(\"systemctl daemon-reload\");\n exec(\"systemctl enable trafic-agent\");\n exec(\"systemctl start trafic-agent\");\n\n success(\"Trafic agent service started\");\n info(\"View logs: journalctl -u trafic-agent -f\");\n}\n","import { step, success, info, exec, warn, commandExists } from \"./steps.js\";\nimport { writeFileSync, existsSync } from \"node:fs\";\n\n/**\n * Configure SSH hardening\n */\nexport function hardenSsh(allowedUsers: string[] = [\"ddev\"]): void {\n step(\"Harden SSH configuration\");\n\n const sshConfig = `# Trafic SSH hardening\n# Generated by trafic setup\n\n# Disable root login\nPermitRootLogin no\n\n# Disable password authentication (keys only)\nPasswordAuthentication no\nPubkeyAuthentication yes\n\n# Limit authentication attempts\nMaxAuthTries 3\n\n# Idle timeout (5 minutes)\nClientAliveInterval 300\nClientAliveCountMax 2\n\n# Allowed users\nAllowUsers ${allowedUsers.join(\" \")}\n\n# Disable X11 forwarding\nX11Forwarding no\n\n# Disable TCP forwarding\nAllowTcpForwarding no\n\n# Use strong algorithms only\nKexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com\nMACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com\n`;\n\n writeFileSync(\"/etc/ssh/sshd_config.d/trafic.conf\", sshConfig);\n exec(\"chmod 644 /etc/ssh/sshd_config.d/trafic.conf\");\n\n // Test config before restarting\n const testResult = exec(\"sshd -t 2>&1 || echo 'error'\", { silent: true });\n if (testResult?.includes(\"error\")) {\n warn(\"SSH config test failed, reverting...\");\n exec(\"rm /etc/ssh/sshd_config.d/trafic.conf\");\n return;\n }\n\n exec(\"systemctl reload sshd || systemctl reload ssh\");\n success(\"SSH hardened: root login disabled, password auth disabled\");\n info(`Allowed users: ${allowedUsers.join(\", \")}`);\n}\n\n/**\n * Configure UFW firewall\n */\nexport function configureFirewall(): void {\n step(\"Configure firewall (UFW)\");\n\n // Install UFW if not present\n if (!commandExists(\"ufw\")) {\n exec(\"apt-get update && apt-get install -y ufw\", { silent: true });\n }\n\n // Reset and configure\n exec(\"ufw --force reset\", { silent: true });\n exec(\"ufw default deny incoming\");\n exec(\"ufw default allow outgoing\");\n exec(\"ufw allow 22/tcp comment 'SSH'\");\n exec(\"ufw allow 80/tcp comment 'HTTP'\");\n exec(\"ufw allow 443/tcp comment 'HTTPS'\");\n\n // Enable firewall\n exec(\"ufw --force enable\");\n\n success(\"Firewall enabled: SSH (22), HTTP (80), HTTPS (443) allowed\");\n}\n\n/**\n * Configure automatic security updates\n */\nexport function configureUnattendedUpgrades(): void {\n step(\"Configure automatic security updates\");\n\n // Install unattended-upgrades\n exec(\n \"DEBIAN_FRONTEND=noninteractive apt-get install -y unattended-upgrades apt-listchanges\",\n { silent: true },\n );\n\n // Configure for security updates only\n const upgradesConfig = `// Trafic: Automatic security updates\nUnattended-Upgrade::Allowed-Origins {\n \"\\${distro_id}:\\${distro_codename}\";\n \"\\${distro_id}:\\${distro_codename}-security\";\n \"\\${distro_id}ESMApps:\\${distro_codename}-apps-security\";\n \"\\${distro_id}ESM:\\${distro_codename}-infra-security\";\n};\n\n// Do not automatically reboot\nUnattended-Upgrade::Automatic-Reboot \"false\";\n\n// Remove unused dependencies\nUnattended-Upgrade::Remove-Unused-Dependencies \"true\";\n\n// Email notifications (optional)\n// Unattended-Upgrade::Mail \"root\";\n`;\n\n writeFileSync(\n \"/etc/apt/apt.conf.d/50unattended-upgrades\",\n upgradesConfig,\n );\n\n // Enable automatic updates\n const autoConfig = `APT::Periodic::Update-Package-Lists \"1\";\nAPT::Periodic::Download-Upgradeable-Packages \"1\";\nAPT::Periodic::AutocleanInterval \"7\";\nAPT::Periodic::Unattended-Upgrade \"1\";\n`;\n\n writeFileSync(\"/etc/apt/apt.conf.d/20auto-upgrades\", autoConfig);\n\n // Enable the service\n exec(\"systemctl enable unattended-upgrades\");\n exec(\"systemctl start unattended-upgrades\");\n\n success(\"Automatic security updates enabled\");\n}\n\n/**\n * Configure Fail2ban\n */\nexport function configureFail2ban(): void {\n step(\"Configure Fail2ban\");\n\n // Install fail2ban\n if (!commandExists(\"fail2ban-client\")) {\n exec(\"apt-get update && apt-get install -y fail2ban\", { silent: true });\n }\n\n // Configure jail\n const jailConfig = `# Trafic: Fail2ban configuration\n[DEFAULT]\nbantime = 3600\nfindtime = 600\nmaxretry = 5\nbackend = auto\n\n[sshd]\nenabled = true\nport = ssh\nfilter = sshd\nlogpath = /var/log/auth.log\nmaxretry = 3\nbantime = 7200\n`;\n\n writeFileSync(\"/etc/fail2ban/jail.local\", jailConfig);\n exec(\"chmod 644 /etc/fail2ban/jail.local\");\n\n // Restart fail2ban\n exec(\"systemctl enable fail2ban\");\n exec(\"systemctl restart fail2ban\");\n\n success(\"Fail2ban enabled: SSH brute force protection active\");\n}\n\n/**\n * Configure system limits\n */\nexport function configureSystemLimits(): void {\n step(\"Configure system limits\");\n\n const limitsConfig = `# Trafic: System limits for ddev user\nddev soft nofile 65536\nddev hard nofile 65536\nddev soft nproc 4096\nddev hard nproc 4096\n`;\n\n writeFileSync(\"/etc/security/limits.d/trafic.conf\", limitsConfig);\n exec(\"chmod 644 /etc/security/limits.d/trafic.conf\");\n\n success(\"System limits configured for ddev user\");\n}\n\n/**\n * Set proper file permissions\n */\nexport function configureFilePermissions(): void {\n step(\"Configure file permissions\");\n\n // Create directories\n exec(\"mkdir -p /etc/trafic\");\n exec(\"mkdir -p /var/lib/trafic\");\n exec(\"mkdir -p /var/backups/trafic\");\n\n // Set permissions\n exec(\"chmod 755 /etc/trafic\");\n exec(\"chmod 700 /var/lib/trafic\");\n exec(\"chmod 700 /var/backups/trafic\");\n exec(\"chown root:ddev /etc/trafic\");\n exec(\"chown ddev:ddev /var/lib/trafic\");\n exec(\"chown ddev:ddev /var/backups/trafic\");\n\n // Config file (if exists)\n if (existsSync(\"/etc/trafic/config.toml\")) {\n exec(\"chmod 640 /etc/trafic/config.toml\");\n exec(\"chown root:ddev /etc/trafic/config.toml\");\n }\n\n success(\"File permissions configured\");\n}\n\n/**\n * Run all hardening steps\n */\nexport function hardenServer(sshUsers: string[] = [\"ddev\"]): void {\n hardenSsh(sshUsers);\n configureFirewall();\n configureUnattendedUpgrades();\n configureFail2ban();\n configureSystemLimits();\n configureFilePermissions();\n}\n","import { execSync } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport type { AuditCheck } from \"./types.js\";\n\n/**\n * Run a command silently and return output\n */\nfunction execSilent(command: string): string {\n try {\n return execSync(command, { encoding: \"utf-8\", stdio: \"pipe\" }).trim();\n } catch {\n return \"\";\n }\n}\n\n/**\n * Check if a service is active\n */\nfunction isServiceActive(service: string): boolean {\n const status = execSilent(`systemctl is-active ${service}`);\n return status === \"active\";\n}\n\n/**\n * Audit SSH configuration\n */\nfunction auditSsh(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Check root login\n const sshdConfig = existsSync(\"/etc/ssh/sshd_config\")\n ? readFileSync(\"/etc/ssh/sshd_config\", \"utf-8\")\n : \"\";\n const traficConfig = existsSync(\"/etc/ssh/sshd_config.d/trafic.conf\")\n ? readFileSync(\"/etc/ssh/sshd_config.d/trafic.conf\", \"utf-8\")\n : \"\";\n\n const config = sshdConfig + \"\\n\" + traficConfig;\n\n const rootLoginDisabled =\n config.includes(\"PermitRootLogin no\") ||\n config.includes(\"PermitRootLogin prohibit-password\");\n checks.push({\n name: \"SSH root login\",\n status: rootLoginDisabled ? \"pass\" : \"fail\",\n message: rootLoginDisabled\n ? \"Root login disabled\"\n : \"Root login may be enabled\",\n fix: \"Add 'PermitRootLogin no' to /etc/ssh/sshd_config.d/trafic.conf\",\n });\n\n const passwordAuthDisabled = config.includes(\"PasswordAuthentication no\");\n checks.push({\n name: \"SSH password auth\",\n status: passwordAuthDisabled ? \"pass\" : \"warn\",\n message: passwordAuthDisabled\n ? \"Password authentication disabled\"\n : \"Password authentication may be enabled\",\n fix: \"Add 'PasswordAuthentication no' to /etc/ssh/sshd_config.d/trafic.conf\",\n });\n\n return checks;\n}\n\n/**\n * Audit firewall configuration\n */\nfunction auditFirewall(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n const ufwStatus = execSilent(\"ufw status\");\n const isActive = ufwStatus.includes(\"Status: active\");\n\n checks.push({\n name: \"Firewall (UFW)\",\n status: isActive ? \"pass\" : \"fail\",\n message: isActive ? \"UFW is active\" : \"UFW is not active\",\n fix: \"Run: ufw enable\",\n });\n\n if (isActive) {\n const allows22 = ufwStatus.includes(\"22/tcp\");\n const allows80 = ufwStatus.includes(\"80/tcp\");\n const allows443 = ufwStatus.includes(\"443/tcp\");\n\n if (allows22 && allows80 && allows443) {\n checks.push({\n name: \"Firewall ports\",\n status: \"pass\",\n message: \"Required ports (22, 80, 443) are open\",\n });\n } else {\n checks.push({\n name: \"Firewall ports\",\n status: \"warn\",\n message: \"Some required ports may be blocked\",\n fix: \"Run: ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp\",\n });\n }\n }\n\n return checks;\n}\n\n/**\n * Audit security services\n */\nfunction auditServices(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Fail2ban\n const fail2banActive = isServiceActive(\"fail2ban\");\n checks.push({\n name: \"Fail2ban\",\n status: fail2banActive ? \"pass\" : \"warn\",\n message: fail2banActive ? \"Fail2ban is active\" : \"Fail2ban is not running\",\n fix: \"Run: apt install fail2ban && systemctl enable fail2ban --now\",\n });\n\n // Unattended upgrades\n const unattendedActive = isServiceActive(\"unattended-upgrades\");\n checks.push({\n name: \"Automatic updates\",\n status: unattendedActive ? \"pass\" : \"warn\",\n message: unattendedActive\n ? \"Unattended upgrades enabled\"\n : \"Automatic updates not configured\",\n fix: \"Run: apt install unattended-upgrades && dpkg-reconfigure unattended-upgrades\",\n });\n\n // Trafic agent\n const agentActive = isServiceActive(\"trafic-agent\");\n checks.push({\n name: \"Trafic agent\",\n status: agentActive ? \"pass\" : \"fail\",\n message: agentActive\n ? \"Trafic agent is running\"\n : \"Trafic agent is not running\",\n fix: \"Run: systemctl start trafic-agent\",\n });\n\n return checks;\n}\n\n/**\n * Audit Docker\n */\nfunction auditDocker(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Docker running\n const dockerActive = isServiceActive(\"docker\");\n checks.push({\n name: \"Docker\",\n status: dockerActive ? \"pass\" : \"fail\",\n message: dockerActive ? \"Docker is running\" : \"Docker is not running\",\n fix: \"Run: systemctl start docker\",\n });\n\n // Dangling images\n const danglingImages = execSilent(\n \"docker images -f dangling=true -q | wc -l\",\n );\n const count = parseInt(danglingImages, 10) || 0;\n if (count > 0) {\n checks.push({\n name: \"Docker cleanup\",\n status: \"warn\",\n message: `${count} dangling Docker images`,\n fix: \"Run: docker system prune -f\",\n });\n }\n\n // Disk usage\n const diskUsage = execSilent(\"df -h / | awk 'NR==2{print $5}'\");\n const usagePercent = parseInt(diskUsage, 10) || 0;\n checks.push({\n name: \"Disk usage\",\n status: usagePercent > 90 ? \"fail\" : usagePercent > 80 ? \"warn\" : \"pass\",\n message: `Disk usage: ${diskUsage}`,\n fix: usagePercent > 80 ? \"Consider cleaning up old projects or Docker resources\" : undefined,\n });\n\n return checks;\n}\n\n/**\n * Audit file permissions\n */\nfunction auditPermissions(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Config file\n if (existsSync(\"/etc/trafic/config.toml\")) {\n const stats = execSilent(\"stat -c '%a' /etc/trafic/config.toml\");\n const isSecure = stats === \"640\" || stats === \"600\";\n checks.push({\n name: \"Config permissions\",\n status: isSecure ? \"pass\" : \"warn\",\n message: isSecure\n ? \"Config file has secure permissions\"\n : `Config file permissions: ${stats} (should be 640)`,\n fix: \"Run: chmod 640 /etc/trafic/config.toml\",\n });\n }\n\n // Projects directory\n if (existsSync(\"/home/ddev/www\")) {\n const owner = execSilent(\"stat -c '%U:%G' /home/ddev/www\");\n const isCorrect = owner === \"ddev:ddev\";\n checks.push({\n name: \"Projects directory\",\n status: isCorrect ? \"pass\" : \"warn\",\n message: isCorrect\n ? \"Projects directory owned by ddev\"\n : `Projects directory owner: ${owner}`,\n fix: \"Run: chown ddev:ddev /home/ddev/www\",\n });\n }\n\n return checks;\n}\n\n/**\n * Run all audit checks\n */\nexport function runAudit(): AuditCheck[] {\n return [\n ...auditSsh(),\n ...auditFirewall(),\n ...auditServices(),\n ...auditDocker(),\n ...auditPermissions(),\n ];\n}\n\n/**\n * Print audit results\n */\nexport function printAuditResults(checks: AuditCheck[]): void {\n console.log(\"\\n\\x1b[1mSecurity Audit Results\\x1b[0m\\n\");\n\n for (const check of checks) {\n const icon =\n check.status === \"pass\"\n ? \"\\x1b[32m✓\\x1b[0m\"\n : check.status === \"warn\"\n ? \"\\x1b[33m⚠\\x1b[0m\"\n : \"\\x1b[31m✗\\x1b[0m\";\n\n console.log(`${icon} \\x1b[1m${check.name}\\x1b[0m: ${check.message}`);\n\n if (check.fix && check.status !== \"pass\") {\n console.log(` \\x1b[90m${check.fix}\\x1b[0m`);\n }\n }\n\n const passed = checks.filter((c) => c.status === \"pass\").length;\n const warnings = checks.filter((c) => c.status === \"warn\").length;\n const failed = checks.filter((c) => c.status === \"fail\").length;\n\n console.log(\n `\\n\\x1b[1mSummary:\\x1b[0m ${passed} passed, ${warnings} warnings, ${failed} failed`,\n );\n}\n","import type { SetupOptions } from \"./types.js\";\nimport { step, info, error, setDryRun, resetSteps, isRoot } from \"./steps.js\";\nimport { createDdevUser } from \"./user.js\";\nimport { installDocker, configureDocker, setupDockerPrune } from \"./docker.js\";\nimport { installDdev, configureDdev, configureTraefik } from \"./ddev.js\";\nimport { installNode, installAgent, createAgentConfig, createSystemdService } from \"./agent.js\";\nimport { hardenServer } from \"./hardening.js\";\nimport { runAudit, printAuditResults } from \"./audit.js\";\n\nexport * from \"./types.js\";\nexport * from \"./audit.js\";\n\n/**\n * Run the full server setup\n */\nexport async function setup(options: SetupOptions): Promise<void> {\n resetSteps();\n setDryRun(options.dryRun ?? false);\n\n console.log(\"\\n\\x1b[1m🚦 Trafic Server Setup\\x1b[0m\\n\");\n\n if (options.dryRun) {\n console.log(\"\\x1b[33m⚠ Dry-run mode: no changes will be made\\x1b[0m\\n\");\n }\n\n // Check root\n if (!isRoot() && !options.dryRun) {\n error(\"This command must be run as root\");\n console.log(\" Run: sudo trafic-agent setup ...\");\n process.exit(1);\n }\n\n info(`TLD: ${options.tld}`);\n if (options.email) {\n info(`Let's Encrypt email: ${options.email}`);\n }\n if (options.noHardening) {\n info(\"Hardening: disabled\");\n }\n\n try {\n // Step 1: Create ddev user\n createDdevUser();\n\n // Step 2: Install Docker\n if (!options.noDocker) {\n installDocker();\n configureDocker();\n setupDockerPrune();\n } else {\n step(\"Install Docker\");\n info(\"Skipped (--no-docker)\");\n }\n\n // Step 3: Install DDEV\n if (!options.noDdev) {\n installDdev();\n configureDdev(options.tld, options.email);\n configureTraefik();\n } else {\n step(\"Install DDEV\");\n info(\"Skipped (--no-ddev)\");\n }\n\n // Step 4: Install Node.js\n installNode();\n\n // Step 5: Install and configure agent\n installAgent();\n createAgentConfig(options.tld, options.email);\n createSystemdService();\n\n // Step 6: Server hardening\n if (!options.noHardening) {\n const sshUsers = options.sshUsers ?? [\"ddev\"];\n hardenServer(sshUsers);\n } else {\n step(\"Server hardening\");\n info(\"Skipped (--no-hardening)\");\n }\n\n // Done\n console.log(\"\\n\\x1b[32m✓ Setup complete!\\x1b[0m\\n\");\n console.log(\"Next steps:\");\n console.log(` 1. Edit /etc/trafic/config.toml to configure authentication`);\n console.log(` 2. Add SSH public keys to /home/ddev/.ssh/authorized_keys`);\n console.log(` 3. Deploy your first project:`);\n console.log(` npx @studiometa/trafic-cli deploy --host=<this-server> --name=my-app`);\n console.log(`\\nYour projects will be available at https://<name>.${options.tld}`);\n } catch (err) {\n error(`Setup failed: ${err instanceof Error ? err.message : err}`);\n process.exit(1);\n }\n}\n\n/**\n * Run security audit\n */\nexport async function audit(): Promise<void> {\n const checks = runAudit();\n printAuditResults(checks);\n\n const failed = checks.filter((c) => c.status === \"fail\").length;\n if (failed > 0) {\n process.exit(1);\n }\n}\n"],"names":["info","success","execSilent"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a, l, p, b, r, s, c, d, v } from "./index-
|
|
1
|
+
import { a, l, p, b, r, s, c, d, v } from "./index-CO8UzDfr.js";
|
|
2
2
|
import { b as b2, c as c2, a as a2, d as d2, g, e, f, h, i, j, k, l as l2, m, n, o, p as p2, q, r as r2, s as s2, t, u, v as v2, w, x, y, z, A } from "./server-CHT47EXr.js";
|
|
3
3
|
export {
|
|
4
4
|
a as audit,
|
package/dist/setup/ddev.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ddev.d.ts","sourceRoot":"","sources":["../../src/setup/ddev.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ddev.d.ts","sourceRoot":"","sources":["../../src/setup/ddev.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAgB,WAAW,IAAI,IAAI,CAoBlC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CA8B/D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAsChD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAqDvC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docker.d.ts","sourceRoot":"","sources":["../../src/setup/docker.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"docker.d.ts","sourceRoot":"","sources":["../../src/setup/docker.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAwBpC;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAqCtC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAWvC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"steps.d.ts","sourceRoot":"","sources":["../../src/setup/steps.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"steps.d.ts","sourceRoot":"","sources":["../../src/setup/steps.ts"],"names":[],"mappings":"AAMA;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAE9C;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,IAAI,CAEjC;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAG1C;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE3C;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,CAiB5E;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAalD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAOtD;AAED;;GAEG;AACH,wBAAgB,MAAM,IAAI,OAAO,CAEhC;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAc7D"}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index-C7OaqqbZ.js","sources":["../src/utils/config.ts","../src/tasks/stop-idle.ts","../src/setup/steps.ts","../src/setup/user.ts","../src/setup/docker.ts","../src/setup/ddev.ts","../src/setup/agent.ts","../src/setup/hardening.ts","../src/setup/audit.ts","../src/setup/index.ts"],"sourcesContent":["import { readFileSync, existsSync } from \"node:fs\";\nimport { parse } from \"smol-toml\";\nimport type { AgentConfig, AuthConfig, AuthRule } from \"../types.js\";\n\nconst DEFAULT_CONFIG_PATHS = [\n \"/etc/trafic/config.toml\",\n \"./config.toml\",\n \"./trafic.toml\",\n];\n\nconst DEFAULT_CONFIG: AgentConfig = {\n tld: \"\",\n port: 9876,\n dbPath: \"/var/lib/trafic/db.sqlite\",\n projectListPath: \"/home/ddev/.ddev/project_list.yaml\",\n projectsDir: \"/home/ddev/www\",\n idleTimeout: \"30m\",\n idleCheckInterval: \"5m\",\n auth: {\n defaultPolicy: \"basic\",\n allowedIps: [],\n tokens: [],\n basicAuth: [],\n rules: [],\n },\n};\n\n/**\n * Parse a duration string (e.g., \"30m\", \"1h\", \"2h30m\") to milliseconds\n */\nexport function parseDuration(duration: string): number {\n const regex = /(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?/;\n const match = duration.match(regex);\n if (!match) return 0;\n\n const hours = parseInt(match[1] ?? \"0\", 10);\n const minutes = parseInt(match[2] ?? \"0\", 10);\n const seconds = parseInt(match[3] ?? \"0\", 10);\n\n return (hours * 3600 + minutes * 60 + seconds) * 1000;\n}\n\n/**\n * Load configuration from TOML file\n */\nexport function loadConfig(configPath?: string): AgentConfig {\n // Find config file\n let path = configPath;\n if (!path) {\n path = DEFAULT_CONFIG_PATHS.find((p) => existsSync(p));\n }\n\n if (!path || !existsSync(path)) {\n console.warn(\"No config file found, using defaults\");\n return DEFAULT_CONFIG;\n }\n\n const content = readFileSync(path, \"utf-8\");\n const parsed = parse(content) as Record<string, unknown>;\n\n // Merge with defaults\n const auth = mergeAuthConfig(parsed.auth as Record<string, unknown>);\n\n return {\n tld: (parsed.tld as string) ?? DEFAULT_CONFIG.tld,\n port: (parsed.port as number) ?? DEFAULT_CONFIG.port,\n dbPath: (parsed.db_path as string) ?? DEFAULT_CONFIG.dbPath,\n projectListPath:\n (parsed.project_list_path as string) ?? DEFAULT_CONFIG.projectListPath,\n projectsDir: (parsed.projects_dir as string) ?? DEFAULT_CONFIG.projectsDir,\n idleTimeout:\n (parsed.idle_timeout as string) ?? DEFAULT_CONFIG.idleTimeout,\n idleCheckInterval:\n (parsed.idle_check_interval as string) ??\n DEFAULT_CONFIG.idleCheckInterval,\n auth,\n };\n}\n\nfunction mergeAuthConfig(raw?: Record<string, unknown>): AuthConfig {\n if (!raw) return DEFAULT_CONFIG.auth;\n\n const rules = (raw.rules as Array<Record<string, unknown>>) ?? [];\n\n return {\n defaultPolicy:\n (raw.default_policy as AuthConfig[\"defaultPolicy\"]) ??\n DEFAULT_CONFIG.auth.defaultPolicy,\n allowedIps:\n (raw.allowed_ips as string[]) ?? DEFAULT_CONFIG.auth.allowedIps,\n tokens: (raw.tokens as string[]) ?? DEFAULT_CONFIG.auth.tokens,\n basicAuth: (raw.basic_auth as string[]) ?? DEFAULT_CONFIG.auth.basicAuth,\n rules: rules.map(\n (r): AuthRule => ({\n match: r.match as string,\n policy: r.policy as AuthRule[\"policy\"],\n tokens: r.tokens as string[] | undefined,\n allowedIps: r.allowed_ips as string[] | undefined,\n }),\n ),\n };\n}\n\n/**\n * Validate configuration\n */\nexport function validateConfig(config: AgentConfig): string[] {\n const errors: string[] = [];\n\n if (!config.tld) {\n errors.push(\"tld is required\");\n }\n\n if (config.port < 1 || config.port > 65535) {\n errors.push(\"port must be between 1 and 65535\");\n }\n\n if (\n config.auth.defaultPolicy !== \"allow\" &&\n config.auth.defaultPolicy !== \"deny\" &&\n config.auth.defaultPolicy !== \"basic\" &&\n config.auth.defaultPolicy !== \"token\"\n ) {\n errors.push('auth.default_policy must be \"allow\", \"deny\", \"basic\", or \"token\"');\n }\n\n return errors;\n}\n","import { getIdleProjects, setProjectStatus } from \"../utils/db.js\";\nimport { stopProject, getProjectInfo, loadProjectList } from \"../utils/ddev.js\";\nimport { parseDuration } from \"../utils/config.js\";\nimport { loadProjectConfig, shouldNeverStop, getIdleTimeoutMs } from \"../utils/project-config.js\";\nimport type { AgentConfig } from \"../types.js\";\n\n/**\n * Stop idle projects that haven't been accessed recently\n * Respects per-project idle_timeout settings from .ddev/config.trafic.yaml\n */\nexport function stopIdleProjects(config: AgentConfig): void {\n const globalThresholdMs = parseDuration(config.idleTimeout);\n\n // Get all projects to check their individual configs\n const projects = loadProjectList(config.projectListPath);\n\n // Get projects idle according to global threshold\n const idleProjects = getIdleProjects(globalThresholdMs);\n\n for (const project of idleProjects) {\n const projectDir = projects.get(project.name);\n\n // Load per-project config if available\n if (projectDir) {\n const projectConfig = loadProjectConfig(projectDir);\n\n // Skip if project is configured to never stop\n if (shouldNeverStop(projectConfig)) {\n continue;\n }\n\n // Check project-specific idle timeout\n const projectThresholdMs = getIdleTimeoutMs(projectConfig);\n if (projectThresholdMs !== undefined) {\n const idleDuration = Date.now() - project.lastAccess;\n if (idleDuration < projectThresholdMs) {\n // Not idle according to project-specific timeout\n continue;\n }\n }\n }\n\n // Double-check project is actually running via DDEV\n const info = getProjectInfo(project.name);\n if (info?.status !== \"running\") {\n // Already stopped, just update our record\n setProjectStatus(project.name, \"stopped\");\n continue;\n }\n\n console.log(`Stopping idle project: ${project.name}`);\n const success = stopProject(project.name);\n\n if (success) {\n setProjectStatus(project.name, \"stopped\");\n console.log(`Stopped: ${project.name}`);\n } else {\n console.error(`Failed to stop: ${project.name}`);\n }\n }\n}\n\n/**\n * Start the idle check scheduler\n */\nexport function startIdleScheduler(config: AgentConfig): NodeJS.Timeout {\n const intervalMs = parseDuration(config.idleCheckInterval);\n\n console.log(\n `Idle scheduler started: checking every ${config.idleCheckInterval}, timeout ${config.idleTimeout}`,\n );\n\n // Run immediately on start\n stopIdleProjects(config);\n\n // Then run on interval\n return setInterval(() => {\n stopIdleProjects(config);\n }, intervalMs);\n}\n","import { execSync } from \"node:child_process\";\n\nlet stepNumber = 0;\nlet dryRun = false;\n\n/**\n * Set dry-run mode\n */\nexport function setDryRun(value: boolean): void {\n dryRun = value;\n}\n\n/**\n * Reset step counter\n */\nexport function resetSteps(): void {\n stepNumber = 0;\n}\n\n/**\n * Log a step\n */\nexport function step(message: string): void {\n stepNumber++;\n console.log(`\\n\\x1b[36m[${stepNumber}]\\x1b[0m ${message}`);\n}\n\n/**\n * Log success\n */\nexport function success(message: string): void {\n console.log(` \\x1b[32m✓\\x1b[0m ${message}`);\n}\n\n/**\n * Log warning\n */\nexport function warn(message: string): void {\n console.log(` \\x1b[33m⚠\\x1b[0m ${message}`);\n}\n\n/**\n * Log error\n */\nexport function error(message: string): void {\n console.log(` \\x1b[31m✗\\x1b[0m ${message}`);\n}\n\n/**\n * Log info\n */\nexport function info(message: string): void {\n console.log(` ${message}`);\n}\n\n/**\n * Execute a command (or show in dry-run mode)\n */\nexport function exec(command: string, options?: { silent?: boolean }): string {\n if (dryRun) {\n if (!options?.silent) {\n console.log(` \\x1b[90m$ ${command}\\x1b[0m`);\n }\n return \"\";\n }\n\n try {\n return execSync(command, {\n encoding: \"utf-8\",\n stdio: options?.silent ? \"pipe\" : \"inherit\",\n });\n } catch (err) {\n const e = err as { status?: number; stderr?: string };\n throw new Error(`Command failed: ${command}\\n${e.stderr ?? \"\"}`);\n }\n}\n\n/**\n * Execute a command and return output (silent)\n */\nexport function execSilent(command: string): string {\n if (dryRun) {\n return \"\";\n }\n\n try {\n return execSync(command, {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n } catch {\n return \"\";\n }\n}\n\n/**\n * Check if a command exists\n */\nexport function commandExists(command: string): boolean {\n try {\n execSync(`which ${command}`, { stdio: \"pipe\" });\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if running as root\n */\nexport function isRoot(): boolean {\n return process.getuid?.() === 0;\n}\n\n/**\n * Write a file (or show in dry-run mode)\n */\nexport function writeFile(path: string, content: string): void {\n if (dryRun) {\n console.log(` \\x1b[90mWrite ${path}:\\x1b[0m`);\n const lines = content.split(\"\\n\").slice(0, 5);\n for (const line of lines) {\n console.log(` \\x1b[90m ${line}\\x1b[0m`);\n }\n if (content.split(\"\\n\").length > 5) {\n console.log(` \\x1b[90m ...\\x1b[0m`);\n }\n return;\n }\n\n const { writeFileSync } = require(\"node:fs\");\n writeFileSync(path, content);\n}\n","import { step, success, info, exec, execSilent, warn } from \"./steps.js\";\n\n/**\n * Create the ddev user with proper permissions\n */\nexport function createDdevUser(): void {\n step(\"Create ddev user\");\n\n // Check if user exists\n const userExists = execSilent(\"id -u ddev 2>/dev/null\");\n if (userExists) {\n info(\"User 'ddev' already exists\");\n } else {\n exec(\"useradd -m -s /bin/bash ddev\");\n success(\"Created user 'ddev'\");\n }\n\n // Add to docker group (will be created by Docker install)\n exec(\"usermod -aG docker ddev 2>/dev/null || true\", { silent: true });\n\n // Create www directory\n exec(\"mkdir -p /home/ddev/www\");\n exec(\"chown ddev:ddev /home/ddev/www\");\n exec(\"chmod 750 /home/ddev/www\");\n success(\"Created /home/ddev/www\");\n\n // Create .ssh directory if needed\n exec(\"mkdir -p /home/ddev/.ssh\");\n exec(\"chmod 700 /home/ddev/.ssh\");\n exec(\"chown ddev:ddev /home/ddev/.ssh\");\n\n // Note: ddev user doesn't need sudo when DNS is properly configured\n // - Docker access: via docker group membership\n // - DDEV: runs as regular user, no sudo needed\n // - Agent: runs as systemd service (root manages it)\n // - Hostname resolution: via DNS (*.tld → server IP), not /etc/hosts\n //\n // If DNS is NOT configured for the custom TLD, DDEV will try to edit\n // /etc/hosts which requires sudo. In that case, you'd need:\n // echo \"ddev ALL=(ALL) NOPASSWD: /usr/local/bin/ddev-hostname\" >> /etc/sudoers.d/ddev\n // But it's better to just configure DNS properly.\n success(\"User ddev configured (no sudo needed with proper DNS)\");\n}\n\n/**\n * Setup authorized_keys for ddev user\n */\nexport function setupAuthorizedKeys(publicKey?: string): void {\n if (!publicKey) {\n warn(\"No SSH public key provided, skipping authorized_keys setup\");\n return;\n }\n\n step(\"Setup SSH authorized_keys\");\n\n const authKeysPath = \"/home/ddev/.ssh/authorized_keys\";\n exec(`touch ${authKeysPath}`);\n exec(`chmod 600 ${authKeysPath}`);\n exec(`chown ddev:ddev ${authKeysPath}`);\n\n // Check if key already exists\n const existing = execSilent(`cat ${authKeysPath}`);\n if (existing.includes(publicKey.trim())) {\n info(\"SSH key already authorized\");\n } else {\n exec(`echo '${publicKey}' >> ${authKeysPath}`);\n success(\"Added SSH public key\");\n }\n}\n","import { step, success, info, exec, commandExists, warn } from \"./steps.js\";\n\n/**\n * Install Docker using the official script\n */\nexport function installDocker(): void {\n step(\"Install Docker\");\n\n if (commandExists(\"docker\")) {\n const version = exec(\"docker --version\", { silent: true });\n info(`Docker already installed: ${version?.trim() ?? \"unknown version\"}`);\n return;\n }\n\n // Install Docker using official script\n info(\"Downloading Docker install script...\");\n exec(\"curl -fsSL https://get.docker.com -o /tmp/get-docker.sh\");\n exec(\"sh /tmp/get-docker.sh\");\n exec(\"rm /tmp/get-docker.sh\");\n\n // Enable and start Docker\n exec(\"systemctl enable docker\");\n exec(\"systemctl start docker\");\n\n success(\"Docker installed and started\");\n\n // Add ddev user to docker group\n exec(\"usermod -aG docker ddev\");\n success(\"Added ddev user to docker group\");\n}\n\n/**\n * Configure Docker for production use\n */\nexport function configureDocker(): void {\n step(\"Configure Docker\");\n\n // Create daemon.json with production settings\n const daemonConfig = {\n \"log-driver\": \"json-file\",\n \"log-opts\": {\n \"max-size\": \"10m\",\n \"max-file\": \"3\",\n },\n \"storage-driver\": \"overlay2\",\n \"live-restore\": true,\n };\n\n exec(\"mkdir -p /etc/docker\");\n\n const configPath = \"/etc/docker/daemon.json\";\n const { writeFileSync, existsSync, readFileSync } = require(\"node:fs\");\n\n // Check if config already exists\n if (existsSync(configPath)) {\n try {\n const existing = JSON.parse(readFileSync(configPath, \"utf-8\"));\n // Merge with existing config\n const merged = { ...existing, ...daemonConfig };\n writeFileSync(configPath, JSON.stringify(merged, null, 2));\n info(\"Merged with existing Docker config\");\n } catch {\n warn(\"Could not parse existing Docker config, skipping\");\n return;\n }\n } else {\n writeFileSync(configPath, JSON.stringify(daemonConfig, null, 2));\n }\n\n // Reload Docker to apply config\n exec(\"systemctl reload docker || systemctl restart docker\");\n success(\"Docker configured with production settings\");\n}\n\n/**\n * Setup Docker system prune cron job\n */\nexport function setupDockerPrune(): void {\n step(\"Setup Docker cleanup cron\");\n\n const cronContent = `# Trafic: Clean up Docker resources weekly\n0 3 * * 0 root docker system prune -af --volumes 2>&1 | logger -t docker-prune\n`;\n\n const { writeFileSync } = require(\"node:fs\");\n writeFileSync(\"/etc/cron.d/trafic-docker-prune\", cronContent);\n exec(\"chmod 644 /etc/cron.d/trafic-docker-prune\");\n\n success(\"Docker prune scheduled weekly at 3am on Sundays\");\n}\n","import { step, success, info, exec, commandExists } from \"./steps.js\";\n\n/**\n * Install DDEV\n */\nexport function installDdev(): void {\n step(\"Install DDEV\");\n\n if (commandExists(\"ddev\")) {\n const version = exec(\"ddev --version 2>/dev/null | head -1\", { silent: true });\n info(`DDEV already installed: ${version?.trim() ?? \"unknown version\"}`);\n return;\n }\n\n // Install DDEV using the official install script\n info(\"Installing DDEV...\");\n exec(\n \"curl -fsSL https://ddev.com/install.sh | bash -s -- --version latest\",\n );\n\n success(\"DDEV installed\");\n}\n\n/**\n * Configure DDEV global settings for production\n *\n * Note on DNS and /etc/hosts:\n * When using a custom TLD (not ddev.site), DDEV may try to edit /etc/hosts\n * if it can't resolve project hostnames via DNS. To avoid this:\n * 1. Configure DNS so *.{tld} resolves to the server's IP (recommended)\n * 2. Or install dnsmasq for local wildcard DNS resolution\n *\n * With proper DNS, DDEV won't need sudo for hostname management.\n */\nexport function configureDdev(tld: string, email?: string): void {\n step(\"Configure DDEV\");\n\n // Run as ddev user\n const ddevCmd = (cmd: string) => `su - ddev -c '${cmd}'`;\n\n // Configure global settings\n // use_dns_when_possible=true (default) means DDEV uses DNS resolution\n // and only falls back to /etc/hosts if DNS fails\n exec(\n ddevCmd(\n `ddev config global --project-tld=${tld} --router-http-port=80 --router-https-port=443 --use-letsencrypt=${email ? \"true\" : \"false\"} ${email ? `--letsencrypt-email=${email}` : \"\"}`,\n ),\n );\n\n success(`DDEV configured with TLD: ${tld}`);\n info(`Ensure DNS is configured: *.${tld} → server IP`);\n\n if (email) {\n success(`Let's Encrypt enabled with email: ${email}`);\n }\n\n // Disable instrumentation (telemetry)\n exec(ddevCmd(\"ddev config global --instrumentation-opt-in=false\"));\n\n // Start the router\n exec(ddevCmd(\"ddev poweroff || true\"));\n exec(ddevCmd(\"ddev start\"));\n\n success(\"DDEV router started\");\n}\n\n/**\n * Install dnsmasq for local DNS resolution (optional)\n * This allows the server itself to resolve *.{tld} hostnames\n * without needing to edit /etc/hosts for each project\n */\nexport function installDnsmasq(tld: string): void {\n step(\"Install dnsmasq for local DNS\");\n\n exec(\"apt-get update && apt-get install -y dnsmasq\", { silent: true });\n\n // Get server's public IP\n const serverIp = exec(\n \"curl -4 -s ifconfig.me || hostname -I | awk '{print $1}'\",\n { silent: true },\n )?.trim() || \"127.0.0.1\";\n\n // Configure dnsmasq to resolve *.{tld} to server IP\n const { writeFileSync } = require(\"node:fs\");\n const dnsmasqConfig = `# Trafic: Local DNS for DDEV projects\n# Resolve all *.${tld} to this server\naddress=/${tld}/${serverIp}\n\n# Don't read /etc/resolv.conf\nno-resolv\n\n# Use upstream DNS for everything else\nserver=1.1.1.1\nserver=8.8.8.8\n\n# Listen on localhost only\nlisten-address=127.0.0.1\nbind-interfaces\n`;\n\n writeFileSync(\"/etc/dnsmasq.d/trafic.conf\", dnsmasqConfig);\n\n // Configure system to use local dnsmasq\n exec(\"systemctl restart dnsmasq\");\n exec(\"systemctl enable dnsmasq\");\n\n // Update resolv.conf to use local DNS first\n info(\"Configure /etc/resolv.conf to use 127.0.0.1 as primary DNS\");\n\n success(`dnsmasq configured: *.${tld} → ${serverIp}`);\n}\n\n/**\n * Configure Traefik for forward auth\n */\nexport function configureTraefik(): void {\n step(\"Configure Traefik for forward auth\");\n\n // Create custom Traefik config directory\n exec(\"mkdir -p /home/ddev/.ddev/traefik\");\n exec(\"chown -R ddev:ddev /home/ddev/.ddev\");\n\n // Static configuration for Trafic middleware\n const staticConfig = `# Trafic: Forward auth middleware\nhttp:\n middlewares:\n trafic-auth:\n forwardAuth:\n address: \"http://host.docker.internal:9876/__auth__\"\n authResponseHeaders:\n - \"X-Trafic-Project\"\n\n trafic-errors:\n errors:\n status:\n - \"502\"\n - \"503\"\n service: trafic-service\n query: \"/\"\n\n services:\n trafic-service:\n loadBalancer:\n servers:\n - url: \"http://host.docker.internal:9876\"\n`;\n\n const { writeFileSync } = require(\"node:fs\");\n writeFileSync(\"/home/ddev/.ddev/traefik/trafic.yaml\", staticConfig);\n exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/trafic.yaml\");\n\n // Create default router config that applies middleware to all projects\n const routerConfig = `# Trafic: Default router configuration\n# This file makes all DDEV projects use the Trafic auth middleware\nhttp:\n routers:\n trafic-catch-all:\n rule: \"HostRegexp(\\`.+\\`)\"\n priority: 1\n middlewares:\n - trafic-auth\n - trafic-errors\n service: api@internal\n`;\n\n writeFileSync(\"/home/ddev/.ddev/traefik/dynamic.yaml\", routerConfig);\n exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/dynamic.yaml\");\n\n success(\"Traefik configured with Trafic middleware\");\n}\n","import { step, success, info, exec, commandExists } from \"./steps.js\";\nimport { writeFileSync } from \"node:fs\";\n\n/**\n * Install Node.js using fnm\n */\nexport function installNode(): void {\n step(\"Install Node.js\");\n\n // Check if Node is already installed\n if (commandExists(\"node\")) {\n const version = exec(\"node --version\", { silent: true });\n info(`Node.js already installed: ${version?.trim()}`);\n return;\n }\n\n // Install fnm (Fast Node Manager)\n exec(\n 'curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-dir \"/opt/fnm\" --skip-shell',\n );\n\n // Add fnm to system path\n const profileContent = `# Trafic: fnm (Node.js version manager)\nexport FNM_DIR=\"/opt/fnm\"\nexport PATH=\"/opt/fnm:$PATH\"\neval \"$(fnm env)\"\n`;\n\n writeFileSync(\"/etc/profile.d/fnm.sh\", profileContent);\n exec(\"chmod 644 /etc/profile.d/fnm.sh\");\n\n // Install Node.js 24\n exec(\"export FNM_DIR=/opt/fnm && /opt/fnm/fnm install 24\");\n exec(\"export FNM_DIR=/opt/fnm && /opt/fnm/fnm default 24\");\n\n // Create symlinks for system-wide access\n exec(\n \"ln -sf /opt/fnm/aliases/default/bin/node /usr/local/bin/node || true\",\n );\n exec(\"ln -sf /opt/fnm/aliases/default/bin/npm /usr/local/bin/npm || true\");\n exec(\"ln -sf /opt/fnm/aliases/default/bin/npx /usr/local/bin/npx || true\");\n\n success(\"Node.js 24 installed via fnm\");\n}\n\n/**\n * Install the Trafic agent\n */\nexport function installAgent(): void {\n step(\"Install Trafic agent\");\n\n // Install the agent globally\n exec(\"npm install -g @studiometa/trafic-agent\");\n\n success(\"Trafic agent installed\");\n}\n\n/**\n * Create the agent configuration\n */\nexport function createAgentConfig(tld: string, _email?: string): void {\n step(\"Create agent configuration\");\n\n const config = `# Trafic agent configuration\n# Generated by trafic setup\n\n# Required: TLD for DDEV projects\ntld = \"${tld}\"\n\n# Agent HTTP server port\nport = 9876\n\n# Database and paths\ndb_path = \"/var/lib/trafic/db.sqlite\"\nproject_list_path = \"/home/ddev/.ddev/project_list.yaml\"\nprojects_dir = \"/home/ddev/www\"\n\n# Scale-to-zero settings\nidle_timeout = \"4h\"\nidle_check_interval = \"5m\"\n\n# Authentication\n[auth]\ndefault_policy = \"basic\"\nallowed_ips = []\ntokens = []\nbasic_auth = []\nrules = []\n`;\n\n writeFileSync(\"/etc/trafic/config.toml\", config);\n exec(\"chmod 640 /etc/trafic/config.toml\");\n exec(\"chown root:ddev /etc/trafic/config.toml\");\n\n success(\"Configuration created at /etc/trafic/config.toml\");\n info(\"Edit this file to configure authentication\");\n}\n\n/**\n * Create systemd service for the agent\n */\nexport function createSystemdService(): void {\n step(\"Create systemd service\");\n\n const serviceContent = `[Unit]\nDescription=Trafic Agent - DDEV preview environment manager\nAfter=network.target docker.service\nRequires=docker.service\n\n[Service]\nType=simple\nUser=ddev\nGroup=ddev\nExecStart=/usr/local/bin/npx trafic-agent start --config /etc/trafic/config.toml\nRestart=always\nRestartSec=5\nStandardOutput=journal\nStandardError=journal\nSyslogIdentifier=trafic-agent\n\n# Environment\nEnvironment=NODE_ENV=production\n\n# Security hardening\nNoNewPrivileges=true\nProtectSystem=strict\nProtectHome=read-only\nReadWritePaths=/var/lib/trafic /home/ddev/.ddev\nPrivateTmp=true\n\n[Install]\nWantedBy=multi-user.target\n`;\n\n writeFileSync(\"/etc/systemd/system/trafic-agent.service\", serviceContent);\n exec(\"chmod 644 /etc/systemd/system/trafic-agent.service\");\n\n // Reload systemd and enable service\n exec(\"systemctl daemon-reload\");\n exec(\"systemctl enable trafic-agent\");\n exec(\"systemctl start trafic-agent\");\n\n success(\"Trafic agent service started\");\n info(\"View logs: journalctl -u trafic-agent -f\");\n}\n","import { step, success, info, exec, warn, commandExists } from \"./steps.js\";\nimport { writeFileSync, existsSync } from \"node:fs\";\n\n/**\n * Configure SSH hardening\n */\nexport function hardenSsh(allowedUsers: string[] = [\"ddev\"]): void {\n step(\"Harden SSH configuration\");\n\n const sshConfig = `# Trafic SSH hardening\n# Generated by trafic setup\n\n# Disable root login\nPermitRootLogin no\n\n# Disable password authentication (keys only)\nPasswordAuthentication no\nPubkeyAuthentication yes\n\n# Limit authentication attempts\nMaxAuthTries 3\n\n# Idle timeout (5 minutes)\nClientAliveInterval 300\nClientAliveCountMax 2\n\n# Allowed users\nAllowUsers ${allowedUsers.join(\" \")}\n\n# Disable X11 forwarding\nX11Forwarding no\n\n# Disable TCP forwarding\nAllowTcpForwarding no\n\n# Use strong algorithms only\nKexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com\nMACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com\n`;\n\n writeFileSync(\"/etc/ssh/sshd_config.d/trafic.conf\", sshConfig);\n exec(\"chmod 644 /etc/ssh/sshd_config.d/trafic.conf\");\n\n // Test config before restarting\n const testResult = exec(\"sshd -t 2>&1 || echo 'error'\", { silent: true });\n if (testResult?.includes(\"error\")) {\n warn(\"SSH config test failed, reverting...\");\n exec(\"rm /etc/ssh/sshd_config.d/trafic.conf\");\n return;\n }\n\n exec(\"systemctl reload sshd || systemctl reload ssh\");\n success(\"SSH hardened: root login disabled, password auth disabled\");\n info(`Allowed users: ${allowedUsers.join(\", \")}`);\n}\n\n/**\n * Configure UFW firewall\n */\nexport function configureFirewall(): void {\n step(\"Configure firewall (UFW)\");\n\n // Install UFW if not present\n if (!commandExists(\"ufw\")) {\n exec(\"apt-get update && apt-get install -y ufw\", { silent: true });\n }\n\n // Reset and configure\n exec(\"ufw --force reset\", { silent: true });\n exec(\"ufw default deny incoming\");\n exec(\"ufw default allow outgoing\");\n exec(\"ufw allow 22/tcp comment 'SSH'\");\n exec(\"ufw allow 80/tcp comment 'HTTP'\");\n exec(\"ufw allow 443/tcp comment 'HTTPS'\");\n\n // Enable firewall\n exec(\"ufw --force enable\");\n\n success(\"Firewall enabled: SSH (22), HTTP (80), HTTPS (443) allowed\");\n}\n\n/**\n * Configure automatic security updates\n */\nexport function configureUnattendedUpgrades(): void {\n step(\"Configure automatic security updates\");\n\n // Install unattended-upgrades\n exec(\n \"DEBIAN_FRONTEND=noninteractive apt-get install -y unattended-upgrades apt-listchanges\",\n { silent: true },\n );\n\n // Configure for security updates only\n const upgradesConfig = `// Trafic: Automatic security updates\nUnattended-Upgrade::Allowed-Origins {\n \"\\${distro_id}:\\${distro_codename}\";\n \"\\${distro_id}:\\${distro_codename}-security\";\n \"\\${distro_id}ESMApps:\\${distro_codename}-apps-security\";\n \"\\${distro_id}ESM:\\${distro_codename}-infra-security\";\n};\n\n// Do not automatically reboot\nUnattended-Upgrade::Automatic-Reboot \"false\";\n\n// Remove unused dependencies\nUnattended-Upgrade::Remove-Unused-Dependencies \"true\";\n\n// Email notifications (optional)\n// Unattended-Upgrade::Mail \"root\";\n`;\n\n writeFileSync(\n \"/etc/apt/apt.conf.d/50unattended-upgrades\",\n upgradesConfig,\n );\n\n // Enable automatic updates\n const autoConfig = `APT::Periodic::Update-Package-Lists \"1\";\nAPT::Periodic::Download-Upgradeable-Packages \"1\";\nAPT::Periodic::AutocleanInterval \"7\";\nAPT::Periodic::Unattended-Upgrade \"1\";\n`;\n\n writeFileSync(\"/etc/apt/apt.conf.d/20auto-upgrades\", autoConfig);\n\n // Enable the service\n exec(\"systemctl enable unattended-upgrades\");\n exec(\"systemctl start unattended-upgrades\");\n\n success(\"Automatic security updates enabled\");\n}\n\n/**\n * Configure Fail2ban\n */\nexport function configureFail2ban(): void {\n step(\"Configure Fail2ban\");\n\n // Install fail2ban\n if (!commandExists(\"fail2ban-client\")) {\n exec(\"apt-get update && apt-get install -y fail2ban\", { silent: true });\n }\n\n // Configure jail\n const jailConfig = `# Trafic: Fail2ban configuration\n[DEFAULT]\nbantime = 3600\nfindtime = 600\nmaxretry = 5\nbackend = auto\n\n[sshd]\nenabled = true\nport = ssh\nfilter = sshd\nlogpath = /var/log/auth.log\nmaxretry = 3\nbantime = 7200\n`;\n\n writeFileSync(\"/etc/fail2ban/jail.local\", jailConfig);\n exec(\"chmod 644 /etc/fail2ban/jail.local\");\n\n // Restart fail2ban\n exec(\"systemctl enable fail2ban\");\n exec(\"systemctl restart fail2ban\");\n\n success(\"Fail2ban enabled: SSH brute force protection active\");\n}\n\n/**\n * Configure system limits\n */\nexport function configureSystemLimits(): void {\n step(\"Configure system limits\");\n\n const limitsConfig = `# Trafic: System limits for ddev user\nddev soft nofile 65536\nddev hard nofile 65536\nddev soft nproc 4096\nddev hard nproc 4096\n`;\n\n writeFileSync(\"/etc/security/limits.d/trafic.conf\", limitsConfig);\n exec(\"chmod 644 /etc/security/limits.d/trafic.conf\");\n\n success(\"System limits configured for ddev user\");\n}\n\n/**\n * Set proper file permissions\n */\nexport function configureFilePermissions(): void {\n step(\"Configure file permissions\");\n\n // Create directories\n exec(\"mkdir -p /etc/trafic\");\n exec(\"mkdir -p /var/lib/trafic\");\n exec(\"mkdir -p /var/backups/trafic\");\n\n // Set permissions\n exec(\"chmod 755 /etc/trafic\");\n exec(\"chmod 700 /var/lib/trafic\");\n exec(\"chmod 700 /var/backups/trafic\");\n exec(\"chown root:ddev /etc/trafic\");\n exec(\"chown ddev:ddev /var/lib/trafic\");\n exec(\"chown ddev:ddev /var/backups/trafic\");\n\n // Config file (if exists)\n if (existsSync(\"/etc/trafic/config.toml\")) {\n exec(\"chmod 640 /etc/trafic/config.toml\");\n exec(\"chown root:ddev /etc/trafic/config.toml\");\n }\n\n success(\"File permissions configured\");\n}\n\n/**\n * Run all hardening steps\n */\nexport function hardenServer(sshUsers: string[] = [\"ddev\"]): void {\n hardenSsh(sshUsers);\n configureFirewall();\n configureUnattendedUpgrades();\n configureFail2ban();\n configureSystemLimits();\n configureFilePermissions();\n}\n","import { execSync } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport type { AuditCheck } from \"./types.js\";\n\n/**\n * Run a command silently and return output\n */\nfunction execSilent(command: string): string {\n try {\n return execSync(command, { encoding: \"utf-8\", stdio: \"pipe\" }).trim();\n } catch {\n return \"\";\n }\n}\n\n/**\n * Check if a service is active\n */\nfunction isServiceActive(service: string): boolean {\n const status = execSilent(`systemctl is-active ${service}`);\n return status === \"active\";\n}\n\n/**\n * Audit SSH configuration\n */\nfunction auditSsh(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Check root login\n const sshdConfig = existsSync(\"/etc/ssh/sshd_config\")\n ? readFileSync(\"/etc/ssh/sshd_config\", \"utf-8\")\n : \"\";\n const traficConfig = existsSync(\"/etc/ssh/sshd_config.d/trafic.conf\")\n ? readFileSync(\"/etc/ssh/sshd_config.d/trafic.conf\", \"utf-8\")\n : \"\";\n\n const config = sshdConfig + \"\\n\" + traficConfig;\n\n const rootLoginDisabled =\n config.includes(\"PermitRootLogin no\") ||\n config.includes(\"PermitRootLogin prohibit-password\");\n checks.push({\n name: \"SSH root login\",\n status: rootLoginDisabled ? \"pass\" : \"fail\",\n message: rootLoginDisabled\n ? \"Root login disabled\"\n : \"Root login may be enabled\",\n fix: \"Add 'PermitRootLogin no' to /etc/ssh/sshd_config.d/trafic.conf\",\n });\n\n const passwordAuthDisabled = config.includes(\"PasswordAuthentication no\");\n checks.push({\n name: \"SSH password auth\",\n status: passwordAuthDisabled ? \"pass\" : \"warn\",\n message: passwordAuthDisabled\n ? \"Password authentication disabled\"\n : \"Password authentication may be enabled\",\n fix: \"Add 'PasswordAuthentication no' to /etc/ssh/sshd_config.d/trafic.conf\",\n });\n\n return checks;\n}\n\n/**\n * Audit firewall configuration\n */\nfunction auditFirewall(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n const ufwStatus = execSilent(\"ufw status\");\n const isActive = ufwStatus.includes(\"Status: active\");\n\n checks.push({\n name: \"Firewall (UFW)\",\n status: isActive ? \"pass\" : \"fail\",\n message: isActive ? \"UFW is active\" : \"UFW is not active\",\n fix: \"Run: ufw enable\",\n });\n\n if (isActive) {\n const allows22 = ufwStatus.includes(\"22/tcp\");\n const allows80 = ufwStatus.includes(\"80/tcp\");\n const allows443 = ufwStatus.includes(\"443/tcp\");\n\n if (allows22 && allows80 && allows443) {\n checks.push({\n name: \"Firewall ports\",\n status: \"pass\",\n message: \"Required ports (22, 80, 443) are open\",\n });\n } else {\n checks.push({\n name: \"Firewall ports\",\n status: \"warn\",\n message: \"Some required ports may be blocked\",\n fix: \"Run: ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp\",\n });\n }\n }\n\n return checks;\n}\n\n/**\n * Audit security services\n */\nfunction auditServices(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Fail2ban\n const fail2banActive = isServiceActive(\"fail2ban\");\n checks.push({\n name: \"Fail2ban\",\n status: fail2banActive ? \"pass\" : \"warn\",\n message: fail2banActive ? \"Fail2ban is active\" : \"Fail2ban is not running\",\n fix: \"Run: apt install fail2ban && systemctl enable fail2ban --now\",\n });\n\n // Unattended upgrades\n const unattendedActive = isServiceActive(\"unattended-upgrades\");\n checks.push({\n name: \"Automatic updates\",\n status: unattendedActive ? \"pass\" : \"warn\",\n message: unattendedActive\n ? \"Unattended upgrades enabled\"\n : \"Automatic updates not configured\",\n fix: \"Run: apt install unattended-upgrades && dpkg-reconfigure unattended-upgrades\",\n });\n\n // Trafic agent\n const agentActive = isServiceActive(\"trafic-agent\");\n checks.push({\n name: \"Trafic agent\",\n status: agentActive ? \"pass\" : \"fail\",\n message: agentActive\n ? \"Trafic agent is running\"\n : \"Trafic agent is not running\",\n fix: \"Run: systemctl start trafic-agent\",\n });\n\n return checks;\n}\n\n/**\n * Audit Docker\n */\nfunction auditDocker(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Docker running\n const dockerActive = isServiceActive(\"docker\");\n checks.push({\n name: \"Docker\",\n status: dockerActive ? \"pass\" : \"fail\",\n message: dockerActive ? \"Docker is running\" : \"Docker is not running\",\n fix: \"Run: systemctl start docker\",\n });\n\n // Dangling images\n const danglingImages = execSilent(\n \"docker images -f dangling=true -q | wc -l\",\n );\n const count = parseInt(danglingImages, 10) || 0;\n if (count > 0) {\n checks.push({\n name: \"Docker cleanup\",\n status: \"warn\",\n message: `${count} dangling Docker images`,\n fix: \"Run: docker system prune -f\",\n });\n }\n\n // Disk usage\n const diskUsage = execSilent(\"df -h / | awk 'NR==2{print $5}'\");\n const usagePercent = parseInt(diskUsage, 10) || 0;\n checks.push({\n name: \"Disk usage\",\n status: usagePercent > 90 ? \"fail\" : usagePercent > 80 ? \"warn\" : \"pass\",\n message: `Disk usage: ${diskUsage}`,\n fix: usagePercent > 80 ? \"Consider cleaning up old projects or Docker resources\" : undefined,\n });\n\n return checks;\n}\n\n/**\n * Audit file permissions\n */\nfunction auditPermissions(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Config file\n if (existsSync(\"/etc/trafic/config.toml\")) {\n const stats = execSilent(\"stat -c '%a' /etc/trafic/config.toml\");\n const isSecure = stats === \"640\" || stats === \"600\";\n checks.push({\n name: \"Config permissions\",\n status: isSecure ? \"pass\" : \"warn\",\n message: isSecure\n ? \"Config file has secure permissions\"\n : `Config file permissions: ${stats} (should be 640)`,\n fix: \"Run: chmod 640 /etc/trafic/config.toml\",\n });\n }\n\n // Projects directory\n if (existsSync(\"/home/ddev/www\")) {\n const owner = execSilent(\"stat -c '%U:%G' /home/ddev/www\");\n const isCorrect = owner === \"ddev:ddev\";\n checks.push({\n name: \"Projects directory\",\n status: isCorrect ? \"pass\" : \"warn\",\n message: isCorrect\n ? \"Projects directory owned by ddev\"\n : `Projects directory owner: ${owner}`,\n fix: \"Run: chown ddev:ddev /home/ddev/www\",\n });\n }\n\n return checks;\n}\n\n/**\n * Run all audit checks\n */\nexport function runAudit(): AuditCheck[] {\n return [\n ...auditSsh(),\n ...auditFirewall(),\n ...auditServices(),\n ...auditDocker(),\n ...auditPermissions(),\n ];\n}\n\n/**\n * Print audit results\n */\nexport function printAuditResults(checks: AuditCheck[]): void {\n console.log(\"\\n\\x1b[1mSecurity Audit Results\\x1b[0m\\n\");\n\n for (const check of checks) {\n const icon =\n check.status === \"pass\"\n ? \"\\x1b[32m✓\\x1b[0m\"\n : check.status === \"warn\"\n ? \"\\x1b[33m⚠\\x1b[0m\"\n : \"\\x1b[31m✗\\x1b[0m\";\n\n console.log(`${icon} \\x1b[1m${check.name}\\x1b[0m: ${check.message}`);\n\n if (check.fix && check.status !== \"pass\") {\n console.log(` \\x1b[90m${check.fix}\\x1b[0m`);\n }\n }\n\n const passed = checks.filter((c) => c.status === \"pass\").length;\n const warnings = checks.filter((c) => c.status === \"warn\").length;\n const failed = checks.filter((c) => c.status === \"fail\").length;\n\n console.log(\n `\\n\\x1b[1mSummary:\\x1b[0m ${passed} passed, ${warnings} warnings, ${failed} failed`,\n );\n}\n","import type { SetupOptions } from \"./types.js\";\nimport { step, info, error, setDryRun, resetSteps, isRoot } from \"./steps.js\";\nimport { createDdevUser } from \"./user.js\";\nimport { installDocker, configureDocker, setupDockerPrune } from \"./docker.js\";\nimport { installDdev, configureDdev, configureTraefik } from \"./ddev.js\";\nimport { installNode, installAgent, createAgentConfig, createSystemdService } from \"./agent.js\";\nimport { hardenServer } from \"./hardening.js\";\nimport { runAudit, printAuditResults } from \"./audit.js\";\n\nexport * from \"./types.js\";\nexport * from \"./audit.js\";\n\n/**\n * Run the full server setup\n */\nexport async function setup(options: SetupOptions): Promise<void> {\n resetSteps();\n setDryRun(options.dryRun ?? false);\n\n console.log(\"\\n\\x1b[1m🚦 Trafic Server Setup\\x1b[0m\\n\");\n\n if (options.dryRun) {\n console.log(\"\\x1b[33m⚠ Dry-run mode: no changes will be made\\x1b[0m\\n\");\n }\n\n // Check root\n if (!isRoot() && !options.dryRun) {\n error(\"This command must be run as root\");\n console.log(\" Run: sudo trafic-agent setup ...\");\n process.exit(1);\n }\n\n info(`TLD: ${options.tld}`);\n if (options.email) {\n info(`Let's Encrypt email: ${options.email}`);\n }\n if (options.noHardening) {\n info(\"Hardening: disabled\");\n }\n\n try {\n // Step 1: Create ddev user\n createDdevUser();\n\n // Step 2: Install Docker\n if (!options.noDocker) {\n installDocker();\n configureDocker();\n setupDockerPrune();\n } else {\n step(\"Install Docker\");\n info(\"Skipped (--no-docker)\");\n }\n\n // Step 3: Install DDEV\n if (!options.noDdev) {\n installDdev();\n configureDdev(options.tld, options.email);\n configureTraefik();\n } else {\n step(\"Install DDEV\");\n info(\"Skipped (--no-ddev)\");\n }\n\n // Step 4: Install Node.js\n installNode();\n\n // Step 5: Install and configure agent\n installAgent();\n createAgentConfig(options.tld, options.email);\n createSystemdService();\n\n // Step 6: Server hardening\n if (!options.noHardening) {\n const sshUsers = options.sshUsers ?? [\"ddev\"];\n hardenServer(sshUsers);\n } else {\n step(\"Server hardening\");\n info(\"Skipped (--no-hardening)\");\n }\n\n // Done\n console.log(\"\\n\\x1b[32m✓ Setup complete!\\x1b[0m\\n\");\n console.log(\"Next steps:\");\n console.log(` 1. Edit /etc/trafic/config.toml to configure authentication`);\n console.log(` 2. Add SSH public keys to /home/ddev/.ssh/authorized_keys`);\n console.log(` 3. Deploy your first project:`);\n console.log(` npx @studiometa/trafic-cli deploy --host=<this-server> --name=my-app`);\n console.log(`\\nYour projects will be available at https://<name>.${options.tld}`);\n } catch (err) {\n error(`Setup failed: ${err instanceof Error ? err.message : err}`);\n process.exit(1);\n }\n}\n\n/**\n * Run security audit\n */\nexport async function audit(): Promise<void> {\n const checks = runAudit();\n printAuditResults(checks);\n\n const failed = checks.filter((c) => c.status === \"fail\").length;\n if (failed > 0) {\n process.exit(1);\n }\n}\n"],"names":["info","success","execSilent","writeFileSync","existsSync","readFileSync"],"mappings":"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"}
|