#!/bin/bash -eu # ============================================================================= # 01-migrate-user.sh — Run as Linux user (not root) # Installs operator-docker and prepares for peer migration # Auto-rollback: restores nginx, certs and legacy stack on failure # ============================================================================= if [ "$EUID" -eq 0 ]; then echo "[X] Run as your Linux user, not root" exit 1 fi RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RESET='\033[0m' BACKUP_DIR="$HOME/.wg-migration" LOG="$BACKUP_DIR/migration.log" REPO_URL="git@git.simplifiedprivacy.com:zenaku/operator-docker.git" OPERATOR_DIR="$HOME/operator-docker" ok() { echo -e "${GREEN}[OK]${RESET} $1"; echo "[OK] $1" >> "$LOG"; } info() { echo -e "${CYAN}[i]${RESET} $1"; echo "[i] $1" >> "$LOG"; } warn() { echo -e "${YELLOW}[!]${RESET} $1"; echo "[!] $1" >> "$LOG"; } fail() { echo -e "${RED}[X]${RESET} $1"; echo "[X] $1" >> "$LOG"; exit 1; } sep() { echo -e "\n${CYAN}------------------------------------${RESET}"; } rollback_01() { echo "" warn "Installation failed — starting rollback..." echo "[ROLLBACK-01] $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG" [ -d "$OPERATOR_DIR" ] && rm -rf "$OPERATOR_DIR" && warn "operator-docker removed." if [ -d "$BACKUP_DIR/nginx/sites-available" ]; then sudo cp -r "$BACKUP_DIR/nginx/sites-available/." /etc/nginx/sites-available/ 2>/dev/null || true sudo cp -r "$BACKUP_DIR/nginx/sites-enabled/." /etc/nginx/sites-enabled/ 2>/dev/null || true warn "Nginx restored." fi if [ -d "$BACKUP_DIR/letsencrypt" ]; then sudo cp -r "$BACKUP_DIR/letsencrypt/." /etc/letsencrypt/ 2>/dev/null || true warn "Certificates restored." fi LEGACY_COMPOSE_DIR=$(cat "$BACKUP_DIR/legacy_compose_dir" 2>/dev/null || true) if [ -n "$LEGACY_COMPOSE_DIR" ] && [ -d "$LEGACY_COMPOSE_DIR" ]; then sudo modprobe ip6table_filter 2>/dev/null || true cd "$LEGACY_COMPOSE_DIR" docker compose up -d 2>/dev/null || true sleep 5 LEGACY_UP=$(docker ps --format '{{.Names}}' | grep -i wireguard | head -1 || true) [ -n "$LEGACY_UP" ] && ok "Legacy restored: $LEGACY_UP" || \ warn "Legacy failed to start. Run manually: cd $LEGACY_COMPOSE_DIR && docker compose up -d" fi sudo systemctl start nginx 2>/dev/null || true warn "Rollback complete. Check: cat $LOG" } trap rollback_01 ERR echo "" echo "=========================================" echo " 01 - operator-docker installation" echo " $(date '+%Y-%m-%d %H:%M:%S')" echo "=========================================" echo "[START-01] $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG" # ─── PREREQUISITES ──────────────────────────────────────────────────────────── sep info "Checking prerequisites..." [ -f "$BACKUP_DIR/private_key" ] || fail "No backup found. Run: sudo bash 00-migrate-root.sh" [ -f "$BACKUP_DIR/wg0-legacy.conf" ] || fail "No peer backup found. Run: sudo bash 00-migrate-root.sh" [ -f "$BACKUP_DIR/legacy_compose_dir" ] || fail "No legacy snapshot found. Run: sudo bash 00-migrate-root.sh" ok "Backups found." # ─── CLEAN FAILED INSTALL ───────────────────────────────────────────────────── sep info "Checking for previous installation..." _env_is_corrupt() { local env_file="$1" [ ! -f "$env_file" ] && return 1 local base_domain base_domain=$(grep -E '^BASE_DOMAIN=' "$env_file" 2>/dev/null \ | head -1 | cut -d= -f2- | tr -d '[:space:]' || true) [ -z "$base_domain" ] && { warn ".env: BASE_DOMAIN is empty."; return 0; } echo "$base_domain" | grep -qE '[/;|&<> ]' && { warn ".env: BASE_DOMAIN contains invalid characters: '$base_domain'"; return 0; } echo "$base_domain" | grep -qE '^[0-9]+-|^ash' && { warn ".env: BASE_DOMAIN looks like a script name: '$base_domain'"; return 0; } [ "${#base_domain}" -gt 253 ] && { warn ".env: BASE_DOMAIN too long (${#base_domain} chars)."; return 0; } return 1 } _purge_failed_install() { warn "Cleaning up failed installation..." echo "[PURGE] $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG" if [ -f "$OPERATOR_DIR/docker-compose.yml" ]; then cd "$OPERATOR_DIR" docker compose down --remove-orphans 2>/dev/null && warn "Previous stack stopped." || true cd "$HOME" fi local project project=$(basename "$OPERATOR_DIR" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-') while IFS= read -r cname; do docker rm -f "$cname" 2>/dev/null && warn "Container removed: $cname" || true done < <(docker ps -a --format '{{.Names}}' | grep -i "^${project}" 2>/dev/null || true) rm -rf "$OPERATOR_DIR" ok "Directory $OPERATOR_DIR removed." } if [ -d "$OPERATOR_DIR" ]; then if _env_is_corrupt "$OPERATOR_DIR/.env"; then warn "Corrupt .env detected from previous attempt." _purge_failed_install elif [ ! -f "$OPERATOR_DIR/docker-compose.yml" ]; then warn "Incomplete repo (missing docker-compose.yml). Cleaning..." _purge_failed_install else warn "Existing directory found. Removing for fresh install..." _purge_failed_install fi else info "No previous installation found." fi # ─── SSH KEY ────────────────────────────────────────────────────────────────── sep info "Checking SSH key..." [ -f "$HOME/.ssh/id_ed25519" ] || fail "SSH key not found. Run: sudo bash 00-migrate-root.sh" ok "SSH key found." info "Checking access to private repo..." SSH_TEST=$(GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10' \ ssh -T git@git.simplifiedprivacy.com 2>&1 || true) if echo "$SSH_TEST" | grep -qiE "Welcome|successfully authenticated"; then ok "Repo access authorized." else echo "" warn "Could not authenticate with private repo." echo "" echo " Send this SSH key to Simplified Privacy:" echo "" cat "$HOME/.ssh/id_ed25519.pub" echo "" fail "Once authorized, run again: bash 01-migrate-user.sh" fi # ─── CLONE REPO ─────────────────────────────────────────────────────────────── sep info "Cloning operator-docker repository..." GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' \ git clone "$REPO_URL" "$OPERATOR_DIR" || fail "Could not clone repository." ok "Repository cloned to $OPERATOR_DIR" [ -f "$OPERATOR_DIR/install.sh" ] || fail "install.sh not found after clone. Invalid repo." ok "install.sh found." # ─── INSTALL ────────────────────────────────────────────────────────────────── sep info "Running installer..." info "WireGuard will NOT start — 02 will start it with the legacy identity." echo "" cd "$OPERATOR_DIR" sudo systemctl start nginx 2>/dev/null || true sudo MIGRATION_MODE=1 bash install.sh || fail "Installer failed. Check output above." if _env_is_corrupt "$OPERATOR_DIR/.env"; then fail "install.sh finished but generated an invalid .env. Check install.sh." fi ok ".env generated with valid values." # ─── WG0.CONF PLACEHOLDER ───────────────────────────────────────────────────── sep info "Checking wg0.conf..." WG_CONF_DIR="$OPERATOR_DIR/services/wireguard/config/wg_confs" WG_CONF_FILE="$WG_CONF_DIR/wg0.conf" if [ ! -f "$WG_CONF_FILE" ]; then warn "wg0.conf missing (MIGRATION_MODE skips WireGuard). Creating placeholder..." mkdir -p "$WG_CONF_DIR" cat > "$WG_CONF_FILE" << 'WGEOF' [Interface] Address = 10.13.13.1/16 ListenPort = 51820 PrivateKey = PLACEHOLDER PostUp = /config/scripts/post-up.sh PreDown = /config/scripts/pre-down.sh WGEOF ok "Placeholder created. 02 will inject the legacy private key." else ok "wg0.conf already exists." fi # ─── RESTORE EXTERNAL NGINX CONFIGS ─────────────────────────────────────────── sep info "Restoring external Nginx configs..." for SITE in "$BACKUP_DIR/nginx/sites-available/"*; do NAME=$(basename "$SITE") if ! grep -q "$NAME" /etc/nginx/sites-available/* 2>/dev/null; then sudo cp "$SITE" "/etc/nginx/sites-available/$NAME" 2>/dev/null || true if [ -L "$BACKUP_DIR/nginx/sites-enabled/$NAME" ] || \ [ -f "$BACKUP_DIR/nginx/sites-enabled/$NAME" ]; then sudo ln -sf "/etc/nginx/sites-available/$NAME" \ "/etc/nginx/sites-enabled/$NAME" 2>/dev/null || true ok "Config restored: $NAME" fi fi done sudo nginx -t && sudo systemctl reload nginx 2>/dev/null \ || warn "Nginx reload failed — check manually." trap - ERR echo "[END-01] $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG" echo "" echo "=========================================" echo " Installation complete" echo " Next: bash 02-migrate-peers.sh" echo "=========================================" echo ""