-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsetup.sh
More file actions
executable file
·479 lines (401 loc) · 17.9 KB
/
setup.sh
File metadata and controls
executable file
·479 lines (401 loc) · 17.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
#!/usr/bin/env bash
# =============================================================================
# ServerCommander OS — Interactive Setup Script
# https://github.com/your-org/servercommander-os
# =============================================================================
set -euo pipefail
# ── Colors ────────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
info() { echo -e "${CYAN}[INFO]${RESET} $*"; }
success() { echo -e "${GREEN}[OK]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
fatal() { error "$*"; exit 1; }
header() { echo -e "\n${BOLD}${BLUE}▶ $*${RESET}"; }
# ── Banner ────────────────────────────────────────────────────────────────────
echo -e "${BOLD}"
cat <<'BANNER'
___ ___ _
/ __| ___ _ ___ _____ _ _ ___ / __|___ _ __ _ __ __ _ _ _ __| |___ _ _
\__ \/ -_) '_\ V / -_) '_(_-< | (__/ _ \ ' \| ' \/ _` | ' \/ _` / -_) '_|
|___/\___|_| \_/\___|_| /__/ \___\___/_|_|_|_|_|_\__,_|_||_\__,_\___|_|
Open-Source Server Management Console
BANNER
echo -e "${RESET}"
# ── Prerequisites Check ───────────────────────────────────────────────────────
header "Checking prerequisites"
require_cmd() {
if ! command -v "$1" &>/dev/null; then
fatal "'$1' is not installed. Please install it and re-run setup."
fi
success "$1 found ($(command -v "$1"))"
}
require_cmd docker
require_cmd node
if docker compose version &>/dev/null; then
COMPOSE_CMD=(docker compose)
COMPOSE_LABEL="docker compose"
success "Docker Compose plugin found (docker compose)"
elif command -v docker-compose &>/dev/null; then
COMPOSE_CMD=(docker-compose)
COMPOSE_LABEL="docker-compose"
success "docker-compose found ($(command -v docker-compose))"
else
fatal "Neither 'docker compose' plugin nor 'docker-compose' is installed. Please install Docker Compose and re-run setup."
fi
# Check Docker daemon is running
if ! docker info &>/dev/null; then
fatal "Docker daemon is not running. Start it and re-run setup."
fi
success "Docker daemon is running"
# ── Collect Admin Credentials ─────────────────────────────────────────────────
header "Admin Account Setup"
echo -e "Create the initial administrator account.\n"
read -rp " Admin username [admin]: " ADMIN_USERNAME
ADMIN_USERNAME="${ADMIN_USERNAME:-admin}"
while true; do
read -rsp " Admin password (min 12 chars): " ADMIN_PASSWORD
echo
if [[ ${#ADMIN_PASSWORD} -lt 12 ]]; then
warn "Password must be at least 12 characters. Try again."
continue
fi
read -rsp " Confirm password: " ADMIN_PASSWORD_CONFIRM
echo
if [[ "$ADMIN_PASSWORD" != "$ADMIN_PASSWORD_CONFIRM" ]]; then
warn "Passwords do not match. Try again."
continue
fi
break
done
success "Admin credentials accepted."
# ── Port Configuration ────────────────────────────────────────────────────────
header "Network Configuration"
read -rp " Host port to expose ServerCommander on [3000]: " APP_PORT
APP_PORT="${APP_PORT:-3000}"
# Basic port validity check
if ! [[ "$APP_PORT" =~ ^[0-9]+$ ]] || (( APP_PORT < 1 || APP_PORT > 65535 )); then
fatal "Invalid port: $APP_PORT"
fi
success "Application port: $APP_PORT"
# ── Trusted Proxy Configuration ───────────────────────────────────────────────
TRUSTED_PROXIES_CONFIGURED=""
detect_trusted_proxy() {
# Check for nginx (process or systemd service)
if command -v systemctl &>/dev/null && systemctl is-active --quiet nginx 2>/dev/null; then
echo "nginx (systemd)"
return 0
fi
if pgrep -x nginx &>/dev/null 2>&1; then
echo "nginx (process)"
return 0
fi
# Check for Caddy
if command -v systemctl &>/dev/null && systemctl is-active --quiet caddy 2>/dev/null; then
echo "caddy"
return 0
fi
if pgrep -x caddy &>/dev/null 2>&1; then
echo "caddy (process)"
return 0
fi
echo ""
}
echo ""
echo " Trusted Proxy: If ServerCommander runs behind a reverse proxy (nginx,"
echo " Caddy, Traefik, …), enter its IP so that X-Forwarded-For headers are"
echo " trusted for rate-limiting and audit logs."
echo " Options:"
echo " detect — auto-detect a local reverse proxy (nginx/Caddy)"
echo " <ip> — enter proxy IP manually, e.g. 127.0.0.1 or 192.168.1.1"
echo " (empty) — no trusted proxy / direct access"
echo ""
read -rp " Trusted proxy IP or 'detect' []: " TRUSTED_PROXY_INPUT
TRUSTED_PROXY_INPUT="${TRUSTED_PROXY_INPUT:-}"
if [[ "${TRUSTED_PROXY_INPUT,,}" == "detect" ]]; then
DETECTED_PROXY=$(detect_trusted_proxy)
if [[ -n "$DETECTED_PROXY" ]]; then
TRUSTED_PROXIES_CONFIGURED="127.0.0.1"
success "Detected $DETECTED_PROXY — using 127.0.0.1 as trusted proxy"
else
warn "No known reverse proxy detected on this host. TRUSTED_PROXIES left empty."
TRUSTED_PROXIES_CONFIGURED=""
fi
elif [[ -n "$TRUSTED_PROXY_INPUT" ]]; then
# Basic IP validation (single IP or CIDR)
if [[ "$TRUSTED_PROXY_INPUT" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}(/[0-9]{1,2})?$ ]] || \
[[ "$TRUSTED_PROXY_INPUT" =~ ^::1$ ]]; then
TRUSTED_PROXIES_CONFIGURED="$TRUSTED_PROXY_INPUT"
success "Trusted proxy set to: $TRUSTED_PROXIES_CONFIGURED"
else
warn "Input '$TRUSTED_PROXY_INPUT' does not look like a valid IP/CIDR — TRUSTED_PROXIES left empty."
TRUSTED_PROXIES_CONFIGURED=""
fi
else
TRUSTED_PROXIES_CONFIGURED=""
success "No trusted proxy configured (direct access mode)"
fi
# ── SSH/SFTP Backend Configuration ───────────────────────────────────────────
header "Remote Access Backend"
read -rp " Use SSH/SFTP for Terminal + Files? (y/N): " SSH_ENABLE_INPUT
SSH_ENABLE_INPUT="${SSH_ENABLE_INPUT:-N}"
SSH_ENABLED=false
SSH_HOST=""
SSH_PORT="22"
SSH_USERNAME=""
SSH_PASSWORD=""
SSH_PRIVATE_KEY=""
SSH_KEY_PASSPHRASE=""
SSH_SFTP_ROOT="/"
if [[ "$SSH_ENABLE_INPUT" =~ ^[Yy]$ ]]; then
SSH_ENABLED=true
read -rp " SSH host/IP: " SSH_HOST
[[ -z "$SSH_HOST" ]] && fatal "SSH host is required when SSH/SFTP is enabled"
read -rp " SSH port [22]: " SSH_PORT
SSH_PORT="${SSH_PORT:-22}"
if ! [[ "$SSH_PORT" =~ ^[0-9]+$ ]] || (( SSH_PORT < 1 || SSH_PORT > 65535 )); then
fatal "Invalid SSH port: $SSH_PORT"
fi
read -rp " SSH username: " SSH_USERNAME
[[ -z "$SSH_USERNAME" ]] && fatal "SSH username is required when SSH/SFTP is enabled"
read -rp " SSH authentication method ([k]ey/[p]assword) [k]: " SSH_AUTH_METHOD
SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-k}"
if [[ "$SSH_AUTH_METHOD" =~ ^[Pp]$ ]]; then
while true; do
read -rsp " SSH password: " SSH_PASSWORD
echo
if [[ -z "$SSH_PASSWORD" ]]; then
warn "SSH password cannot be empty. Try again."
continue
fi
break
done
else
read -rp " Path to private key [~/.ssh/id_ed25519]: " SSH_KEY_PATH
SSH_KEY_PATH="${SSH_KEY_PATH:-~/.ssh/id_ed25519}"
SSH_KEY_PATH="${SSH_KEY_PATH/#\~/$HOME}"
[[ -f "$SSH_KEY_PATH" ]] || fatal "SSH private key not found: $SSH_KEY_PATH"
SSH_PRIVATE_KEY="$(cat "$SSH_KEY_PATH")"
read -rsp " Key passphrase (optional): " SSH_KEY_PASSPHRASE
echo
fi
read -rp " SFTP root path [/]: " SSH_SFTP_ROOT
SSH_SFTP_ROOT="${SSH_SFTP_ROOT:-/}"
success "SSH/SFTP backend enabled (${SSH_USERNAME}@${SSH_HOST}:${SSH_PORT})"
else
success "Using local host-mount backend for Terminal + Files"
fi
# ── SMTP / Mail Configuration ────────────────────────────────────────────────
header "SMTP / Mail Configuration"
read -rp " Enable SMTP (mail sending)? (y/N): " SMTP_ENABLE_INPUT
SMTP_ENABLE_INPUT="${SMTP_ENABLE_INPUT:-N}"
SMTP_ENABLED=false
SMTP_HOST=""
SMTP_PORT="587"
SMTP_SECURE=false
SMTP_USERNAME=""
SMTP_PASSWORD=""
SMTP_FROM_EMAIL=""
SMTP_USE_ALIAS=false
SMTP_FROM_NAME=""
if [[ "$SMTP_ENABLE_INPUT" =~ ^[Yy]$ ]]; then
SMTP_ENABLED=true
read -rp " SMTP host: " SMTP_HOST
[[ -z "$SMTP_HOST" ]] && fatal "SMTP host is required when SMTP is enabled"
read -rp " SMTP port [587]: " SMTP_PORT
SMTP_PORT="${SMTP_PORT:-587}"
if ! [[ "$SMTP_PORT" =~ ^[0-9]+$ ]] || (( SMTP_PORT < 1 || SMTP_PORT > 65535 )); then
fatal "Invalid SMTP port: $SMTP_PORT"
fi
read -rp " Use encrypted SMTP transport? (Port 465 = SSL/TLS, Port 587 = STARTTLS) (y/N): " SMTP_SECURE_INPUT
SMTP_SECURE_INPUT="${SMTP_SECURE_INPUT:-N}"
if [[ "$SMTP_SECURE_INPUT" =~ ^[Yy]$ ]]; then
SMTP_SECURE=true
fi
read -rp " SMTP username: " SMTP_USERNAME
[[ -z "$SMTP_USERNAME" ]] && fatal "SMTP username is required when SMTP is enabled"
while true; do
read -rsp " SMTP password: " SMTP_PASSWORD
echo
if [[ -z "$SMTP_PASSWORD" ]]; then
warn "SMTP password cannot be empty. Try again."
continue
fi
break
done
read -rp " Mail from address (e.g. noreply@example.com): " SMTP_FROM_EMAIL
[[ -z "$SMTP_FROM_EMAIL" ]] && fatal "Mail from address is required when SMTP is enabled"
read -rp " Use alias sender name? (y/N): " SMTP_ALIAS_INPUT
SMTP_ALIAS_INPUT="${SMTP_ALIAS_INPUT:-N}"
if [[ "$SMTP_ALIAS_INPUT" =~ ^[Yy]$ ]]; then
SMTP_USE_ALIAS=true
read -rp " Alias sender name (e.g. ServerCommander Security): " SMTP_FROM_NAME
[[ -z "$SMTP_FROM_NAME" ]] && fatal "Alias name is required when alias sender is enabled"
fi
success "SMTP enabled (${SMTP_HOST}:${SMTP_PORT})"
else
success "SMTP disabled"
fi
# ── Secure Secret Generation ──────────────────────────────────────────────────
header "Generating cryptographic secrets"
gen_hex() { head -c "$1" /dev/urandom | xxd -p | tr -d '\n' | cut -c1-"$((2 * $1))"; }
SESSION_SECRET="$(gen_hex 32)"
JWT_SECRET="$(gen_hex 32)"
INTERNAL_RPC_SECRET="$(gen_hex 32)"
ENCRYPTION_KEY="$(gen_hex 32)"
success "SESSION_SECRET generated (64-char hex)"
success "JWT_SECRET generated (64-char hex)"
success "INTERNAL_RPC_SECRET generated (64-char hex)"
success "ENCRYPTION_KEY generated (64-char hex)"
# ── Write .env File ───────────────────────────────────────────────────────────
header "Writing .env"
ENV_FILE=".env"
if [[ -f "$ENV_FILE" ]]; then
BACKUP=".env.backup.$(date +%Y%m%d%H%M%S)"
cp "$ENV_FILE" "$BACKUP"
warn "Existing .env backed up to $BACKUP"
fi
escape_env() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
encrypt_secret_env() {
local plaintext="$1"
PLAINTEXT="$plaintext" ENCRYPTION_KEY="$ENCRYPTION_KEY" node <<'NODE'
const { createCipheriv, randomBytes } = require('crypto');
const keyHex = (process.env.ENCRYPTION_KEY || '').trim();
if (!/^[0-9a-fA-F]{64}$/.test(keyHex)) {
process.stderr.write('ENCRYPTION_KEY must be exactly 64 hex characters\n');
process.exit(1);
}
const plaintext = process.env.PLAINTEXT || '';
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', Buffer.from(keyHex, 'hex'), iv);
const encrypted = Buffer.concat([cipher.update(Buffer.from(plaintext, 'utf8')), cipher.final()]);
const tag = cipher.getAuthTag();
process.stdout.write(`${iv.toString('hex')}:${encrypted.toString('hex')}:${tag.toString('hex')}`);
NODE
}
ADMIN_USERNAME_ESCAPED="$(escape_env "$ADMIN_USERNAME")"
SSH_HOST_ESCAPED="$(escape_env "$SSH_HOST")"
SSH_USERNAME_ESCAPED="$(escape_env "$SSH_USERNAME")"
SSH_SFTP_ROOT_ESCAPED="$(escape_env "$SSH_SFTP_ROOT")"
SMTP_HOST_ESCAPED="$(escape_env "$SMTP_HOST")"
SMTP_USERNAME_ESCAPED="$(escape_env "$SMTP_USERNAME")"
SMTP_FROM_EMAIL_ESCAPED="$(escape_env "$SMTP_FROM_EMAIL")"
SMTP_FROM_NAME_ESCAPED="$(escape_env "$SMTP_FROM_NAME")"
ADMIN_PASSWORD_ENC="$(encrypt_secret_env "$ADMIN_PASSWORD")"
ADMIN_PASSWORD_ENC_ESCAPED="$(escape_env "$ADMIN_PASSWORD_ENC")"
# Auto-enable COOKIE_SECURE when behind a reverse proxy (implies HTTPS)
if [[ -n "$TRUSTED_PROXIES_CONFIGURED" ]]; then
COOKIE_SECURE_VALUE=true
else
COOKIE_SECURE_VALUE=false
fi
SSH_PASSWORD_ENC=""
SSH_PRIVATE_KEY_ENC=""
SSH_KEY_PASSPHRASE_ENC=""
SMTP_PASSWORD_ENC=""
if [[ "$SSH_ENABLED" == "true" ]]; then
if [[ -n "$SSH_PASSWORD" ]]; then
SSH_PASSWORD_ENC="$(encrypt_secret_env "$SSH_PASSWORD")"
fi
if [[ -n "$SSH_PRIVATE_KEY" ]]; then
SSH_PRIVATE_KEY_ENC="$(encrypt_secret_env "$SSH_PRIVATE_KEY")"
fi
if [[ -n "$SSH_KEY_PASSPHRASE" ]]; then
SSH_KEY_PASSPHRASE_ENC="$(encrypt_secret_env "$SSH_KEY_PASSPHRASE")"
fi
fi
if [[ "$SMTP_ENABLED" == "true" ]]; then
SMTP_PASSWORD_ENC="$(encrypt_secret_env "$SMTP_PASSWORD")"
fi
SSH_PASSWORD_ENC_ESCAPED="$(escape_env "$SSH_PASSWORD_ENC")"
SSH_PRIVATE_KEY_ENC_ESCAPED="$(escape_env "$SSH_PRIVATE_KEY_ENC")"
SSH_KEY_PASSPHRASE_ENC_ESCAPED="$(escape_env "$SSH_KEY_PASSPHRASE_ENC")"
SMTP_PASSWORD_ENC_ESCAPED="$(escape_env "$SMTP_PASSWORD_ENC")"
cat > "$ENV_FILE" <<EOF
# ─────────────────────────────────────────────────────────────────────────────
# ServerCommander OS — Environment (generated by setup.sh on $(date))
# DO NOT COMMIT THIS FILE
# ─────────────────────────────────────────────────────────────────────────────
NODE_ENV=production
NEXT_PUBLIC_APP_NAME="ServerCommander OS"
NEXT_PUBLIC_APP_VERSION="1.0.0"
PORT=3000
HOST_PORT=${APP_PORT}
SESSION_SECRET=${SESSION_SECRET}
JWT_SECRET=${JWT_SECRET}
INTERNAL_RPC_SECRET=${INTERNAL_RPC_SECRET}
ENCRYPTION_KEY=${ENCRYPTION_KEY}
DATABASE_URL="file:/app/data/servercommander.db"
ADMIN_USERNAME="${ADMIN_USERNAME_ESCAPED}"
ADMIN_PASSWORD_ENC="${ADMIN_PASSWORD_ENC_ESCAPED}"
DOCKER_HOST=tcp://docker-socket-proxy:2375
HOST_FS_MOUNT=/host_system
HOST_FS_SOURCE=/srv/servercommander
TRUSTED_PROXIES=${TRUSTED_PROXIES_CONFIGURED}
SSH_ENABLED=${SSH_ENABLED}
SSH_HOST="${SSH_HOST_ESCAPED}"
SSH_PORT=${SSH_PORT}
SSH_USERNAME="${SSH_USERNAME_ESCAPED}"
SSH_PASSWORD_ENC="${SSH_PASSWORD_ENC_ESCAPED}"
SSH_PRIVATE_KEY_ENC="${SSH_PRIVATE_KEY_ENC_ESCAPED}"
SSH_KEY_PASSPHRASE_ENC="${SSH_KEY_PASSPHRASE_ENC_ESCAPED}"
SSH_HOST_KEY_SHA256=""
SSH_SFTP_ROOT="${SSH_SFTP_ROOT_ESCAPED}"
SMTP_ENABLED=${SMTP_ENABLED}
SMTP_HOST="${SMTP_HOST_ESCAPED}"
SMTP_PORT=${SMTP_PORT}
SMTP_SECURE=${SMTP_SECURE}
SMTP_USERNAME="${SMTP_USERNAME_ESCAPED}"
SMTP_PASSWORD_ENC="${SMTP_PASSWORD_ENC_ESCAPED}"
SMTP_FROM_EMAIL="${SMTP_FROM_EMAIL_ESCAPED}"
SMTP_USE_ALIAS=${SMTP_USE_ALIAS}
SMTP_FROM_NAME="${SMTP_FROM_NAME_ESCAPED}"
SESSION_MAX_AGE=28800
COOKIE_SECURE=${COOKIE_SECURE_VALUE}
EOF
chmod 600 "$ENV_FILE"
success ".env written with restricted permissions (600)"
# ── Build & Launch ────────────────────────────────────────────────────────────
header "Building Docker image (this may take a few minutes on first run)"
"${COMPOSE_CMD[@]}" build --no-cache
success "Docker image built."
header "Starting ServerCommander OS"
"${COMPOSE_CMD[@]}" up -d
# ── Wait for health ───────────────────────────────────────────────────────────
header "Waiting for application to become healthy"
MAX_WAIT=120
WAITED=0
HEALTH_URL="http://localhost:${APP_PORT}/login"
HEALTH_OK=false
until curl -sf "$HEALTH_URL" > /dev/null 2>&1; do
WAITED=$((WAITED + 3))
if (( WAITED >= MAX_WAIT )); then
warn "Health check timed out after ${MAX_WAIT}s on ${HEALTH_URL}. Check logs: ${COMPOSE_LABEL} logs -f"
break
fi
echo -n "."
sleep 3
done
if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then
HEALTH_OK=true
fi
echo
if [[ "$HEALTH_OK" == "true" ]]; then
success "Application is healthy!"
else
warn "Application did not become healthy during setup wait window."
fi
# ── Done ──────────────────────────────────────────────────────────────────────
echo -e "\n${BOLD}${GREEN}╔══════════════════════════════════════════════════════════════╗${RESET}"
echo -e "${BOLD}${GREEN}║ ServerCommander OS is running! ║${RESET}"
echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════════════╝${RESET}\n"
echo -e " ${BOLD}URL:${RESET} http://$(hostname -I | awk '{print $1}'):${APP_PORT}"
echo -e " ${BOLD}Username:${RESET} ${ADMIN_USERNAME}"
echo -e " ${BOLD}Password:${RESET} (as entered)\n"
echo -e " Manage: ${CYAN}${COMPOSE_LABEL} logs -f${RESET} — view logs"
echo -e " ${CYAN}${COMPOSE_LABEL} down${RESET} — stop"
echo -e " ${CYAN}${COMPOSE_LABEL} restart${RESET} — restart\n"
warn "Keep the .env file secure and never share your secrets."