#!/usr/bin/env bash # ────────────────────────────────────────────────────────────────────────────── # NixPanel Installer · Ubuntu 24.04 LTS · x86_64 # Installs: WHM (NixServer), cPanel (NixClient), Licensing Server, # Website API, and all system dependencies. # ────────────────────────────────────────────────────────────────────────────── set -euo pipefail # ── Colours ─────────────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m' YELLOW='\033[1;33m'; 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}[ERR ]${RESET} $*" >&2; exit 1; } step() { echo -e "\n${BOLD}${CYAN}━━━ $* ━━━${RESET}"; } # ── Preflight ───────────────────────────────────────────────────────────────── [[ $EUID -ne 0 ]] && error "Must be run as root → sudo bash install.sh" OS_ID=$(. /etc/os-release && echo "$ID") OS_VER=$(. /etc/os-release && echo "$VERSION_ID") ARCH=$(uname -m) [[ "$OS_ID" != "ubuntu" ]] && error "Requires Ubuntu 24.04 LTS (detected: $OS_ID)" [[ "$OS_VER" != "24.04" ]] && error "Requires Ubuntu 24.04 LTS (detected: Ubuntu $OS_VER)" [[ "$ARCH" != "x86_64" ]] && error "Requires x86_64 (detected: $ARCH)" # ── Paths ───────────────────────────────────────────────────────────────────── INSTALL_DIR="/opt/nixpanel" BIN_DIR="$INSTALL_DIR/bin" KEYS_DIR="$INSTALL_DIR/keys" LOG_DIR="/var/log/nixpanel" STEP_DIR="/var/lib/nixpanel/install" SRC_DIR="$INSTALL_DIR/src" GITHUB_REPO="NixReaper/nixpanel" mkdir -p "$INSTALL_DIR" "$BIN_DIR" "$KEYS_DIR" "$LOG_DIR" "$STEP_DIR" "$SRC_DIR" chmod 700 "$KEYS_DIR" mark_done() { touch "$STEP_DIR/$1.done"; } step_done() { [[ -f "$STEP_DIR/$1.done" ]]; } # ── Banner ──────────────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}${CYAN}" echo " ███╗ ██╗██╗██╗ ██╗██████╗ █████╗ ███╗ ██╗███████╗██╗ " echo " ████╗ ██║██║╚██╗██╔╝██╔══██╗██╔══██╗████╗ ██║██╔════╝██║ " echo " ██╔██╗ ██║██║ ╚███╔╝ ██████╔╝███████║██╔██╗ ██║█████╗ ██║ " echo " ██║╚██╗██║██║ ██╔██╗ ██╔═══╝ ██╔══██║██║╚██╗██║██╔══╝ ██║ " echo " ██║ ╚████║██║██╔╝ ██╗██║ ██║ ██║██║ ╚████║███████╗███████╗" echo " ╚═╝ ╚═══╝╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝" echo -e "${RESET}" echo -e " Web Hosting Control Panel · Ubuntu 24.04 · x86_64" echo "" # ── 1. System update ────────────────────────────────────────────────────────── step "Updating system packages" if ! step_done "apt_update"; then export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get upgrade -y -qq mark_done "apt_update" fi success "System packages up to date" # ── 2. Free port 53 for PowerDNS ───────────────────────────────────────────── step "Freeing port 53 for PowerDNS" if ! step_done "resolved_stub"; then sed -i '/^#\?DNSStubListener=/d' /etc/systemd/resolved.conf echo "DNSStubListener=no" >> /etc/systemd/resolved.conf systemctl restart systemd-resolved 2>/dev/null || true mark_done "resolved_stub" fi success "Port 53 free" # ── 3. ondrej/php PPA (required for PHP 8.2 on Ubuntu 24.04) ───────────────── step "Adding ondrej/php PPA" if ! step_done "php_ppa"; then export DEBIAN_FRONTEND=noninteractive # Install prereqs for add-apt-repository apt-get install -y -qq ca-certificates gnupg software-properties-common add-apt-repository -y ppa:ondrej/php apt-get update -qq mark_done "php_ppa" fi success "ondrej/php PPA added" # ── 4. System packages ──────────────────────────────────────────────────────── step "Installing system packages" if ! step_done "packages"; then export DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ build-essential pkg-config libssl-dev libmariadb-dev curl wget git unzip \ ca-certificates gnupg software-properties-common \ apache2 \ php8.3 php8.3-fpm php8.3-cli php8.3-mysql php8.3-curl php8.3-mbstring \ php8.3-xml php8.3-zip php8.3-gd php8.3-intl php8.3-bcmath \ php8.2 php8.2-fpm php8.2-cli php8.2-mysql php8.2-curl php8.2-mbstring \ php8.2-xml php8.2-zip php8.2-gd php8.2-intl php8.2-bcmath \ mariadb-server mariadb-client \ pdns-server pdns-backend-bind \ exim4 exim4-daemon-light \ dovecot-imapd dovecot-pop3d dovecot-lmtpd \ spamassassin spamc \ certbot python3-certbot-apache \ fail2ban \ nodejs npm \ openssl mark_done "packages" fi success "System packages installed" # ── 4. Rust toolchain ───────────────────────────────────────────────────────── step "Installing Rust toolchain" if ! step_done "rust"; then # Install for root (used by this script) and persist for system builds curl -fsSL https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal source "$HOME/.cargo/env" rustup target add x86_64-unknown-linux-gnu mark_done "rust" else # Ensure cargo is on PATH for subsequent steps source "$HOME/.cargo/env" 2>/dev/null || true fi CARGO=$(command -v cargo) || error "cargo not found — Rust install may have failed" success "Rust $(rustc --version) installed" # ── 5. Start services ───────────────────────────────────────────────────────── step "Starting service stack" if ! step_done "services_start"; then a2enmod rewrite ssl proxy proxy_http proxy_wstunnel proxy_fcgi headers expires deflate systemctl enable --now apache2 mariadb systemctl enable pdns && systemctl start pdns 2>/dev/null || true systemctl enable spamd && systemctl start spamd 2>/dev/null || true mark_done "services_start" fi success "Service stack running" # ── 6. MariaDB databases ────────────────────────────────────────────────────── step "Configuring MariaDB databases" if ! step_done "mariadb_setup"; then DB_PASS_PANEL=$(openssl rand -hex 20) DB_PASS_LICENSE=$(openssl rand -hex 20) DB_PASS_WEBSITE=$(openssl rand -hex 20) mysql -u root < "$INSTALL_DIR/.db_pass_panel" echo "$DB_PASS_LICENSE" > "$INSTALL_DIR/.db_pass_license" echo "$DB_PASS_WEBSITE" > "$INSTALL_DIR/.db_pass_website" chmod 600 "$INSTALL_DIR"/.db_pass_* mark_done "mariadb_setup" else DB_PASS_PANEL=$(cat "$INSTALL_DIR/.db_pass_panel") DB_PASS_LICENSE=$(cat "$INSTALL_DIR/.db_pass_license") DB_PASS_WEBSITE=$(cat "$INSTALL_DIR/.db_pass_website") fi success "Databases created" # ── 7. Download / update source ─────────────────────────────────────────────── step "Downloading NixPanel source" if ! step_done "source"; then LATEST_TAG=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" \ | grep '"tag_name"' | cut -d'"' -f4 || echo "main") [[ -z "$LATEST_TAG" ]] && LATEST_TAG="main" info "Fetching tag: $LATEST_TAG" if [[ -d "$SRC_DIR/.git" ]]; then git -C "$SRC_DIR" fetch --tags --quiet git -C "$SRC_DIR" checkout "$LATEST_TAG" --quiet else git clone --depth 1 --branch "$LATEST_TAG" \ "https://github.com/${GITHUB_REPO}.git" "$SRC_DIR" 2>/dev/null \ || git clone --depth 1 "https://github.com/${GITHUB_REPO}.git" "$SRC_DIR" fi mark_done "source" fi success "Source ready" # ── 8. Build Rust binaries ──────────────────────────────────────────────────── step "Building nixpanel-licensing (Rust)" if ! step_done "build_licensing"; then cd "$SRC_DIR/nixpanel-licensing" $CARGO build --release --quiet cp target/release/nixpanel-licensing "$BIN_DIR/nixpanel-licensing" cp target/release/keygen "$BIN_DIR/nixpanel-keygen" chmod 755 "$BIN_DIR/nixpanel-licensing" "$BIN_DIR/nixpanel-keygen" mark_done "build_licensing" fi success "nixpanel-licensing built" step "Building nixpanel-website-api (Rust)" if ! step_done "build_website_api"; then cd "$SRC_DIR/nixpanel-website/api" $CARGO build --release --quiet cp target/release/nixpanel-website-api "$BIN_DIR/nixpanel-website-api" chmod 755 "$BIN_DIR/nixpanel-website-api" mark_done "build_website_api" fi success "nixpanel-website-api built" # ── 9. RSA key generation ───────────────────────────────────────────────────── step "Generating RSA-4096 license signing keys" if ! step_done "keygen"; then # Run keygen from the licensing source dir so it finds ./keys/ cd "$SRC_DIR/nixpanel-licensing" "$BIN_DIR/nixpanel-keygen" cp keys/private.pem "$KEYS_DIR/license_private.pem" cp keys/public.pem "$KEYS_DIR/license_public.pem" chmod 600 "$KEYS_DIR/license_private.pem" chmod 644 "$KEYS_DIR/license_public.pem" mark_done "keygen" fi success "RSA keys generated → $KEYS_DIR" # ── 10. Secrets ─────────────────────────────────────────────────────────────── step "Generating secrets" if ! step_done "secrets"; then JWT_SECRET_LICENSE=$(openssl rand -hex 64) JWT_SECRET_WEBSITE=$(openssl rand -hex 64) JWT_SECRET_PANEL=$(openssl rand -hex 64) LICENSING_API_KEY=$(openssl rand -hex 32) echo "$JWT_SECRET_LICENSE" > "$INSTALL_DIR/.jwt_license" echo "$JWT_SECRET_WEBSITE" > "$INSTALL_DIR/.jwt_website" echo "$JWT_SECRET_PANEL" > "$INSTALL_DIR/.jwt_panel" echo "$LICENSING_API_KEY" > "$INSTALL_DIR/.licensing_api_key" chmod 600 "$INSTALL_DIR"/.jwt_* "$INSTALL_DIR/.licensing_api_key" mark_done "secrets" else JWT_SECRET_LICENSE=$(cat "$INSTALL_DIR/.jwt_license") JWT_SECRET_WEBSITE=$(cat "$INSTALL_DIR/.jwt_website") JWT_SECRET_PANEL=$(cat "$INSTALL_DIR/.jwt_panel") LICENSING_API_KEY=$(cat "$INSTALL_DIR/.licensing_api_key") fi success "Secrets ready" # ── 11. Environment files ───────────────────────────────────────────────────── step "Writing environment files" # nixpanel-licensing cat > "$INSTALL_DIR/licensing.env" < "$INSTALL_DIR/.env" < /etc/systemd/system/nixpanel-licensing.service < "$INSTALL_DIR/website.env" </dev/null || warn "Admin creation script not found — set credentials via panel after install" mark_done "admin_account" fi success "Admin account ready" # ── 14. systemd — website API ───────────────────────────────────────────────── step "Installing systemd service: nixpanel-website-api" if ! step_done "systemd_website"; then cat > /etc/systemd/system/nixpanel-website-api.service < /etc/systemd/system/nixpanel.service <> /etc/apache2/ports.conf done cat > /etc/apache2/sites-available/nixpanel.conf <<'APACHEEOF' # ── NixServer — WHM Admin Panel (port 2087) ─────────────────────────────────── ProxyPreserveHost On ProxyPass /api/ http://127.0.0.1:4000/api/ ProxyPassReverse /api/ http://127.0.0.1:4000/api/ ProxyPass /ws/ ws://127.0.0.1:4000/ws/ ProxyPassReverse /ws/ ws://127.0.0.1:4000/ws/ DocumentRoot /opt/nixpanel/nixserver/dist Options -Indexes AllowOverride All Require all granted FallbackResource /index.html ErrorLog /var/log/nixpanel/nixserver-error.log CustomLog /var/log/nixpanel/nixserver-access.log combined # ── NixClient — cPanel User Panel (port 2083) ───────────────────────────────── ProxyPreserveHost On ProxyPass /api/ http://127.0.0.1:4000/api/ ProxyPassReverse /api/ http://127.0.0.1:4000/api/ DocumentRoot /opt/nixpanel/nixclient/dist Options -Indexes AllowOverride All Require all granted FallbackResource /index.html ErrorLog /var/log/nixpanel/nixclient-error.log CustomLog /var/log/nixpanel/nixclient-access.log combined # ── NixPanel Website API (port 4001) ────────────────────────────────────────── ProxyPreserveHost On ProxyPass / http://127.0.0.1:4001/ ProxyPassReverse / http://127.0.0.1:4001/ APACHEEOF a2ensite nixpanel apache2ctl configtest && systemctl reload apache2 mark_done "apache_vhost" fi success "Apache vhosts configured" # ── 17. Fail2ban ────────────────────────────────────────────────────────────── step "Configuring Fail2ban" if ! step_done "fail2ban"; then cat > /etc/fail2ban/jail.d/nixpanel.conf <<'EOF' [nixpanel-auth] enabled = true port = 2083,2087,4001 filter = nixpanel-auth logpath = /var/log/nixpanel/*.log maxretry = 10 bantime = 3600 findtime = 600 EOF systemctl enable --now fail2ban mark_done "fail2ban" fi success "Fail2ban configured" # ── 18. File permissions ────────────────────────────────────────────────────── step "Setting permissions" chown -R root:root "$INSTALL_DIR" chmod 700 "$KEYS_DIR" chmod 600 "$INSTALL_DIR"/.env \ "$INSTALL_DIR"/licensing.env \ "$INSTALL_DIR"/website.env 2>/dev/null || true find "$INSTALL_DIR/bin" -type f -exec chmod 755 {} \; [[ -d "$INSTALL_DIR/nixserver/dist" ]] && \ find "$INSTALL_DIR/nixserver/dist" \( -type d -exec chmod 755 {} \; -o -type f -exec chmod 644 {} \; \) [[ -d "$INSTALL_DIR/nixclient/dist" ]] && \ find "$INSTALL_DIR/nixclient/dist" \( -type d -exec chmod 755 {} \; -o -type f -exec chmod 644 {} \; \) success "Permissions set" # ── 19. Service health check ────────────────────────────────────────────────── step "Verifying services" sleep 2 # give systemd a moment check_service() { if systemctl is-active --quiet "$1"; then success "$1 is running" else warn "$1 is NOT running — check: journalctl -u $1 -n 50" fi } check_service nixpanel-licensing check_service nixpanel-website-api check_service nixpanel check_service apache2 check_service mariadb # ── Done ────────────────────────────────────────────────────────────────────── SERVER_IP=$(hostname -I | awk '{print $1}') echo "" echo -e "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" echo -e "${BOLD}${GREEN} NixPanel Installation Complete!${RESET}" echo -e "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" echo "" echo -e " ${BOLD}WHM (Admin panel):${RESET} http://${SERVER_IP}:2087" echo -e " ${BOLD}cPanel (User panel):${RESET} http://${SERVER_IP}:2083" echo -e " ${BOLD}Website API:${RESET} http://${SERVER_IP}:4001" echo -e " ${BOLD}Licensing server:${RESET} http://127.0.0.1:3000 (internal only)" echo "" echo -e " ${BOLD}RSA public key:${RESET} ${KEYS_DIR}/license_public.pem" echo -e " ${BOLD}Logs:${RESET} ${LOG_DIR}/" echo -e " ${BOLD}Config:${RESET} ${INSTALL_DIR}/*.env" echo "" echo -e " ${YELLOW}Stripe Price IDs must still be added to:${RESET}" echo -e " ${INSTALL_DIR}/website.env" echo "" echo -e " To add SSL: certbot --apache" echo -e " To restart: systemctl restart nixpanel nixpanel-licensing nixpanel-website-api" echo ""