Files
jabali-panel/install_from_gitea.sh
2026-01-29 04:01:43 +02:00

3456 lines
118 KiB
Bash
Executable File

#!/bin/bash
#
# Jabali Panel Installer
#
# Quick install (if curl/sudo are available):
# curl -fsSL http://192.168.100.100:3001/shukivaknin/jabali-panel/raw/branch/main/install_from_gitea.sh | sudo bash
#
# Minimal installation (if curl/sudo not installed):
# apt-get update && apt-get install -y curl sudo
# curl -fsSL http://192.168.100.100:3001/shukivaknin/jabali-panel/raw/branch/main/install_from_gitea.sh | sudo bash
#
set -e
# Version - prefer local VERSION file if present, fallback for curl installs
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
JABALI_VERSION="$(sed -n 's/^VERSION=//p' "$SCRIPT_DIR/VERSION")"
fi
JABALI_VERSION="${JABALI_VERSION:-0.9-rc26}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
BOLD='\033[1m'
# Configuration
JABALI_DIR="/var/www/jabali"
JABALI_USER="www-data"
JABALI_REPO="http://192.168.100.100:3001/shukivaknin/jabali-panel.git"
NODE_VERSION="20"
# PHP version will be detected after installation
PHP_VERSION=""
# Feature flags (default: all enabled)
INSTALL_MAIL=true
INSTALL_DNS=true
INSTALL_FIREWALL=true
INSTALL_SECURITY=true # Fail2ban + ClamAV
# Feature selection menu
select_features() {
echo ""
echo -e "${BOLD}Installation Options${NC}"
echo ""
echo "Jabali Panel includes optional components that can be installed based on your needs."
echo "Each component uses additional server resources."
echo ""
# Check if non-interactive mode
if [[ -n "$JABALI_MINIMAL" ]]; then
info "Minimal installation mode: Only core components will be installed"
INSTALL_MAIL=false
INSTALL_DNS=false
INSTALL_SECURITY=false
return
fi
if [[ -n "$JABALI_FULL" ]]; then
info "Full installation mode: All components will be installed"
return
fi
echo "Select installation type:"
echo ""
echo " 1) Full Installation (Recommended)"
echo " - Web Server (Nginx, PHP, MariaDB, Redis)"
echo " - Mail Server (Postfix, Dovecot, Rspamd)"
echo " - DNS Server (BIND9)"
echo " - Security (Firewall, Fail2ban, ClamAV)"
echo ""
echo " 2) Minimal Installation"
echo " - Web Server only (Nginx, PHP, MariaDB, Redis)"
echo " - Firewall (UFW)"
echo ""
echo " 3) Custom Installation"
echo " - Choose individual components"
echo ""
local choice
read -p "Enter choice [1-3]: " choice < /dev/tty
case $choice in
1)
info "Full installation selected"
;;
2)
info "Minimal installation selected"
INSTALL_MAIL=false
INSTALL_DNS=false
INSTALL_SECURITY=false
;;
3)
echo ""
echo -e "${BOLD}Custom Installation${NC}"
echo ""
# Mail Server
read -p "Install Mail Server (Postfix, Dovecot)? [Y/n]: " mail_choice < /dev/tty
if [[ "$mail_choice" =~ ^[Nn]$ ]]; then
INSTALL_MAIL=false
fi
# DNS Server
read -p "Install DNS Server (BIND9)? [Y/n]: " dns_choice < /dev/tty
if [[ "$dns_choice" =~ ^[Nn]$ ]]; then
INSTALL_DNS=false
fi
# Firewall
read -p "Install Firewall (UFW)? [Y/n]: " fw_choice < /dev/tty
if [[ "$fw_choice" =~ ^[Nn]$ ]]; then
INSTALL_FIREWALL=false
fi
# Security tools
read -p "Install Security Tools (Fail2ban, ClamAV)? [Y/n]: " sec_choice < /dev/tty
if [[ "$sec_choice" =~ ^[Nn]$ ]]; then
INSTALL_SECURITY=false
fi
echo ""
info "Custom installation configured"
;;
*)
info "Defaulting to full installation"
;;
esac
echo ""
echo -e "${BOLD}Components to install:${NC}"
echo -e " - Web Server: ${GREEN}Yes${NC}"
[[ "$INSTALL_MAIL" == "true" ]] && echo -e " - Mail Server: ${GREEN}Yes${NC}" || echo -e " - Mail Server: ${YELLOW}No${NC}"
[[ "$INSTALL_DNS" == "true" ]] && echo -e " - DNS Server: ${GREEN}Yes${NC}" || echo -e " - DNS Server: ${YELLOW}No${NC}"
[[ "$INSTALL_FIREWALL" == "true" ]] && echo -e " - Firewall: ${GREEN}Yes${NC}" || echo -e " - Firewall: ${YELLOW}No${NC}"
[[ "$INSTALL_SECURITY" == "true" ]] && echo -e " - Security Tools: ${GREEN}Yes${NC}" || echo -e " - Security Tools: ${YELLOW}No${NC}"
echo ""
}
# Detect installed PHP version
detect_php_version() {
# Try to find the highest installed PHP version
if command -v php &> /dev/null; then
PHP_VERSION=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;' 2>/dev/null)
fi
# Fallback: check for PHP-FPM sockets
if [[ -z "$PHP_VERSION" ]]; then
for ver in 8.4 8.3 8.2 8.1 8.0; do
if [[ -f "/etc/php/${ver}/fpm/php.ini" ]]; then
PHP_VERSION="$ver"
break
fi
done
fi
if [[ -z "$PHP_VERSION" ]]; then
error "Could not detect PHP version. Please ensure PHP is installed."
fi
info "Detected PHP version: $PHP_VERSION"
}
# Logging
log() {
echo -e "${GREEN}[✓]${NC} $1"
}
warn() {
echo -e "${YELLOW}[!]${NC} $1"
}
error() {
echo -e "${RED}[✗]${NC} $1"
exit 1
}
info() {
echo -e "${CYAN}[i]${NC} $1"
}
header() {
echo ""
echo -e "${BOLD}${BLUE}=== $1 ===${NC}"
echo ""
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root (use sudo)"
fi
}
# Check OS
check_os() {
if [[ ! -f /etc/debian_version ]]; then
error "This installer only supports Debian/Ubuntu systems"
fi
. /etc/os-release
info "Detected: $PRETTY_NAME"
case $ID in
debian)
# Read debian_version file
local debian_ver=$(cat /etc/debian_version 2>/dev/null || echo "")
# Check for testing/unstable first (trixie/sid)
if [[ "$debian_ver" == *trixie* ]] || [[ "$debian_ver" == *sid* ]] || \
[[ "$VERSION_CODENAME" == "trixie" ]] || [[ "$VERSION_CODENAME" == "sid" ]] || \
[[ -z "${VERSION_ID:-}" ]]; then
info "Detected Debian testing/unstable (trixie/sid) - proceeding"
elif [[ "$debian_ver" == *bookworm* ]] || [[ "$VERSION_CODENAME" == "bookworm" ]]; then
info "Detected Debian 12 (bookworm)"
elif [[ "$debian_ver" == *bullseye* ]] || [[ "$VERSION_CODENAME" == "bullseye" ]]; then
info "Detected Debian 11 (bullseye)"
elif [[ -n "${VERSION_ID:-}" ]] && [[ "${VERSION_ID}" -lt 11 ]]; then
error "Debian 11 or later is required"
else
# Try numeric extraction from debian_version
local num_ver=$(echo "$debian_ver" | grep -oE '^[0-9]+' | head -1)
if [[ -n "$num_ver" ]] && [[ "$num_ver" -ge 11 ]]; then
info "Detected Debian $num_ver"
else
warn "Unknown Debian version: $debian_ver - proceeding anyway"
fi
fi
;;
ubuntu)
if [[ -n "${VERSION_ID:-}" ]] && [[ ${VERSION_ID%.*} -lt 22 ]]; then
error "Ubuntu 22.04 or later is required"
fi
;;
*)
warn "Untested distribution: $ID. Proceeding anyway..."
;;
esac
}
# Display banner
show_banner() {
echo ""
echo -e "${YELLOW}░░░░░██╗░█████╗░██████╗░░█████╗░██╗░░░░░██╗${NC}"
echo -e "${YELLOW}░░░░░██║██╔══██╗██╔══██╗██╔══██╗██║░░░░░██║${NC}"
echo -e "${YELLOW}░░░░░██║███████║██████╦╝███████║██║░░░░░██║${NC}"
echo -e "${YELLOW}██╗░░██║██╔══██║██╔══██╗██╔══██║██║░░░░░██║${NC}"
echo -e "${YELLOW}╚█████╔╝██║░░██║██████╦╝██║░░██║███████╗██║${NC}"
echo -e "${YELLOW}░╚════╝░╚═╝░░╚═╝╚═════╝░╚═╝░░╚═╝╚══════╝╚═╝${NC}"
echo ""
echo -e " ${BOLD}Jabali Panel${NC} v${JABALI_VERSION} - ${CYAN}Modern Web Hosting Control Panel${NC}"
echo ""
}
# Prompt for server hostname
prompt_hostname() {
local current_hostname=$(hostname -f 2>/dev/null || hostname)
local server_ip=$(hostname -I | awk '{print $1}')
# Check if SERVER_HOSTNAME is already set (non-interactive mode)
if [[ -n "$SERVER_HOSTNAME" ]]; then
# Validate the preset hostname
if [[ ! "$SERVER_HOSTNAME" =~ ^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$ ]]; then
error "Invalid hostname format: $SERVER_HOSTNAME. Please use a valid FQDN (e.g., panel.example.com)"
fi
info "Using preset hostname: $SERVER_HOSTNAME"
else
echo -e "${BOLD}Server Configuration${NC}"
echo ""
echo "Enter the fully qualified domain name (FQDN) for this server."
echo "This will be used for:"
echo " - Server hostname"
echo " - Admin email address (admin@hostname)"
echo " - Mail server configuration"
echo ""
echo -e "Current hostname: ${CYAN}${current_hostname}${NC}"
echo -e "Server IP: ${CYAN}${server_ip}${NC}"
echo ""
while true; do
read -p "Enter hostname [$current_hostname]: " SERVER_HOSTNAME < /dev/tty
# Use current hostname as default if empty
if [[ -z "$SERVER_HOSTNAME" ]]; then
SERVER_HOSTNAME="$current_hostname"
fi
# Basic hostname validation
if [[ ! "$SERVER_HOSTNAME" =~ ^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$ ]]; then
warn "Invalid hostname format. Please use a valid FQDN (e.g., panel.example.com)"
continue
fi
break
done
echo ""
info "Using hostname: $SERVER_HOSTNAME"
echo ""
fi
# Set system hostname
hostnamectl set-hostname "$SERVER_HOSTNAME" 2>/dev/null || hostname "$SERVER_HOSTNAME"
# Update /etc/hosts
if ! grep -q "$SERVER_HOSTNAME" /etc/hosts; then
echo "127.0.1.1 $SERVER_HOSTNAME" >> /etc/hosts
fi
# Export for use in other functions
export SERVER_HOSTNAME
export ADMIN_EMAIL="admin@${SERVER_HOSTNAME}"
}
# Add required repositories
add_repositories() {
header "Adding Repositories"
# Update package list
apt-get update -qq
# Install prerequisites (software-properties-common is optional, mainly for Ubuntu)
apt-get install -y -qq apt-transport-https ca-certificates curl gnupg lsb-release sudo
apt-get install -y -qq software-properties-common 2>/dev/null || true
# Detect codename
local codename=$(lsb_release -sc)
# Ensure Debian contrib repository for geoipupdate and related packages
if [[ -f /etc/debian_version ]]; then
info "Ensuring Debian contrib repository..."
local contrib_list="/etc/apt/sources.list.d/jabali-contrib.list"
if [[ ! -f "$contrib_list" ]]; then
cat > "$contrib_list" <<EOF
deb https://deb.debian.org/debian ${codename} contrib
deb https://deb.debian.org/debian ${codename}-updates contrib
deb https://security.debian.org/debian-security ${codename}-security contrib
EOF
fi
fi
# Sury supports: bookworm, bullseye, buster, trixie (Debian) and jammy, focal, noble (Ubuntu)
info "Using Sury PHP repository for $codename"
# Add/update PHP repository (Sury) - always update to ensure correct codename
info "Configuring PHP repository..."
curl -fsSL https://packages.sury.org/php/apt.gpg | gpg --dearmor --yes -o /usr/share/keyrings/sury-php.gpg 2>/dev/null || true
echo "deb [signed-by=/usr/share/keyrings/sury-php.gpg] https://packages.sury.org/php/ ${codename} main" > /etc/apt/sources.list.d/php.list
# Add NodeJS repository
if [[ ! -f /etc/apt/sources.list.d/nodesource.list ]]; then
info "Adding NodeJS repository..."
curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - > /dev/null 2>&1
fi
# Add MariaDB repository (optional, system version usually fine)
apt-get update -qq
log "Repositories configured"
}
# Install system packages
install_packages() {
header "Installing System Packages"
# Clean up conflicting packages from previous failed installations
if dpkg -l apache2 2>/dev/null | grep -q '^ii'; then
info "Removing Apache2 (conflicts with nginx)..."
systemctl stop apache2 2>/dev/null || true
systemctl disable apache2 2>/dev/null || true
DEBIAN_FRONTEND=noninteractive apt-get purge -y apache2 apache2-bin apache2-utils apache2-data 2>/dev/null || true
fi
# Clean up broken PHP state from previous failed installations
# Check if php8.4 packages exist in any state (installed, config-files, half-installed, etc.)
# dpkg remembers "deleted" config files and won't recreate them on reinstall
if dpkg -l 'php8.4*' 2>/dev/null | grep -qE '^(ii|rc|iU|iF|hi|pi)'; then
# Check if configs are missing or broken
if [[ ! -f /etc/php/8.4/fpm/php.ini ]] || [[ ! -f /etc/php/8.4/cli/php.ini ]] || \
dpkg -l php8.4-fpm 2>/dev/null | grep -qE '^(rc|iU|iF)'; then
info "Cleaning up broken PHP installation..."
systemctl stop php8.4-fpm 2>/dev/null || true
systemctl reset-failed php8.4-fpm 2>/dev/null || true
DEBIAN_FRONTEND=noninteractive apt-get purge -y 'php8.4*' 2>/dev/null || true
rm -rf /etc/php/8.4
apt-get clean
dpkg --configure -a 2>/dev/null || true
fi
fi
# Core packages (always installed)
local base_packages=(
# Web Server
nginx
# Database
mariadb-server
mariadb-client
# Cache
redis-server
# SSL
certbot
python3-certbot-nginx
# Utilities
git
curl
wget
zip
unzip
htop
net-tools
dnsutils
nodejs
acl
socat
sshpass
pigz
locales
# Security (always installed)
fail2ban
geoipupdate
libnginx-mod-http-geoip2
# For screenshots (Puppeteer)
chromium
# For PHP compilation/extensions
build-essential
# Disk quota management
quota
# Log analysis
goaccess
)
# Add Mail Server packages if enabled
if [[ "$INSTALL_MAIL" == "true" ]]; then
info "Including Mail Server packages..."
base_packages+=(
postfix
postfix-mysql
dovecot-core
dovecot-imapd
dovecot-pop3d
dovecot-lmtpd
dovecot-mysql
dovecot-sqlite
opendkim
opendkim-tools
rspamd
# Webmail
roundcube
roundcube-core
roundcube-sqlite3
roundcube-plugins
# SQLite tools
sqlite3
)
fi
# Add DNS Server packages if enabled
if [[ "$INSTALL_DNS" == "true" ]]; then
info "Including DNS Server packages..."
base_packages+=(
bind9
bind9-utils
)
fi
# Add Firewall packages if enabled
if [[ "$INSTALL_FIREWALL" == "true" ]]; then
info "Including Firewall packages..."
base_packages+=(
ufw
)
fi
# Add Security packages if enabled
if [[ "$INSTALL_SECURITY" == "true" ]]; then
info "Including Security packages..."
base_packages+=(
clamav
clamav-daemon
clamav-freshclam
# Vulnerability scanners
lynis
# nikto is installed from GitHub (not in apt repos)
# Ruby for WPScan
ruby
ruby-dev
)
fi
# Prevent Apache2 and libapache2-mod-php from being installed
# (roundcube recommends apache2, php metapackage recommends libapache2-mod-php, but we use nginx+php-fpm)
info "Blocking Apache2 and mod-php installation (we use nginx + php-fpm)..."
apt-mark hold apache2 libapache2-mod-php libapache2-mod-php8.4 2>/dev/null || true
# Pre-configure postfix and roundcube to avoid interactive prompts
# Note: debconf templates may not exist yet on fresh install, so suppress errors
if [[ "$INSTALL_MAIL" == "true" ]]; then
echo "postfix postfix/mailname string $(hostname -f)" | debconf-set-selections 2>/dev/null || true
echo "postfix postfix/main_mailer_type string 'Internet Site'" | debconf-set-selections 2>/dev/null || true
# Skip roundcube dbconfig - we configure it manually
echo "roundcube-core roundcube/dbconfig-install boolean false" | debconf-set-selections 2>/dev/null || true
echo "roundcube-core roundcube/database-type select sqlite3" | debconf-set-selections 2>/dev/null || true
fi
info "Installing base packages..."
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${base_packages[@]}" || {
warn "Some packages may not be available, retrying individually..."
for pkg in "${base_packages[@]}"; do
apt-get install -y -qq "$pkg" 2>/dev/null || warn "Could not install: $pkg"
done
}
if command -v locale-gen >/dev/null 2>&1; then
info "Configuring locales..."
if ! grep -q '^en_US.UTF-8 UTF-8' /etc/locale.gen 2>/dev/null; then
echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen
fi
locale-gen >/dev/null 2>&1 || warn "Locale generation failed"
update-locale LANG=en_US.UTF-8 >/dev/null 2>&1 || warn "Failed to set default locale"
fi
# Unhold packages in case user wants to install them manually later
apt-mark unhold apache2 libapache2-mod-php libapache2-mod-php8.4 2>/dev/null || true
# Install PHP 8.4 (required for Jabali Panel)
info "Installing PHP 8.4..."
# Stop, disable and mask Apache2 if installed (conflicts with nginx on port 80)
# Apache2 can be installed as a dependency of some PHP packages
if dpkg -l apache2 2>/dev/null | grep -q '^ii' || systemctl is-active --quiet apache2 2>/dev/null; then
info "Stopping Apache2 (conflicts with nginx)..."
systemctl stop apache2 2>/dev/null || true
systemctl disable apache2 2>/dev/null || true
systemctl mask apache2 2>/dev/null || true
fi
# Clean up any broken PHP-FPM state from previous installations
if systemctl is-failed --quiet php8.4-fpm 2>/dev/null; then
info "Resetting failed PHP-FPM service state..."
systemctl reset-failed php8.4-fpm
fi
# Required PHP extensions for Jabali Panel
local php_extensions=(
php8.4
php8.4-fpm
php8.4-cli
php8.4-common
php8.4-mysql
php8.4-pgsql
php8.4-sqlite3
php8.4-curl
php8.4-gd
php8.4-mbstring
php8.4-xml # Provides dom extension
php8.4-zip
php8.4-bcmath
php8.4-intl # Required by Filament
php8.4-readline
php8.4-soap
php8.4-imap
php8.4-ldap
php8.4-imagick
php8.4-redis
php8.4-opcache
)
# Install all PHP packages (use --force-confmiss to handle dpkg's "deleted config" state)
if ! DEBIAN_FRONTEND=noninteractive apt-get install -y -o Dpkg::Options::="--force-confmiss" "${php_extensions[@]}"; then
warn "PHP installation had errors, attempting aggressive recovery..."
# Stop PHP-FPM if it's somehow running in a broken state
systemctl stop php8.4-fpm 2>/dev/null || true
systemctl reset-failed php8.4-fpm 2>/dev/null || true
# Purge ALL PHP 8.4 packages including config files
info "Purging all PHP 8.4 packages..."
DEBIAN_FRONTEND=noninteractive apt-get purge -y 'php8.4*' 2>/dev/null || true
# Also remove libapache2-mod-php if it got installed (it conflicts with php-fpm)
DEBIAN_FRONTEND=noninteractive apt-get purge -y 'libapache2-mod-php*' 2>/dev/null || true
# Remove config directories to force fresh install (dpkg won't replace "deleted" configs)
info "Removing PHP config directories..."
rm -rf /etc/php/8.4/fpm
rm -rf /etc/php/8.4/cli
rm -rf /etc/php/8.4/apache2
# Clean package cache
apt-get clean
apt-get autoclean
# Fix any broken dpkg state
dpkg --configure -a 2>/dev/null || true
# Reinstall PHP with force-confmiss to ensure config files are created
info "Reinstalling PHP 8.4 with fresh configuration..."
if ! DEBIAN_FRONTEND=noninteractive apt-get install -y -o Dpkg::Options::="--force-confmiss" "${php_extensions[@]}"; then
error "Failed to install PHP 8.4. Please check your system's package state and try again."
fi
fi
# Stop and disable Apache2 completely - it conflicts with nginx on port 80
# Apache2 can be installed as a dependency of PHP packages
if dpkg -l apache2 2>/dev/null | grep -q '^ii'; then
info "Disabling Apache2 (conflicts with nginx)..."
systemctl stop apache2 2>/dev/null || true
systemctl disable apache2 2>/dev/null || true
systemctl mask apache2 2>/dev/null || true # Prevent it from starting
fi
# Verify PHP 8.4 is installed and working
if ! php -v 2>/dev/null | grep -q "PHP 8.4"; then
error "PHP 8.4 installation failed. Found: $(php -v 2>/dev/null | head -1)"
fi
PHP_VERSION="8.4"
log "PHP 8.4 installed successfully"
# Ensure PHP-FPM is properly configured
if [[ ! -f "/etc/php/8.4/fpm/php-fpm.conf" ]] || [[ ! -f "/etc/php/8.4/fpm/php.ini" ]]; then
warn "PHP-FPM config files missing after install"
info "Purging and reinstalling PHP-FPM with fresh config..."
systemctl stop php8.4-fpm 2>/dev/null || true
systemctl reset-failed php8.4-fpm 2>/dev/null || true
DEBIAN_FRONTEND=noninteractive apt-get purge -y php8.4-fpm 2>/dev/null || true
rm -rf /etc/php/8.4/fpm
apt-get clean
DEBIAN_FRONTEND=noninteractive apt-get install -y -o Dpkg::Options::="--force-confmiss" php8.4-fpm
fi
# Verify PHP-FPM is running
if ! systemctl is-active --quiet php8.4-fpm; then
# Reset failed state first if needed
systemctl reset-failed php8.4-fpm 2>/dev/null || true
if ! systemctl start php8.4-fpm; then
warn "PHP-FPM failed to start, attempting recovery..."
# Check for config errors
php-fpm8.4 -t 2>&1 || true
systemctl status php8.4-fpm --no-pager -l || true
fi
fi
# Verify PHP CLI is working and has required extensions
info "Verifying PHP CLI and extensions..."
if ! command -v php &>/dev/null; then
error "PHP CLI is not in PATH after installation."
fi
# Ensure php.ini exists for CLI (dpkg doesn't replace deleted config files)
local cli_ini="/etc/php/8.4/cli/php.ini"
if [[ ! -f "$cli_ini" ]]; then
warn "PHP CLI config file missing: $cli_ini"
info "Reinstalling php8.4-cli with fresh config..."
DEBIAN_FRONTEND=noninteractive apt-get purge -y php8.4-cli 2>/dev/null || true
rm -rf /etc/php/8.4/cli
DEBIAN_FRONTEND=noninteractive apt-get install -y -o Dpkg::Options::="--force-confmiss" php8.4-cli
fi
# Verify required extensions are available
local missing_ext=""
php -r "class_exists('Phar') || exit(1);" 2>/dev/null || missing_ext="$missing_ext phar"
php -r "extension_loaded('dom') || exit(1);" 2>/dev/null || missing_ext="$missing_ext dom"
php -r "extension_loaded('intl') || exit(1);" 2>/dev/null || missing_ext="$missing_ext intl"
php -r "extension_loaded('mbstring') || exit(1);" 2>/dev/null || missing_ext="$missing_ext mbstring"
if [[ -n "$missing_ext" ]]; then
warn "Missing PHP extensions:$missing_ext"
info "Extension .ini files may be missing. Purging and reinstalling PHP packages..."
# Purge php-common to remove stale config state, then reinstall all packages
DEBIAN_FRONTEND=noninteractive apt-get purge -y php8.4-common
DEBIAN_FRONTEND=noninteractive apt-get install -y "${php_extensions[@]}"
# Check again
php -r "class_exists('Phar') || exit(1);" 2>/dev/null || error "PHP Phar extension is missing"
php -r "extension_loaded('dom') || exit(1);" 2>/dev/null || error "PHP DOM extension is missing (install php8.4-xml)"
php -r "extension_loaded('intl') || exit(1);" 2>/dev/null || error "PHP Intl extension is missing (install php8.4-intl)"
fi
log "PHP 8.4 CLI verified with all required extensions"
# Install WPScan if security is enabled
if [[ "$INSTALL_SECURITY" == "true" ]]; then
info "Installing WPScan..."
if command -v gem &> /dev/null; then
gem install wpscan --no-document 2>/dev/null && {
log "WPScan installed successfully"
} || {
warn "WPScan installation failed (may require more memory)"
}
else
warn "Ruby gem not available, skipping WPScan"
fi
# Install Nikto from GitHub if not available via apt
if ! command -v nikto &> /dev/null; then
info "Installing Nikto from GitHub..."
if [[ ! -d "/opt/nikto" ]]; then
git clone https://github.com/sullo/nikto.git /opt/nikto 2>/dev/null && {
ln -sf /opt/nikto/program/nikto.pl /usr/local/bin/nikto
chmod +x /opt/nikto/program/nikto.pl
log "Nikto installed successfully"
} || {
warn "Nikto installation failed"
}
fi
fi
fi
# Final Apache2 cleanup - ensure it's stopped and masked before nginx starts
if systemctl is-active --quiet apache2 2>/dev/null; then
warn "Apache2 is still running, forcing stop..."
systemctl stop apache2 || true
systemctl disable apache2 || true
systemctl mask apache2 || true
fi
log "System packages installed"
}
install_geoipupdate_binary() {
if command -v geoipupdate &>/dev/null; then
return
fi
info "geoipupdate not found, installing from MaxMind releases..."
local arch
arch="$(uname -m)"
local arch_token="$arch"
if [[ "$arch" == "x86_64" ]]; then
arch_token="amd64"
elif [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then
arch_token="arm64"
fi
local api_url="https://api.github.com/repos/maxmind/geoipupdate/releases/latest"
local metadata
metadata=$(curl -fsSL "$api_url" 2>/dev/null || true)
if [[ -z "$metadata" ]]; then
metadata=$(wget -qO- "$api_url" 2>/dev/null || true)
fi
if [[ -z "$metadata" ]]; then
warn "Failed to download geoipupdate release metadata"
return
fi
local download_url
download_url=$(echo "$metadata" | grep -Eo "https://[^\"]+${arch_token}[^\"]+\\.tar\\.gz" | head -n1)
if [[ -z "$download_url" && "$arch_token" == "amd64" ]]; then
download_url=$(echo "$metadata" | grep -Eo "https://[^\"]+x86_64[^\"]+\\.tar\\.gz" | head -n1)
fi
if [[ -z "$download_url" ]]; then
warn "No suitable geoipupdate binary found for ${arch}"
return
fi
local tmp_dir
tmp_dir=$(mktemp -d)
local archive="${tmp_dir}/geoipupdate.tgz"
if command -v curl &>/dev/null; then
curl -fsSL "$download_url" -o "$archive" 2>/dev/null || true
else
wget -qO "$archive" "$download_url" 2>/dev/null || true
fi
if [[ ! -s "$archive" ]]; then
warn "Failed to download geoipupdate binary"
rm -rf "$tmp_dir"
return
fi
tar -xzf "$archive" -C "$tmp_dir" 2>/dev/null || true
local binary
binary=$(find "$tmp_dir" -type f -name geoipupdate | head -n1)
if [[ -z "$binary" ]]; then
warn "geoipupdate binary not found in archive"
rm -rf "$tmp_dir"
return
fi
install -m 0755 "$binary" /usr/local/bin/geoipupdate 2>/dev/null || true
rm -rf "$tmp_dir"
}
# Install Composer
install_composer() {
header "Installing Composer"
# PHP and Phar extension should already be verified by install_packages
# Just do a quick sanity check
if ! command -v php &>/dev/null; then
error "PHP is not installed or not in PATH"
fi
# Quick Phar check (should already be verified, but be safe)
if ! php -r "echo class_exists('Phar') ? 'ok' : 'no';" 2>/dev/null | grep -q ok; then
error "PHP Phar extension is required. Please reinstall PHP CLI: apt-get install --reinstall php${PHP_VERSION}-cli"
fi
info "PHP Phar extension: OK"
if command -v composer &>/dev/null; then
log "Composer already installed"
return
fi
info "Downloading and installing Composer..."
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
if ! command -v composer &>/dev/null; then
error "Composer installation failed"
fi
log "Composer installed"
}
# Clone Jabali Panel
clone_jabali() {
header "Installing Jabali Panel"
if [[ -d "$JABALI_DIR" ]]; then
warn "Jabali directory exists, backing up..."
mv "$JABALI_DIR" "${JABALI_DIR}.bak.$(date +%s)"
fi
git clone "$JABALI_REPO" "$JABALI_DIR"
chown -R $JABALI_USER:$JABALI_USER "$JABALI_DIR"
# Prevent git safe.directory issues for upgrades run as root or www-data
git config --system --add safe.directory "$JABALI_DIR" 2>/dev/null || true
sudo -u $JABALI_USER git config --global --add safe.directory "$JABALI_DIR" 2>/dev/null || true
# Ensure runtime directories stay writable for PHP-FPM (default: www-data)
if id www-data &>/dev/null; then
chown -R $JABALI_USER:www-data \
"$JABALI_DIR/database" \
"$JABALI_DIR/storage" \
"$JABALI_DIR/bootstrap/cache" 2>/dev/null || true
chmod -R g+rwX \
"$JABALI_DIR/database" \
"$JABALI_DIR/storage" \
"$JABALI_DIR/bootstrap/cache" 2>/dev/null || true
find "$JABALI_DIR/database" "$JABALI_DIR/storage" "$JABALI_DIR/bootstrap/cache" -type d -exec chmod g+s {} + 2>/dev/null || true
fi
# Read version from cloned VERSION file
if [[ -f "$JABALI_DIR/VERSION" ]]; then
source "$JABALI_DIR/VERSION"
JABALI_VERSION="${VERSION:-$JABALI_VERSION}"
info "Installed version: ${JABALI_VERSION}"
fi
log "Jabali Panel cloned"
}
# Configure PHP
configure_php() {
header "Configuring PHP"
# Detect PHP version if not already set
if [[ -z "$PHP_VERSION" ]]; then
detect_php_version
fi
# Start PHP-FPM first to ensure files are created
systemctl start php${PHP_VERSION}-fpm 2>/dev/null || true
# Find PHP ini file - check multiple locations
local php_ini=""
local possible_paths=(
"/etc/php/${PHP_VERSION}/fpm/php.ini"
"/etc/php/${PHP_VERSION}/cli/php.ini"
"/etc/php/${PHP_VERSION}/php.ini"
"/etc/php/php.ini"
)
for path in "${possible_paths[@]}"; do
if [[ -f "$path" ]]; then
php_ini="$path"
break
fi
done
# If still not found, try to find it with broader search
if [[ -z "$php_ini" ]]; then
php_ini=$(find /etc/php -name "php.ini" 2>/dev/null | head -1)
fi
if [[ -z "$php_ini" || ! -f "$php_ini" ]]; then
warn "PHP configuration not found, skipping PHP configuration"
warn "You may need to configure PHP manually"
return
fi
info "Configuring PHP ${PHP_VERSION} using $php_ini..."
# PHP.ini settings for both FPM and CLI
local ini_files=("$php_ini")
[[ -f "/etc/php/${PHP_VERSION}/cli/php.ini" ]] && ini_files+=("/etc/php/${PHP_VERSION}/cli/php.ini")
[[ -f "/etc/php/${PHP_VERSION}/fpm/php.ini" ]] && ini_files+=("/etc/php/${PHP_VERSION}/fpm/php.ini")
# Remove duplicates
ini_files=($(echo "${ini_files[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))
for ini in "${ini_files[@]}"; do
if [[ -f "$ini" ]]; then
sed -i 's/upload_max_filesize = .*/upload_max_filesize = 512M/' "$ini"
sed -i 's/post_max_size = .*/post_max_size = 512M/' "$ini"
sed -i 's/memory_limit = .*/memory_limit = 512M/' "$ini"
sed -i 's/max_execution_time = .*/max_execution_time = 600/' "$ini"
sed -i 's/max_input_time = .*/max_input_time = 600/' "$ini"
sed -i 's/;date.timezone =.*/date.timezone = UTC/' "$ini"
fi
done
# Enable necessary extensions
phpenmod -v ${PHP_VERSION} phar curl mbstring xml zip 2>/dev/null || true
# Configure PHP-FPM www pool for large uploads
local www_pool="/etc/php/${PHP_VERSION}/fpm/pool.d/www.conf"
if [[ -f "$www_pool" ]]; then
# Remove existing settings if present
sed -i '/^php_admin_value\[upload_max_filesize\]/d' "$www_pool"
sed -i '/^php_admin_value\[post_max_size\]/d' "$www_pool"
sed -i '/^php_admin_value\[max_execution_time\]/d' "$www_pool"
sed -i '/^php_admin_value\[max_input_time\]/d' "$www_pool"
# Add upload settings
echo 'php_admin_value[upload_max_filesize] = 512M' >> "$www_pool"
echo 'php_admin_value[post_max_size] = 512M' >> "$www_pool"
echo 'php_admin_value[max_execution_time] = 600' >> "$www_pool"
echo 'php_admin_value[max_input_time] = 600' >> "$www_pool"
fi
# Reload PHP-FPM
if systemctl reload php${PHP_VERSION}-fpm 2>/dev/null; then
log "PHP ${PHP_VERSION} configured"
elif systemctl reload php-fpm 2>/dev/null; then
log "PHP configured"
else
warn "Could not reload PHP-FPM, you may need to reload it manually"
fi
}
# Configure MariaDB
configure_mariadb() {
header "Configuring MariaDB"
systemctl enable mariadb
systemctl start mariadb
# Secure installation (non-interactive)
mysql -e "DELETE FROM mysql.user WHERE User='';"
mysql -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');"
mysql -e "DROP DATABASE IF EXISTS test;"
mysql -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';"
mysql -e "FLUSH PRIVILEGES;"
# Create Jabali database
local db_password=$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 32)
mysql -e "CREATE DATABASE IF NOT EXISTS jabali CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -e "CREATE USER IF NOT EXISTS 'jabali'@'localhost' IDENTIFIED BY '${db_password}';"
mysql -e "GRANT ALL PRIVILEGES ON jabali.* TO 'jabali'@'localhost';"
mysql -e "FLUSH PRIVILEGES;"
# Save credentials
echo "DB_PASSWORD=${db_password}" > /root/.jabali_db_credentials
chmod 600 /root/.jabali_db_credentials
log "MariaDB configured"
info "Database credentials saved to /root/.jabali_db_credentials"
}
# Configure Nginx
configure_nginx() {
header "Configuring Nginx"
# Remove default site
rm -f /etc/nginx/sites-enabled/default
# Configure nginx global settings for large uploads
local nginx_conf="/etc/nginx/nginx.conf"
if [[ -f "$nginx_conf" ]]; then
# Add timeouts if not present
if ! grep -q "client_body_timeout" "$nginx_conf"; then
sed -i '/server_tokens/a\ client_max_body_size 512M;\n client_body_timeout 600s;\n fastcgi_read_timeout 600s;\n proxy_read_timeout 600s;' "$nginx_conf"
fi
# Add FastCGI cache settings if not present
if ! grep -q "fastcgi_cache_path" "$nginx_conf"; then
# Create cache directory
mkdir -p /var/cache/nginx/fastcgi
chown www-data:www-data /var/cache/nginx/fastcgi
# Add FastCGI cache configuration before the closing brace of http block
sed -i '/^http {/,/^}/ {
/include \/etc\/nginx\/sites-enabled\/\*;/a\
\
\t##\
\t# FastCGI Cache\
\t##\
\
\tfastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=JABALI:100m inactive=60m max_size=1g;\
\tfastcgi_cache_key "$scheme$request_method$host$request_uri";\
\tfastcgi_cache_use_stale error timeout invalid_header http_500 http_503;
}' "$nginx_conf"
fi
# Enable gzip compression for all text-based content
if ! grep -q "gzip_types" "$nginx_conf"; then
# Uncomment existing gzip settings (with tab-indented comments)
sed -i 's/^[[:space:]]*# gzip_vary on;/\tgzip_vary on;/' "$nginx_conf"
sed -i 's/^[[:space:]]*# gzip_proxied any;/\tgzip_proxied any;/' "$nginx_conf"
sed -i 's/^[[:space:]]*# gzip_comp_level 6;/\tgzip_comp_level 6;/' "$nginx_conf"
sed -i 's/^[[:space:]]*# gzip_buffers 16 8k;/\tgzip_buffers 16 8k;/' "$nginx_conf"
sed -i 's/^[[:space:]]*# gzip_http_version 1.1;/\tgzip_http_version 1.1;/' "$nginx_conf"
sed -i 's/^[[:space:]]*# gzip_types .*/\tgzip_types text\/plain text\/css text\/xml text\/javascript application\/json application\/javascript application\/xml application\/xml+rss application\/x-javascript application\/vnd.ms-fontobject application\/x-font-ttf font\/opentype font\/woff font\/woff2 image\/svg+xml image\/x-icon;/' "$nginx_conf"
# Add gzip_min_length after gzip_types to avoid compressing tiny files
sed -i '/^[[:space:]]*gzip_types/a\ gzip_min_length 256;' "$nginx_conf"
fi
fi
# Find PHP-FPM socket
local php_sock=""
local possible_sockets=(
"/var/run/php/php${PHP_VERSION}-fpm.sock"
"/var/run/php/php-fpm.sock"
"/run/php/php${PHP_VERSION}-fpm.sock"
"/run/php/php-fpm.sock"
)
for sock in "${possible_sockets[@]}"; do
if [[ -S "$sock" ]] || [[ -e "$sock" ]]; then
php_sock="$sock"
break
fi
done
# If not found, try to find it
if [[ -z "$php_sock" ]]; then
php_sock=$(find /var/run/php /run/php -name "*.sock" 2>/dev/null | head -1)
fi
# Default fallback
if [[ -z "$php_sock" ]]; then
php_sock="/var/run/php/php${PHP_VERSION}-fpm.sock"
warn "PHP socket not found, using default: $php_sock"
else
info "Using PHP socket: $php_sock"
fi
# Generate self-signed SSL certificate for the panel
log "Generating self-signed SSL certificate..."
local ssl_dir="/etc/ssl/jabali"
mkdir -p "$ssl_dir"
# Generate private key and self-signed certificate (valid for 10 years)
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout "$ssl_dir/panel.key" \
-out "$ssl_dir/panel.crt" \
-subj "/C=US/ST=State/L=City/O=Jabali Panel/CN=${SERVER_HOSTNAME:-localhost}" \
2>/dev/null
chmod 600 "$ssl_dir/panel.key"
chmod 644 "$ssl_dir/panel.crt"
# Ensure Jabali Nginx include files exist for WAF/Geo includes
local jabali_includes="/etc/nginx/jabali/includes"
mkdir -p "$jabali_includes"
if [[ ! -f "$jabali_includes/waf.conf" ]]; then
cat > "$jabali_includes/waf.conf" <<'EOF'
# Managed by Jabali
modsecurity off;
EOF
fi
if [[ ! -f "$jabali_includes/geo.conf" ]]; then
echo "# Managed by Jabali" > "$jabali_includes/geo.conf"
fi
# Create Jabali site config with HTTPS and HTTP redirect
cat > /etc/nginx/sites-available/${SERVER_HOSTNAME} << NGINX
# Redirect HTTP to HTTPS
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name ${SERVER_HOSTNAME} _;
return 301 https://\$host\$request_uri;
}
# HTTPS server
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
http2 on;
server_name ${SERVER_HOSTNAME} _;
root /var/www/jabali/public;
index index.php index.html;
# SSL Configuration
ssl_certificate /etc/ssl/jabali/panel.crt;
ssl_certificate_key /etc/ssl/jabali/panel.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
client_max_body_size 512M;
location / {
try_files \$uri \$uri/ /index.php?\$query_string;
}
location ~ \.php\$ {
fastcgi_pass unix:${php_sock};
fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name;
include fastcgi_params;
fastcgi_read_timeout 600;
}
location ~ /\.(?!well-known).* {
deny all;
}
# Roundcube Webmail
location ^~ /webmail/ {
alias /var/lib/roundcube/public_html/;
index index.php;
location ~ \.php\$ {
fastcgi_pass unix:${php_sock};
fastcgi_param SCRIPT_FILENAME \$request_filename;
include fastcgi_params;
fastcgi_read_timeout 600;
}
}
}
NGINX
ln -sf /etc/nginx/sites-available/${SERVER_HOSTNAME} /etc/nginx/sites-enabled/
# Create nginx pre-start script to ensure log directories exist
# This prevents nginx from failing if a user deletes their logs directory
cat > /usr/local/bin/nginx-ensure-logs << 'ENSURELOG'
#!/bin/bash
for conf in /etc/nginx/sites-enabled/*; do
if [ -f "$conf" ]; then
grep -oP '(access_log|error_log)\s+\K[^\s;]+' "$conf" 2>/dev/null | while read -r logpath; do
if [[ "$logpath" != "off" && "$logpath" != "/dev/null" && "$logpath" != syslog* && "$logpath" == /* ]]; then
logdir=$(dirname "$logpath")
if [ ! -d "$logdir" ]; then
mkdir -p "$logdir"
if [[ "$logdir" =~ ^/home/([^/]+)/domains/([^/]+)/logs$ ]]; then
username="${BASH_REMATCH[1]}"
id "$username" &>/dev/null && chown "$username:$username" "$logdir"
fi
fi
fi
done
fi
done
exit 0
ENSURELOG
chmod +x /usr/local/bin/nginx-ensure-logs
# Add systemd override to run the script before nginx starts
mkdir -p /etc/systemd/system/nginx.service.d
cat > /etc/systemd/system/nginx.service.d/ensure-logs.conf << 'OVERRIDE'
[Service]
ExecStartPre=
ExecStartPre=/usr/local/bin/nginx-ensure-logs
ExecStartPre=/usr/sbin/nginx -t -q -g 'daemon on; master_process on;'
OVERRIDE
systemctl daemon-reload
nginx -t && systemctl reload nginx
log "Nginx configured with HTTPS (self-signed certificate)"
}
# Configure Mail Server
configure_mail() {
header "Configuring Mail Server"
# Basic Postfix config
postconf -e "smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem"
postconf -e "smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key"
postconf -e "smtpd_tls_security_level=may"
postconf -e "smtpd_tls_auth_only=yes"
postconf -e "virtual_transport=lmtp:unix:private/dovecot-lmtp"
postconf -e "virtual_mailbox_domains=hash:/etc/postfix/virtual_mailbox_domains"
postconf -e "virtual_mailbox_maps=hash:/etc/postfix/virtual_mailbox_maps"
postconf -e "virtual_alias_maps=hash:/etc/postfix/virtual_aliases"
# Create empty virtual map files and generate hash databases
touch /etc/postfix/virtual_mailbox_domains
touch /etc/postfix/virtual_mailbox_maps
touch /etc/postfix/virtual_aliases
postmap /etc/postfix/virtual_mailbox_domains
postmap /etc/postfix/virtual_mailbox_maps
postmap /etc/postfix/virtual_aliases
# Configure submission port (587) for authenticated mail clients
if ! grep -q "^submission" /etc/postfix/master.cf; then
cat >> /etc/postfix/master.cf << 'SUBMISSION'
# Submission port for authenticated mail clients
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_sasl_type=dovecot
-o smtpd_sasl_path=private/auth
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
SUBMISSION
fi
# Basic Dovecot config
mkdir -p /var/mail/vhosts
groupadd -g 5000 vmail 2>/dev/null || true
useradd -g vmail -u 5000 vmail -d /var/mail 2>/dev/null || true
chown -R vmail:vmail /var/mail
# Add dovecot to www-data group for SQLite access
usermod -a -G www-data dovecot 2>/dev/null || true
# Configure Dovecot SQL authentication for SQLite (Dovecot 2.4 format)
info "Configuring Dovecot SQL authentication..."
cat > /etc/dovecot/conf.d/auth-sql.conf.ext << 'DOVECOT_SQL'
# Authentication for SQL users - Jabali Panel
# Dovecot 2.4 configuration format
sql_driver = sqlite
sqlite_path = /var/www/jabali/database/database.sqlite
passdb sql {
query = SELECT \
m.local_part || '@' || d.domain AS user, \
m.password_hash AS password \
FROM mailboxes m \
JOIN email_domains e ON m.email_domain_id = e.id \
JOIN domains d ON e.domain_id = d.id \
WHERE m.local_part || '@' || d.domain = '%{user}' \
AND m.is_active = 1 \
AND e.is_active = 1
}
userdb sql {
query = SELECT \
m.maildir_path AS home, \
m.system_uid AS uid, \
m.system_gid AS gid \
FROM mailboxes m \
JOIN email_domains e ON m.email_domain_id = e.id \
JOIN domains d ON e.domain_id = d.id \
WHERE m.local_part || '@' || d.domain = '%{user}' \
AND m.is_active = 1
iterate_query = SELECT m.local_part || '@' || d.domain AS user \
FROM mailboxes m \
JOIN email_domains e ON m.email_domain_id = e.id \
JOIN domains d ON e.domain_id = d.id \
WHERE m.is_active = 1
}
DOVECOT_SQL
# Configure Dovecot master user for Jabali SSO
if [[ ! -d /etc/jabali ]]; then
mkdir -p /etc/jabali
chown root:www-data /etc/jabali
chmod 750 /etc/jabali
fi
master_user="jabali-master"
master_pass=""
if [[ -f /etc/jabali/roundcube-sso.conf ]]; then
master_user=$(grep -m1 '^JABALI_SSO_MASTER_USER=' /etc/jabali/roundcube-sso.conf | cut -d= -f2-)
master_pass=$(grep -m1 '^JABALI_SSO_MASTER_PASS=' /etc/jabali/roundcube-sso.conf | cut -d= -f2-)
fi
if [[ -z "$master_user" ]]; then
master_user="jabali-master"
fi
if [[ -z "$master_pass" ]]; then
master_pass=$(openssl rand -hex 24)
cat > /etc/jabali/roundcube-sso.conf <<EOF
JABALI_SSO_MASTER_USER=${master_user}
JABALI_SSO_MASTER_PASS=${master_pass}
EOF
chown root:www-data /etc/jabali/roundcube-sso.conf
chmod 640 /etc/jabali/roundcube-sso.conf
fi
if command -v doveadm >/dev/null 2>&1; then
master_hash=$(doveadm pw -s SHA512-CRYPT -p "$master_pass")
else
master_hash="{SHA512-CRYPT}$(openssl passwd -6 "$master_pass")"
fi
cat > /etc/dovecot/master-users <<EOF
${master_user}:${master_hash}
EOF
chown root:dovecot /etc/dovecot/master-users
chmod 640 /etc/dovecot/master-users
cat > /etc/dovecot/conf.d/auth-master.conf.ext << 'DOVECOT_MASTER'
passdb {
driver = passwd-file
args = /etc/dovecot/master-users
master = yes
}
DOVECOT_MASTER
# Enable SQL auth in Dovecot (disable system auth, enable SQL auth)
if [[ -f /etc/dovecot/conf.d/10-auth.conf ]]; then
# Comment out system auth if not already
sed -i 's/^!include auth-system.conf.ext/#!include auth-system.conf.ext/' /etc/dovecot/conf.d/10-auth.conf
# Enable SQL auth if not already
if ! grep -q "^!include auth-sql.conf.ext" /etc/dovecot/conf.d/10-auth.conf; then
sed -i 's/#!include auth-sql.conf.ext/!include auth-sql.conf.ext/' /etc/dovecot/conf.d/10-auth.conf
# If the line doesn't exist at all, add it
if ! grep -q "auth-sql.conf.ext" /etc/dovecot/conf.d/10-auth.conf; then
echo "!include auth-sql.conf.ext" >> /etc/dovecot/conf.d/10-auth.conf
fi
fi
if ! grep -q "^auth_master_user_separator" /etc/dovecot/conf.d/10-auth.conf; then
echo "auth_master_user_separator = *" >> /etc/dovecot/conf.d/10-auth.conf
fi
if ! grep -q "auth-master.conf.ext" /etc/dovecot/conf.d/10-auth.conf; then
echo "!include auth-master.conf.ext" >> /etc/dovecot/conf.d/10-auth.conf
fi
fi
# Configure Dovecot sockets for Postfix
if [[ -f /etc/dovecot/conf.d/10-master.conf ]]; then
# Add Postfix auth socket for SASL authentication
# Note: Check for our specific comment, not just the socket path (default config has commented example)
if ! grep -q "Postfix SMTP authentication socket" /etc/dovecot/conf.d/10-master.conf; then
cat >> /etc/dovecot/conf.d/10-master.conf << 'DOVECOT_AUTH'
# Postfix SMTP authentication socket
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}
DOVECOT_AUTH
fi
# Add LMTP socket for Postfix mail delivery
# Note: Check for our specific comment
if ! grep -q "LMTP socket for Postfix mail delivery" /etc/dovecot/conf.d/10-master.conf; then
cat >> /etc/dovecot/conf.d/10-master.conf << 'DOVECOT_LMTP'
# LMTP socket for Postfix mail delivery
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}
DOVECOT_LMTP
fi
fi
# Fix LMTP auth_username_format to keep full email address
if [[ -f /etc/dovecot/conf.d/20-lmtp.conf ]]; then
sed -i 's/auth_username_format = %{user | username | lower}/#auth_username_format = %{user | username | lower}/' /etc/dovecot/conf.d/20-lmtp.conf
fi
# Configure Dovecot mail storage (Maildir format)
cat > /etc/dovecot/conf.d/10-mail.conf << 'DOVECOT_MAIL'
##
## Mailbox locations and namespaces - Jabali Panel
##
# Mail storage format and location
# Using Maildir format - home is returned by userdb
mail_driver = maildir
mail_home = %{userdb:home}
mail_path = %{userdb:home}
namespace inbox {
inbox = yes
separator = /
}
DOVECOT_MAIL
systemctl enable postfix dovecot
# Create Roundcube SSO script for Jabali Panel
if [[ -d /var/lib/roundcube/public_html ]]; then
cat > /var/lib/roundcube/public_html/jabali-sso.php << 'RCUBE_SSO'
<?php
/**
* Jabali Panel SSO login for Roundcube
* Uses direct login API instead of form submission
*/
$token = $_GET["token"] ?? "";
if (empty($token) || !preg_match("/^[a-f0-9]{64}$/", $token)) {
die("Invalid token");
}
$cacheFile = "/tmp/roundcube_sso_" . $token;
if (!file_exists($cacheFile)) {
die("Token expired or invalid");
}
$data = json_decode(file_get_contents($cacheFile), true);
if (!$data) {
@unlink($cacheFile);
die("Invalid token data");
}
@unlink($cacheFile);
if (isset($data["expires"]) && time() > $data["expires"]) {
die("Token expired");
}
$masterUser = "";
$masterPass = "";
$masterConfig = "/etc/jabali/roundcube-sso.conf";
if (is_readable($masterConfig)) {
$lines = file($masterConfig, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (!str_contains($line, "=")) {
continue;
}
[$key, $value] = explode("=", $line, 2);
$key = trim($key);
$value = trim($value);
if ($key === "JABALI_SSO_MASTER_USER") {
$masterUser = $value;
} elseif ($key === "JABALI_SSO_MASTER_PASS") {
$masterPass = $value;
}
}
}
$useMaster = !empty($data["use_master"]);
if ($useMaster) {
$loginAs = trim($data["login_as"] ?? "");
if ($loginAs === "" || $masterUser === "" || $masterPass === "") {
die("SSO master login is not configured");
}
$authUser = $loginAs . "*" . $masterUser;
$authPass = $masterPass;
} else {
if (!isset($data["email"]) || !isset($data["password"])) {
die("Invalid token data");
}
$authUser = trim($data["email"]);
$authPass = $data["password"];
}
// Initialize Roundcube
define("INSTALL_PATH", "/var/lib/roundcube/");
require_once "/usr/share/roundcube/program/include/iniset.php";
$rcmail = rcmail::get_instance(0, "web");
// Perform direct login instead of form submission
$auth = $rcmail->plugins->exec_hook("authenticate", [
"host" => $rcmail->autoselect_host(),
"user" => $authUser,
"pass" => $authPass,
"valid" => true,
"cookiecheck" => false,
]);
if ($auth["valid"] && !$auth["abort"]) {
$login = $rcmail->login($auth["user"], $auth["pass"], $auth["host"], $auth["cookiecheck"]);
if ($login) {
// Login successful - redirect to inbox
$rcmail->session->regenerate_id(false);
$rcmail->session->set_auth_cookie();
header("Location: /webmail/?_task=mail");
exit;
}
}
// Login failed - show error
?>
<!DOCTYPE html>
<html>
<head><title>Login Failed</title></head>
<body>
<p>Login failed. Please try again or contact support.</p>
<p><a href="/webmail/">Go to webmail login</a></p>
</body>
</html>
RCUBE_SSO
chmod 755 /var/lib/roundcube/public_html/jabali-sso.php
log "Roundcube SSO script installed"
fi
# Configure Roundcube SMTP with TLS (use 127.0.0.1 instead of localhost for reliable TLS)
if [[ -f /etc/roundcube/config.inc.php ]]; then
sed -i "s|\$config\['smtp_host'\] = 'localhost:587';|\$config['smtp_host'] = 'tls://127.0.0.1:587';|g" /etc/roundcube/config.inc.php
sed -i "s|\$config\['smtp_host'\] = 'tls://localhost:587';|\$config['smtp_host'] = 'tls://127.0.0.1:587';|g" /etc/roundcube/config.inc.php
# Add SMTP SSL options if not present (disable cert verification for localhost)
if ! grep -q "smtp_conn_options" /etc/roundcube/config.inc.php; then
cat >> /etc/roundcube/config.inc.php << 'SMTP_SSL'
// Disable TLS certificate verification for localhost SMTP
$config['smtp_conn_options'] = [
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
],
];
SMTP_SSL
fi
fi
if [[ -f /var/lib/roundcube/config/config.inc.php ]]; then
sed -i "s|\$config\['smtp_host'\] = 'localhost:587';|\$config['smtp_host'] = 'tls://127.0.0.1:587';|g" /var/lib/roundcube/config/config.inc.php
sed -i "s|\$config\['smtp_host'\] = 'tls://localhost:587';|\$config['smtp_host'] = 'tls://127.0.0.1:587';|g" /var/lib/roundcube/config/config.inc.php
fi
# Fix Roundcube config file permissions so www-data can read them
# This is required for the SSO script to work
chown root:www-data /etc/roundcube/config.inc.php /etc/roundcube/debian-db.php 2>/dev/null
chmod 640 /etc/roundcube/config.inc.php /etc/roundcube/debian-db.php 2>/dev/null
chown root:www-data /var/lib/roundcube/config/config.inc.php /var/lib/roundcube/config/debian-db.php 2>/dev/null
chmod 640 /var/lib/roundcube/config/config.inc.php /var/lib/roundcube/config/debian-db.php 2>/dev/null
# Fix Roundcube SQLite database configuration
# The default debian-db.php has empty $basepath which causes DB errors
if [[ -f /etc/roundcube/debian-db.php ]]; then
cat > /etc/roundcube/debian-db.php << 'RCUBE_DB'
<?php
// Roundcube SQLite database configuration
// Configured by Jabali installer
$dbuser='roundcube';
$dbpass='';
$basepath='/var/lib/roundcube';
$dbname='roundcube.db';
$dbserver='';
$dbport='';
$dbtype='sqlite3';
RCUBE_DB
chown root:www-data /etc/roundcube/debian-db.php
chmod 640 /etc/roundcube/debian-db.php
# Initialize SQLite database if it doesn't exist
if [[ ! -f /var/lib/roundcube/roundcube.db ]]; then
mkdir -p /var/lib/roundcube
if [[ -f /usr/share/roundcube/SQL/sqlite.initial.sql ]]; then
sqlite3 /var/lib/roundcube/roundcube.db < /usr/share/roundcube/SQL/sqlite.initial.sql
log "Roundcube SQLite database initialized"
fi
fi
chown -R www-data:www-data /var/lib/roundcube
chmod 750 /var/lib/roundcube
chmod 640 /var/lib/roundcube/roundcube.db 2>/dev/null
fi
# Configure OpenDKIM for DKIM signing
info "Configuring OpenDKIM..."
mkdir -p /etc/opendkim/keys
chown -R opendkim:opendkim /etc/opendkim
chmod 750 /etc/opendkim
# Create OpenDKIM configuration files
touch /etc/opendkim/KeyTable
touch /etc/opendkim/SigningTable
cat > /etc/opendkim/TrustedHosts << 'TRUSTED'
127.0.0.1
localhost
::1
TRUSTED
chown opendkim:opendkim /etc/opendkim/KeyTable /etc/opendkim/SigningTable /etc/opendkim/TrustedHosts
chmod 644 /etc/opendkim/KeyTable /etc/opendkim/SigningTable /etc/opendkim/TrustedHosts
# Configure OpenDKIM socket in Postfix chroot
mkdir -p /var/spool/postfix/opendkim
chown opendkim:postfix /var/spool/postfix/opendkim
chmod 750 /var/spool/postfix/opendkim
# Update OpenDKIM configuration
if [[ -f /etc/opendkim.conf ]]; then
# Comment out the default Socket line (we'll add our own)
sed -i 's|^Socket.*local:/run/opendkim/opendkim.sock|#Socket local:/run/opendkim/opendkim.sock|' /etc/opendkim.conf
# Add Jabali configuration if not present
if ! grep -q "^KeyTable" /etc/opendkim.conf; then
cat >> /etc/opendkim.conf << 'OPENDKIM_CONF'
# Jabali Panel configuration
KeyTable /etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
InternalHosts /etc/opendkim/TrustedHosts
ExternalIgnoreList /etc/opendkim/TrustedHosts
Socket local:/var/spool/postfix/opendkim/opendkim.sock
OPENDKIM_CONF
fi
fi
# Configure Postfix to use OpenDKIM milter
postconf -e "smtpd_milters = unix:opendkim/opendkim.sock"
postconf -e "non_smtpd_milters = unix:opendkim/opendkim.sock"
postconf -e "milter_default_action = accept"
# Add postfix to opendkim group for socket access
usermod -aG opendkim postfix 2>/dev/null || true
systemctl enable opendkim
# Restart mail services to apply configuration
systemctl restart opendkim
systemctl restart dovecot
systemctl restart postfix
log "OpenDKIM configured"
log "Mail server configured with SQLite authentication"
}
# Create webmaster mailbox for system notifications
create_webmaster_mailbox() {
if [[ "$INSTALL_MAIL" != "true" ]]; then
info "Skipping webmaster mailbox creation (mail server not installed)"
return
fi
header "Creating Webmaster Mailbox"
local webmaster_password=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9!@#$%' | head -c 16)
# Extract root domain for email (e.g., panel.example.com -> example.com)
local dot_count=$(echo "$SERVER_HOSTNAME" | tr -cd '.' | wc -c)
local email_domain="$SERVER_HOSTNAME"
if [[ $dot_count -gt 1 ]]; then
email_domain=$(echo "$SERVER_HOSTNAME" | awk -F. '{print $(NF-1)"."$NF}')
fi
cd "$JABALI_DIR"
php artisan tinker --execute="
use App\Models\Domain;
use App\Models\EmailDomain;
use App\Models\Mailbox;
use App\Models\User;
use App\Services\Agent\AgentClient;
use Illuminate\Support\Facades\Crypt;
\$hostname = '${email_domain}';
\$password = '${webmaster_password}';
// Find admin user
\$admin = User::where('is_admin', true)->first();
if (!\$admin) {
echo 'No admin user found, skipping webmaster mailbox';
return;
}
// Create or find domain for hostname
\$domain = Domain::firstOrCreate(
['domain' => \$hostname],
['user_id' => \$admin->id, 'status' => 'active', 'document_root' => '/var/www/html']
);
// Enable email for domain if not already
\$agent = new AgentClient();
try {
\$agent->emailEnableDomain(\$admin->username, \$hostname);
} catch (Exception \$e) {
// Domain might already be enabled
}
// Create EmailDomain record
\$emailDomain = EmailDomain::firstOrCreate(
['domain_id' => \$domain->id],
['is_active' => true]
);
// Check if webmaster mailbox exists
if (Mailbox::where('email_domain_id', \$emailDomain->id)->where('local_part', 'webmaster')->exists()) {
echo 'Webmaster mailbox already exists';
return;
}
// Create mailbox via agent
\$result = \$agent->mailboxCreate(\$admin->username, 'webmaster@' . \$hostname, \$password, 1073741824);
Mailbox::create([
'email_domain_id' => \$emailDomain->id,
'user_id' => \$admin->id,
'local_part' => 'webmaster',
'password_hash' => \$result['password_hash'] ?? '',
'password_encrypted' => Crypt::encryptString(\$password),
'maildir_path' => \$result['maildir_path'] ?? null,
'system_uid' => \$result['uid'] ?? null,
'system_gid' => \$result['gid'] ?? null,
'name' => 'System Webmaster',
'quota_bytes' => 1073741824,
'is_active' => true,
]);
echo 'Webmaster mailbox created successfully';
" 2>/dev/null || true
log "Webmaster mailbox: webmaster@${SERVER_HOSTNAME}"
log "Webmaster password: ${webmaster_password}"
# Save credentials
echo "" >> /root/jabali_credentials.txt
echo "=== Webmaster Email ===" >> /root/jabali_credentials.txt
echo "Email: webmaster@${SERVER_HOSTNAME}" >> /root/jabali_credentials.txt
echo "Password: ${webmaster_password}" >> /root/jabali_credentials.txt
}
# Configure SSH login notifications via PAM
configure_ssh_notifications() {
header "Configuring SSH Login Notifications"
# Create PAM hook script in /etc/security
local pam_script="/etc/security/jabali-ssh-notify.sh"
cat > "$pam_script" << 'PAMSCRIPT'
#!/bin/bash
# Jabali SSH Login Notification Hook
# Called by PAM on successful SSH authentication
# Only run on successful authentication
if [ "$PAM_TYPE" != "open_session" ]; then
exit 0
fi
# Get the username and IP
USERNAME="$PAM_USER"
IP="${PAM_RHOST:-unknown}"
# Determine auth method
if [ -n "$SSH_AUTH_INFO_0" ]; then
case "$SSH_AUTH_INFO_0" in
publickey*) METHOD="publickey" ;;
password*) METHOD="password" ;;
keyboard-interactive*) METHOD="keyboard-interactive" ;;
*) METHOD="password" ;;
esac
else
METHOD="password"
fi
# Run the notification command in background
cd /var/www/jabali && /usr/bin/php artisan notify:ssh-login "$USERNAME" "$IP" --method="$METHOD" > /dev/null 2>&1 &
exit 0
PAMSCRIPT
chmod +x "$pam_script"
# Add to PAM sshd configuration if not already present
local pam_sshd="/etc/pam.d/sshd"
if ! grep -q "jabali-ssh-notify" "$pam_sshd" 2>/dev/null; then
echo "# Jabali SSH login notification" >> "$pam_sshd"
echo "session optional pam_exec.so quiet /etc/security/jabali-ssh-notify.sh" >> "$pam_sshd"
log "Added SSH login notification hook to PAM"
else
info "SSH login notification hook already configured"
fi
log "SSH login notifications configured"
}
# Configure DNS Zone for server hostname
configure_dns() {
header "Configuring DNS Server"
local server_ip=$(hostname -I | awk '{print $1}')
# Extract domain from hostname (e.g., panel.example.com -> example.com)
local domain=$(echo "$SERVER_HOSTNAME" | awk -F. '{if (NF>2) {print $(NF-1)"."$NF} else {print $0}}')
local hostname_part=$(echo "$SERVER_HOSTNAME" | sed "s/\.$domain$//")
# If hostname is same as domain (e.g., example.com), use @ for the host part
if [[ "$hostname_part" == "$domain" ]]; then
hostname_part="@"
fi
info "Setting up DNS zone for: $domain"
info "Server hostname: $SERVER_HOSTNAME"
# Create zones directory
mkdir -p /etc/bind/zones
# Generate serial (YYYYMMDD01 format)
local serial=$(date +%Y%m%d)01
# Create zone file
cat > /etc/bind/zones/db.$domain << ZONEFILE
\$TTL 3600
@ IN SOA ns1.$domain. admin.$domain. (
$serial ; Serial
3600 ; Refresh
1800 ; Retry
604800 ; Expire
86400 ) ; Negative Cache TTL
; Name servers
@ IN NS ns1.$domain.
@ IN NS ns2.$domain.
; A records
@ IN A $server_ip
ns1 IN A $server_ip
ns2 IN A $server_ip
ZONEFILE
# Add hostname A record if it's a subdomain
if [[ "$hostname_part" != "@" ]]; then
echo "$hostname_part IN A $server_ip" >> /etc/bind/zones/db.$domain
fi
# Add mail records
cat >> /etc/bind/zones/db.$domain << MAILRECORDS
; Mail records
@ IN MX 10 mail.$domain.
mail IN A $server_ip
; SPF record
@ IN TXT "v=spf1 mx a ip4:$server_ip ~all"
; DMARC record
_dmarc IN TXT "v=DMARC1; p=none; rua=mailto:admin@$domain"
; Autodiscover for mail clients
autoconfig IN A $server_ip
autodiscover IN A $server_ip
MAILRECORDS
# Set permissions
chown bind:bind /etc/bind/zones/db.$domain
chmod 644 /etc/bind/zones/db.$domain
# Add zone to named.conf.local if not already present
if ! grep -q "zone \"$domain\"" /etc/bind/named.conf.local 2>/dev/null; then
cat >> /etc/bind/named.conf.local << NAMEDCONF
zone "$domain" {
type master;
file "/etc/bind/zones/db.$domain";
allow-transfer { none; };
};
NAMEDCONF
fi
# Configure BIND9 options for better security
if [[ ! -f /etc/bind/named.conf.options.bak ]]; then
cp /etc/bind/named.conf.options /etc/bind/named.conf.options.bak
cat > /etc/bind/named.conf.options << 'BINDOPTIONS'
options {
directory "/var/cache/bind";
// Forward queries to public DNS if not authoritative
forwarders {
8.8.8.8;
8.8.4.4;
};
// Security settings
dnssec-validation auto;
auth-nxdomain no;
listen-on-v6 { any; };
// Allow queries from anywhere (for authoritative zones)
allow-query { any; };
// Disable recursion for security (authoritative only)
recursion no;
};
BINDOPTIONS
fi
# Enable and restart BIND9
systemctl enable named 2>/dev/null || systemctl enable bind9 2>/dev/null || true
systemctl restart named 2>/dev/null || systemctl restart bind9 2>/dev/null || true
log "DNS zone created for $domain"
info "Zone file: /etc/bind/zones/db.$domain"
echo ""
echo -e "${YELLOW}Important DNS Setup:${NC}"
echo "Point your domain's nameservers to this server:"
echo " ns1.$domain -> $server_ip"
echo " ns2.$domain -> $server_ip"
echo ""
echo "Or add a glue record at your registrar for ns1.$domain"
echo ""
}
# Setup Disk Quotas
setup_quotas() {
header "Setting Up Disk Quotas"
# Check if quotas are already enabled on root filesystem
if mount | grep ' / ' | grep -q 'usrquota'; then
log "Quotas already enabled on root filesystem"
return 0
fi
# Get the root filesystem device
local root_dev=$(findmnt -n -o SOURCE /)
local root_mount=$(findmnt -n -o TARGET /)
if [[ -z "$root_dev" ]]; then
warn "Could not determine root filesystem device"
return 1
fi
info "Enabling quotas on $root_dev ($root_mount)..."
# Check filesystem type
local fs_type=$(findmnt -n -o FSTYPE /)
if [[ "$fs_type" != "ext4" && "$fs_type" != "ext3" && "$fs_type" != "xfs" ]]; then
warn "Quotas not supported on $fs_type filesystem"
return 1
fi
# For ext4, try to enable quota feature directly (modern approach)
if [[ "$fs_type" == "ext4" ]]; then
# Check if quota feature is available
if tune2fs -l "$root_dev" 2>/dev/null | grep -q "quota"; then
info "Using ext4 native quota feature..."
fi
fi
# Add quota options to fstab if not present
if ! grep -E "^\s*$root_dev|^\s*UUID=" /etc/fstab | grep -q 'usrquota'; then
info "Adding quota options to /etc/fstab..."
# Backup fstab
cp /etc/fstab /etc/fstab.backup.$(date +%Y%m%d_%H%M%S)
# Add quota options to root mount
# Use # as delimiter since | is used as regex OR and $root_dev contains /
sed -i -E "s#^([^#]*\s+/\s+\S+\s+)(\S+)#\1\2,usrquota,grpquota#" /etc/fstab
# If sed didn't change anything (different fstab format), try another approach
if ! grep ' / ' /etc/fstab | grep -q 'usrquota'; then
# Try to add to defaults
sed -i -E "s#^(\s*\S+\s+/\s+\S+\s+)defaults#\1defaults,usrquota,grpquota#" /etc/fstab
fi
fi
# Remount with quota options
info "Remounting root filesystem with quota options..."
mount -o remount,usrquota,grpquota / 2>/dev/null || {
warn "Could not remount with quotas - may require reboot"
}
# Check if quota tools work
if mount | grep ' / ' | grep -q 'usrquota'; then
# Create quota files if needed
quotacheck -avugm 2>/dev/null || true
# Enable quotas
quotaon -avug 2>/dev/null || true
if quotaon -p / 2>/dev/null | grep -q "is on"; then
log "Disk quotas enabled successfully"
else
warn "Quotas configured but may require reboot to activate"
fi
else
warn "Quota options not applied - may require reboot"
fi
}
# Configure Firewall
configure_firewall() {
header "Configuring Firewall"
if ! command -v ufw &> /dev/null; then
warn "UFW not found, skipping firewall configuration"
return
fi
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp # SSH
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
# Mail ports (only if mail server is being installed)
if [[ "$INSTALL_MAIL" == "true" ]]; then
ufw allow 25/tcp # SMTP
ufw allow 465/tcp # SMTPS (SMTP over SSL)
ufw allow 587/tcp # Submission (SMTP with STARTTLS)
ufw allow 110/tcp # POP3
ufw allow 143/tcp # IMAP
ufw allow 993/tcp # IMAPS (IMAP over SSL)
ufw allow 995/tcp # POP3S (POP3 over SSL)
fi
# DNS (only if DNS server is being installed)
if [[ "$INSTALL_DNS" == "true" ]]; then
ufw allow 53/tcp # DNS
ufw allow 53/udp # DNS
fi
ufw --force enable
log "Firewall configured"
}
# Configure Security Tools (Fail2ban and ClamAV)
configure_security() {
header "Configuring Security Tools"
# Install ModSecurity + CRS (optional)
if [[ "$INSTALL_SECURITY" == "true" ]]; then
info "Installing ModSecurity (optional WAF)..."
local module_pkg=""
if apt-cache show libnginx-mod-http-modsecurity &>/dev/null; then
module_pkg="libnginx-mod-http-modsecurity"
elif apt-cache show libnginx-mod-http-modsecurity2 &>/dev/null; then
module_pkg="libnginx-mod-http-modsecurity2"
elif apt-cache show nginx-extras &>/dev/null; then
module_pkg="nginx-extras"
else
warn "ModSecurity nginx module not available in apt repositories"
fi
local modsec_lib=""
if apt-cache show libmodsecurity3t64 &>/dev/null; then
modsec_lib="libmodsecurity3t64"
elif apt-cache show libmodsecurity3 &>/dev/null; then
modsec_lib="libmodsecurity3"
fi
local crs_pkg=""
if apt-cache show modsecurity-crs &>/dev/null; then
crs_pkg="modsecurity-crs"
fi
if [[ -n "$module_pkg" ]]; then
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "$module_pkg" $modsec_lib $crs_pkg 2>/dev/null || warn "ModSecurity install failed"
# Ensure ModSecurity base config
if [[ ! -f /etc/modsecurity/modsecurity.conf ]]; then
if [[ -f /etc/nginx/modsecurity.conf ]]; then
cp /etc/nginx/modsecurity.conf /etc/modsecurity/modsecurity.conf
elif [[ -f /etc/modsecurity/modsecurity.conf-recommended ]]; then
cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
elif [[ -f /usr/share/modsecurity-crs/modsecurity.conf-recommended ]]; then
cp /usr/share/modsecurity-crs/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
else
cat > /etc/modsecurity/modsecurity.conf <<'EOF'
SecRuleEngine DetectionOnly
SecRequestBodyAccess On
SecResponseBodyAccess Off
SecAuditEngine RelevantOnly
SecAuditLog /var/log/nginx/modsec_audit.log
EOF
fi
fi
# Create main include file for nginx if missing
mkdir -p /etc/nginx/modsec
if [[ ! -f /etc/nginx/modsec/main.conf ]]; then
if [[ -f /usr/share/modsecurity-crs/owasp-crs.load ]]; then
cat > /etc/nginx/modsec/main.conf <<'EOF'
Include /etc/modsecurity/modsecurity.conf
Include /usr/share/modsecurity-crs/owasp-crs.load
EOF
elif [[ -f /etc/modsecurity/crs/crs-setup.conf ]]; then
cat > /etc/nginx/modsec/main.conf <<'EOF'
Include /etc/modsecurity/modsecurity.conf
Include /etc/modsecurity/crs/crs-setup.conf
Include /usr/share/modsecurity-crs/rules/*.conf
EOF
else
cat > /etc/nginx/modsec/main.conf <<'EOF'
Include /etc/modsecurity/modsecurity.conf
EOF
fi
fi
fi
fi
# Configure Fail2ban
info "Configuring Fail2ban..."
cat > /etc/fail2ban/jail.local << 'FAIL2BAN'
[DEFAULT]
bantime = 600
findtime = 600
maxretry = 5
ignoreip = 127.0.0.1/8 ::1
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
[nginx-botsearch]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /var/log/nginx/access.log
maxretry = 2
FAIL2BAN
# Create WordPress filter and jail
cat > /etc/fail2ban/filter.d/wordpress.conf << 'WPFILTER'
[Definition]
failregex = ^<HOST> -.*"POST.*/wp-login\.php.*" (200|403)
^<HOST> -.*"POST.*/xmlrpc\.php.*" (200|403)
ignoreregex = action=logout
WPFILTER
cat > /etc/fail2ban/jail.d/wordpress.conf << 'WPJAIL'
[wordpress]
enabled = false
filter = wordpress
port = http,https
logpath = /home/*/domains/*/logs/access.log
/var/log/nginx/access.log
maxretry = 5
findtime = 300
bantime = 3600
WPJAIL
# Create mail service jails (disabled by default, enabled when mail is configured)
cat > /etc/fail2ban/jail.d/dovecot.conf << 'DOVECOTJAIL'
[dovecot]
enabled = false
filter = dovecot
backend = systemd
maxretry = 5
findtime = 300
bantime = 3600
DOVECOTJAIL
cat > /etc/fail2ban/jail.d/postfix.conf << 'POSTFIXJAIL'
[postfix]
enabled = false
filter = postfix
mode = more
backend = systemd
maxretry = 5
findtime = 300
bantime = 3600
[postfix-sasl]
enabled = false
filter = postfix[mode=auth]
backend = systemd
maxretry = 3
findtime = 300
bantime = 3600
POSTFIXJAIL
cat > /etc/fail2ban/jail.d/roundcube.conf << 'RCJAIL'
[roundcube-auth]
enabled = false
filter = roundcube-auth
port = http,https
logpath = /var/lib/roundcube/logs/errors.log
maxretry = 5
findtime = 300
bantime = 3600
RCJAIL
systemctl enable fail2ban
systemctl restart fail2ban
log "Fail2ban configured and enabled"
# Configure SSH Jail for users
info "Configuring SSH/SFTP jail..."
# Create groups for SFTP and shell users
groupadd sftpusers 2>/dev/null || true
groupadd shellusers 2>/dev/null || true
# Backup original sshd_config
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup.$(date +%Y%m%d%H%M%S)
# Remove any existing Jabali SSH config
sed -i '/# Jabali SSH Jail Configuration/,/# End Jabali SSH Jail/d' /etc/ssh/sshd_config
# Add SSH jail configuration
cat >> /etc/ssh/sshd_config << 'SSHJAIL'
# Jabali SSH Jail Configuration
# SFTP-only users (default for all panel users)
Match Group sftpusers
ChrootDirectory /home/%u
ForceCommand internal-sftp
AllowTcpForwarding no
AllowAgentForwarding no
PermitTTY no
X11Forwarding no
# Shell users (jailed with limited commands)
Match Group shellusers
ChrootDirectory /var/jail
AllowTcpForwarding no
AllowAgentForwarding no
X11Forwarding no
# End Jabali SSH Jail
SSHJAIL
# Restart SSH to apply changes
systemctl restart sshd
log "SSH jail configured"
# Set up jail environment for shell users
info "Setting up jail environment..."
# Create jail directory structure
mkdir -p /var/jail/{bin,lib,lib64,usr,etc,dev,home}
mkdir -p /var/jail/usr/{bin,lib,share}
mkdir -p /var/jail/usr/lib/x86_64-linux-gnu
# Copy essential binaries to jail
JAIL_BINS="bash sh ls cat cp mv rm mkdir rmdir pwd echo head tail grep sed awk wc sort uniq cut tr touch chmod find which"
for bin in $JAIL_BINS; do
if [ -f "/bin/$bin" ]; then
cp /bin/$bin /var/jail/bin/ 2>/dev/null || true
elif [ -f "/usr/bin/$bin" ]; then
cp /usr/bin/$bin /var/jail/usr/bin/ 2>/dev/null || true
fi
done
# Copy wp-cli if available
if [ -f "/usr/local/bin/wp" ]; then
cp /usr/local/bin/wp /var/jail/usr/bin/wp
fi
# Copy PHP for wp-cli support
if [ -f "/usr/bin/php" ]; then
cp /usr/bin/php /var/jail/usr/bin/
fi
# Copy env (needed by wp-cli shebang)
if [ -f "/usr/bin/env" ]; then
cp /usr/bin/env /var/jail/usr/bin/
fi
# Copy required libraries for binaries
copy_libs_for_binary() {
local binary="$1"
if [ -f "$binary" ]; then
ldd "$binary" 2>/dev/null | grep -o '/[^ ]*' | while read lib; do
if [ -f "$lib" ]; then
local libdir=$(dirname "$lib")
mkdir -p "/var/jail$libdir"
cp -n "$lib" "/var/jail$libdir/" 2>/dev/null || true
fi
done
fi
}
for bin in /var/jail/bin/* /var/jail/usr/bin/*; do
[ -f "$bin" ] && copy_libs_for_binary "$bin"
done
# Copy PHP extensions for wp-cli
PHP_EXT_DIR=$(php -i 2>/dev/null | grep "^extension_dir" | awk '{print $3}')
if [ -d "$PHP_EXT_DIR" ]; then
mkdir -p "/var/jail$PHP_EXT_DIR"
cp "$PHP_EXT_DIR"/*.so "/var/jail$PHP_EXT_DIR/" 2>/dev/null || true
# Copy extension library dependencies
for ext in "$PHP_EXT_DIR"/*.so; do
[ -f "$ext" ] && copy_libs_for_binary "$ext"
done
fi
# Copy PHP CLI configuration (resolve symlinks to actual files)
mkdir -p /var/jail/etc/php/8.4/cli/conf.d
cp /etc/php/8.4/cli/php.ini /var/jail/etc/php/8.4/cli/ 2>/dev/null || true
# Set timezone in jail's php.ini to avoid warnings
sed -i 's/;date.timezone =/date.timezone = UTC/' /var/jail/etc/php/8.4/cli/php.ini 2>/dev/null || true
for f in /etc/php/8.4/cli/conf.d/*.ini; do
if [ -L "$f" ]; then
# Resolve symlink and copy actual file
cp "$(readlink -f "$f")" "/var/jail/etc/php/8.4/cli/conf.d/$(basename "$f")" 2>/dev/null || true
elif [ -f "$f" ]; then
cp "$f" "/var/jail/etc/php/8.4/cli/conf.d/" 2>/dev/null || true
fi
done
# Create essential device nodes
mknod -m 666 /var/jail/dev/null c 1 3 2>/dev/null || true
mknod -m 666 /var/jail/dev/zero c 1 5 2>/dev/null || true
mknod -m 666 /var/jail/dev/random c 1 8 2>/dev/null || true
mknod -m 666 /var/jail/dev/urandom c 1 9 2>/dev/null || true
mknod -m 666 /var/jail/dev/tty c 5 0 2>/dev/null || true
# Create minimal /etc files
grep -E "^(root|nobody)" /etc/passwd > /var/jail/etc/passwd
grep -E "^(root|nogroup)" /etc/group > /var/jail/etc/group
cp /etc/nsswitch.conf /var/jail/etc/ 2>/dev/null || true
cp /etc/hosts /var/jail/etc/ 2>/dev/null || true
# Copy timezone data for PHP
mkdir -p /var/jail/usr/share/zoneinfo
cp -r /usr/share/zoneinfo/* /var/jail/usr/share/zoneinfo/ 2>/dev/null || true
ln -sf /usr/share/zoneinfo/UTC /var/jail/etc/localtime 2>/dev/null || true
# Set permissions
chown root:root /var/jail
chmod 755 /var/jail
log "Jail environment configured with wp-cli support"
# Configure ClamAV only if INSTALL_SECURITY is enabled
if [[ "$INSTALL_SECURITY" != "true" ]]; then
log "Skipping ClamAV configuration (security tools not selected)"
return
fi
# Configure ClamAV (disabled by default to save memory)
info "Configuring ClamAV (disabled by default)..."
# Create quarantine directory
mkdir -p /var/lib/clamav/quarantine
chown clamav:clamav /var/lib/clamav/quarantine
chmod 750 /var/lib/clamav/quarantine
# Create optimized ClamAV configuration
cat > /etc/clamav/clamd.conf << 'CLAMAV_CONF'
# Jabali ClamAV Configuration - Optimized for low resource usage
LocalSocket /var/run/clamav/clamd.ctl
FixStaleSocket true
LocalSocketGroup clamav
LocalSocketMode 666
User clamav
LogFile /var/log/clamav/clamav.log
LogRotate true
LogTime true
DatabaseDirectory /var/lib/clamav
MaxThreads 2
MaxScanSize 25M
MaxFileSize 5M
MaxRecursion 10
ScanArchive false
ScanPE true
ScanELF true
ScanHTML true
AlgorithmicDetection true
PhishingSignatures true
Foreground false
ExitOnOOM true
CLAMAV_CONF
# Configure freshclam
cat > /etc/clamav/freshclam.conf << 'FRESHCLAM_CONF'
DatabaseDirectory /var/lib/clamav
UpdateLogFile /var/log/clamav/freshclam.log
LogRotate true
LogTime true
DatabaseMirror database.clamav.net
NotifyClamd /etc/clamav/clamd.conf
Checks 4
FRESHCLAM_CONF
# Stop ClamAV services - keep them disabled by default
systemctl stop clamav-daemon 2>/dev/null || true
systemctl stop clamav-freshclam 2>/dev/null || true
systemctl disable clamav-daemon 2>/dev/null || true
systemctl disable clamav-freshclam 2>/dev/null || true
# Update signatures once during install
info "Downloading initial virus signatures..."
freshclam --quiet 2>/dev/null || warn "Could not download virus signatures"
log "ClamAV configured (disabled by default to save memory)"
info "Enable ClamAV via Security Center in Admin Panel when needed"
}
# Configure Redis with ACL
configure_redis() {
header "Configuring Redis"
# Generate random admin password
REDIS_ADMIN_PASSWORD=$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 32)
# Save credentials for later use
cat > /root/.jabali_redis_credentials << CREDS
REDIS_ADMIN_PASSWORD=${REDIS_ADMIN_PASSWORD}
CREDS
chmod 600 /root/.jabali_redis_credentials
# Configure Redis with ACL
cat > /etc/redis/redis.conf << 'REDIS_CONF'
# Redis Configuration for Jabali Panel
# Network
bind 127.0.0.1
port 6379
protected-mode yes
# General
daemonize yes
pidfile /var/run/redis/redis-server.pid
loglevel notice
logfile /var/log/redis/redis-server.log
# Persistence
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /var/lib/redis
# Memory management
maxmemory 256mb
maxmemory-policy allkeys-lru
# ACL - Disable default user, require authentication
aclfile /etc/redis/users.acl
# Clients
timeout 0
tcp-keepalive 300
REDIS_CONF
# Create ACL file with admin user (no comments allowed in Redis 8 ACL files)
cat > /etc/redis/users.acl << ACL_CONF
user default off
user jabali_admin on >${REDIS_ADMIN_PASSWORD} ~* &* +@all
ACL_CONF
# Remove any comments or empty lines (Redis 8 doesn't allow them)
sed -i '/^#/d; /^$/d' /etc/redis/users.acl
chmod 640 /etc/redis/users.acl
chown redis:redis /etc/redis/users.acl
# Restart Redis
systemctl restart redis-server
# Check if Redis started successfully
if ! systemctl is-active --quiet redis-server; then
error "Redis failed to start. Check /var/log/redis/redis-server.log"
exit 1
fi
log "Redis configured with ACL authentication"
}
# Setup Jabali Panel
setup_jabali() {
header "Setting Up Jabali Panel"
cd "$JABALI_DIR"
# Load database credentials
source /root/.jabali_db_credentials
# Load Redis credentials
source /root/.jabali_redis_credentials
# Create .env file
cp .env.example .env 2>/dev/null || cat > .env << ENV
APP_NAME=Jabali
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=https://$(hostname -I | awk '{print $1}')
LOG_CHANNEL=stack
LOG_LEVEL=error
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=jabali
DB_USERNAME=jabali
DB_PASSWORD=${DB_PASSWORD}
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_USERNAME=jabali_admin
REDIS_PASSWORD=${REDIS_ADMIN_PASSWORD}
REDIS_PORT=6379
MAIL_MAILER=sendmail
MAIL_FROM_ADDRESS=webmaster@${SERVER_HOSTNAME}
MAIL_FROM_NAME="Jabali Panel"
ENV
# Ensure mail settings are correct (in case .env.example was used)
sed -i "s/^MAIL_MAILER=.*/MAIL_MAILER=sendmail/" .env
sed -i "s/^MAIL_FROM_ADDRESS=.*/MAIL_FROM_ADDRESS=webmaster@${SERVER_HOSTNAME}/" .env
# Install dependencies
log "Installing Composer dependencies..."
sudo -u $JABALI_USER COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader
if [ ! -f "$JABALI_DIR/vendor/autoload.php" ]; then
error "Composer install failed - vendor/autoload.php not found"
exit 1
fi
# Set storage permissions BEFORE running artisan commands
# This prevents files being created as root and becoming inaccessible to www-data
log "Setting storage permissions..."
mkdir -p "$JABALI_DIR/storage/logs"
mkdir -p "$JABALI_DIR/storage/framework/cache/data"
mkdir -p "$JABALI_DIR/storage/framework/sessions"
mkdir -p "$JABALI_DIR/storage/framework/views"
mkdir -p "$JABALI_DIR/storage/app/public"
mkdir -p "$JABALI_DIR/bootstrap/cache"
touch "$JABALI_DIR/storage/logs/laravel.log"
chown -R www-data:www-data "$JABALI_DIR/storage"
chown -R www-data:www-data "$JABALI_DIR/bootstrap/cache"
chmod -R 775 "$JABALI_DIR/storage"
chmod -R 775 "$JABALI_DIR/bootstrap/cache"
# Generate app key
php artisan key:generate --force
# Run migrations
php artisan migrate --force
# Create storage symlink for public files (screenshots, etc.)
php artisan storage:link --force 2>/dev/null || true
# Publish and configure Livewire for large file uploads
php artisan livewire:publish --config 2>/dev/null || php artisan vendor:publish --tag=livewire:config 2>/dev/null || true
if [[ -f "$JABALI_DIR/config/livewire.php" ]]; then
sed -i "s/'rules' => null,/'rules' => ['required', 'file', 'max:524288'],/" "$JABALI_DIR/config/livewire.php"
sed -i "s/'max_upload_time' => 5,/'max_upload_time' => 15,/" "$JABALI_DIR/config/livewire.php"
fi
# Configure DNS settings with server hostname and IP
local server_ip=$(hostname -I | awk '{print $1}')
# Extract root domain from hostname (e.g., panel.example.com -> example.com)
# Count the number of dots to determine if it's a subdomain
local dot_count=$(echo "$SERVER_HOSTNAME" | tr -cd '.' | wc -c)
if [[ $dot_count -gt 1 ]]; then
# It's a subdomain - extract root domain (last two parts)
local root_domain=$(echo "$SERVER_HOSTNAME" | awk -F. '{print $(NF-1)"."$NF}')
local subdomain_part=$(echo "$SERVER_HOSTNAME" | sed "s/\.$root_domain$//")
log "Detected subdomain installation: $subdomain_part.$root_domain"
else
# It's already a root domain
local root_domain="$SERVER_HOSTNAME"
local subdomain_part=""
fi
log "Configuring DNS settings for ${SERVER_HOSTNAME} (root: ${root_domain}, IP: ${server_ip})..."
php artisan tinker --execute="
use App\Models\DnsSetting;
DnsSetting::set('ns1', 'ns1.${root_domain}');
DnsSetting::set('ns2', 'ns2.${root_domain}');
DnsSetting::set('ns1_ip', '${server_ip}');
DnsSetting::set('ns2_ip', '${server_ip}');
DnsSetting::set('default_ip', '${server_ip}');
DnsSetting::set('admin_email', 'admin.${root_domain}');
DnsSetting::set('default_ttl', '3600');
" 2>/dev/null || true
# Create DNS zone for root domain
log "Creating DNS zone for ${root_domain}..."
# Build records array - include subdomain A record if installing on subdomain
local records_json="[
{\"name\": \"@\", \"type\": \"NS\", \"content\": \"ns1.${root_domain}\", \"ttl\": 3600},
{\"name\": \"@\", \"type\": \"NS\", \"content\": \"ns2.${root_domain}\", \"ttl\": 3600},
{\"name\": \"@\", \"type\": \"A\", \"content\": \"${server_ip}\", \"ttl\": 3600},
{\"name\": \"www\", \"type\": \"A\", \"content\": \"${server_ip}\", \"ttl\": 3600},
{\"name\": \"ns1\", \"type\": \"A\", \"content\": \"${server_ip}\", \"ttl\": 3600},
{\"name\": \"ns2\", \"type\": \"A\", \"content\": \"${server_ip}\", \"ttl\": 3600},
{\"name\": \"mail\", \"type\": \"A\", \"content\": \"${server_ip}\", \"ttl\": 3600},
{\"name\": \"@\", \"type\": \"MX\", \"content\": \"mail.${root_domain}\", \"ttl\": 3600, \"priority\": 10},
{\"name\": \"@\", \"type\": \"TXT\", \"content\": \"v=spf1 mx a ~all\", \"ttl\": 3600},
{\"name\": \"_dmarc\", \"type\": \"TXT\", \"content\": \"v=DMARC1; p=none; rua=mailto:postmaster@${root_domain}\", \"ttl\": 3600}"
# Add subdomain A record if installing on a subdomain
if [[ -n "$subdomain_part" ]]; then
records_json="${records_json},
{\"name\": \"${subdomain_part}\", \"type\": \"A\", \"content\": \"${server_ip}\", \"ttl\": 3600}"
fi
records_json="${records_json}]"
php artisan tinker --execute="
\$agent = new App\Services\Agent\AgentClient();
\$agent->send('dns.sync_zone', [
'domain' => '${root_domain}',
'records' => json_decode('${records_json}', true),
'ns1' => 'ns1.${root_domain}',
'ns2' => 'ns2.${root_domain}',
'admin_email' => 'admin.${root_domain}',
'default_ip' => '${server_ip}',
'default_ttl' => 3600,
]);
" 2>/dev/null || true
# Build assets
npm install
npm run build
# Final permissions - ensure everything is correct after all setup
chown -R $JABALI_USER:www-data "$JABALI_DIR"
chown -R www-data:www-data "$JABALI_DIR/storage"
chown -R www-data:www-data "$JABALI_DIR/bootstrap/cache"
chmod -R 775 "$JABALI_DIR/storage"
chmod -R 775 "$JABALI_DIR/bootstrap/cache"
# Set SQLite database permissions for Dovecot access (if mail server is installed)
if [[ "$INSTALL_MAIL" == "true" ]] && [[ -f "$JABALI_DIR/database/database.sqlite" ]]; then
info "Setting SQLite database permissions for mail server..."
chmod 664 "$JABALI_DIR/database/database.sqlite"
chmod 775 "$JABALI_DIR/database"
fi
# Create CLI symlink
ln -sf "$JABALI_DIR/bin/jabali" /usr/local/bin/jabali
chmod +x "$JABALI_DIR/bin/jabali"
chmod +x "$JABALI_DIR/bin/jabali-agent"
# Update version file
cat > "$JABALI_DIR/VERSION" << EOF
VERSION=${JABALI_VERSION}
EOF
chown $JABALI_USER:$JABALI_USER "$JABALI_DIR/VERSION"
# Create agent directories
mkdir -p /var/run/jabali
mkdir -p /var/log/jabali
chown $JABALI_USER:$JABALI_USER /var/run/jabali
chown $JABALI_USER:$JABALI_USER /var/log/jabali
# Create backup directories
mkdir -p /var/backups/jabali
mkdir -p /var/backups/jabali/cpanel-migrations
mkdir -p /var/backups/jabali/whm-migrations
chown -R $JABALI_USER:$JABALI_USER /var/backups/jabali
chmod 755 /var/backups/jabali /var/backups/jabali/cpanel-migrations /var/backups/jabali/whm-migrations
log "Jabali Panel setup complete"
}
# Create systemd service for agent
setup_agent_service() {
header "Setting Up Jabali Agent Service"
cat > /etc/systemd/system/jabali-agent.service << 'SERVICE'
[Unit]
Description=Jabali Panel Agent
After=network.target
[Service]
Type=simple
User=root
Group=root
ExecStart=/usr/bin/php /var/www/jabali/bin/jabali-agent
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
SERVICE
systemctl daemon-reload
systemctl enable jabali-agent
systemctl start jabali-agent
log "Jabali Agent service configured"
}
setup_queue_service() {
header "Setting Up Jabali Queue Worker"
cat > /etc/systemd/system/jabali-queue.service << 'SERVICE'
[Unit]
Description=Jabali Queue Worker
After=network.target jabali-agent.service
Wants=jabali-agent.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/jabali
ExecStart=/usr/bin/php /var/www/jabali/artisan queue:work --sleep=3 --tries=1 --timeout=120
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=jabali-queue
[Install]
WantedBy=multi-user.target
SERVICE
systemctl daemon-reload
systemctl enable jabali-queue
systemctl start jabali-queue
log "Jabali Queue Worker service configured"
}
# Setup Laravel scheduler cron job
setup_scheduler_cron() {
header "Setting Up Laravel Scheduler"
# Create log directory if it doesn't exist
mkdir -p "$JABALI_DIR/storage/logs"
chown -R www-data:www-data "$JABALI_DIR/storage/logs"
# Add cron job for Laravel scheduler as www-data (runs every minute)
CRON_LINE="* * * * * cd $JABALI_DIR && php artisan schedule:run >> /dev/null 2>&1"
# Add to www-data's crontab (not root) to avoid permission issues with log files
if ! sudo -u www-data crontab -l 2>/dev/null | grep -q "artisan schedule:run"; then
(sudo -u www-data crontab -l 2>/dev/null; echo "$CRON_LINE") | sudo -u www-data crontab -
log "Laravel scheduler cron job added"
else
log "Laravel scheduler cron job already exists"
fi
log "Laravel scheduler configured - SSL auto-renewal and backups will run automatically"
}
# Setup logrotate for user domain logs
setup_logrotate() {
header "Setting Up Log Rotation"
cat > /etc/logrotate.d/jabali-users << 'LOGROTATE'
# Logrotate configuration for Jabali Panel user domain logs
/home/*/domains/*/logs/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0644 root root
sharedscripts
postrotate
invoke-rc.d nginx rotate >/dev/null 2>&1
endscript
}
LOGROTATE
log "Log rotation configured for user domain logs"
}
# Setup SSL certificates for panel and mail services
setup_panel_ssl() {
header "Setting Up SSL Certificates for Services"
# Get public IP (try external service first, fall back to hostname -I)
local server_ip=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null || curl -s --max-time 5 https://ipv4.icanhazip.com 2>/dev/null || hostname -I | awk '{print $1}')
server_ip=$(echo "$server_ip" | tr -d '[:space:]')
# Check if hostname resolves to this server (required for Let's Encrypt)
local resolved_ip=$(dig +short "$SERVER_HOSTNAME" 2>/dev/null | head -1)
if [[ -z "$resolved_ip" ]]; then
warn "Cannot resolve $SERVER_HOSTNAME - skipping Let's Encrypt"
warn "SSL can be issued later using: certbot --nginx -d $SERVER_HOSTNAME"
return
fi
if [[ "$resolved_ip" != "$server_ip" ]]; then
warn "Hostname $SERVER_HOSTNAME resolves to $resolved_ip, not this server ($server_ip)"
warn "SSL can be issued later using: certbot --nginx -d $SERVER_HOSTNAME"
return
fi
info "Attempting to issue Let's Encrypt certificate for $SERVER_HOSTNAME"
# Issue certificate and configure nginx automatically
if certbot --nginx -d "$SERVER_HOSTNAME" --non-interactive --agree-tos --email "$ADMIN_EMAIL" --redirect 2>&1; then
log "Let's Encrypt certificate issued and nginx configured for $SERVER_HOSTNAME"
export PANEL_SSL_INSTALLED=true
else
warn "Could not issue Let's Encrypt certificate for $SERVER_HOSTNAME"
warn "This may be because the domain is not publicly accessible"
warn "You can issue manually later: certbot --nginx -d $SERVER_HOSTNAME"
fi
# Try to issue certificate for mail hostname if different
local mail_hostname="mail.$(echo "$SERVER_HOSTNAME" | awk -F. '{if(NF>2){for(i=2;i<=NF;i++)printf "%s%s",$i,(i<NF?".":"")}else print $0}')"
if [[ "$mail_hostname" != "mail." && "$mail_hostname" != "$SERVER_HOSTNAME" ]]; then
local mail_resolved=$(dig +short "$mail_hostname" 2>/dev/null | head -1)
if [[ "$mail_resolved" == "$server_ip" ]]; then
info "Attempting to issue Let's Encrypt certificate for $mail_hostname"
if certbot certonly --standalone -d "$mail_hostname" --non-interactive --agree-tos --email "$ADMIN_EMAIL" 2>/dev/null; then
log "Let's Encrypt certificate issued for $mail_hostname"
# Update Postfix to use the certificate
postconf -e "smtpd_tls_cert_file=/etc/letsencrypt/live/$mail_hostname/fullchain.pem"
postconf -e "smtpd_tls_key_file=/etc/letsencrypt/live/$mail_hostname/privkey.pem"
systemctl reload postfix 2>/dev/null || true
# Update Dovecot to use the certificate
if [[ -f /etc/dovecot/conf.d/10-ssl.conf ]]; then
sed -i "s|^ssl_cert = .*|ssl_cert = </etc/letsencrypt/live/$mail_hostname/fullchain.pem|" /etc/dovecot/conf.d/10-ssl.conf
sed -i "s|^ssl_key = .*|ssl_key = </etc/letsencrypt/live/$mail_hostname/privkey.pem|" /etc/dovecot/conf.d/10-ssl.conf
systemctl reload dovecot 2>/dev/null || true
fi
log "Mail services updated to use Let's Encrypt certificate"
else
warn "Could not issue certificate for $mail_hostname"
fi
fi
fi
log "SSL setup complete"
}
# Setup self-healing services (automatic restart on failure)
setup_self_healing() {
header "Setting Up Self-Healing Services"
# List of critical services to harden with restart policies
local services=(
"nginx"
"mariadb"
"jabali-agent"
"jabali-queue"
)
# Add PHP-FPM (detect version)
for version in 8.4 8.3 8.2 8.1 8.0; do
if systemctl list-unit-files "php${version}-fpm.service" &>/dev/null | grep -q "php${version}-fpm"; then
services+=("php${version}-fpm")
break
fi
done
# Add optional services if installed
if systemctl list-unit-files postfix.service &>/dev/null | grep -q postfix; then
services+=("postfix")
fi
if systemctl list-unit-files dovecot.service &>/dev/null | grep -q dovecot; then
services+=("dovecot")
fi
if systemctl list-unit-files named.service &>/dev/null | grep -q named; then
services+=("named")
elif systemctl list-unit-files bind9.service &>/dev/null | grep -q bind9; then
services+=("bind9")
fi
if systemctl list-unit-files redis-server.service &>/dev/null | grep -q redis-server; then
services+=("redis-server")
fi
if systemctl list-unit-files fail2ban.service &>/dev/null | grep -q fail2ban; then
services+=("fail2ban")
fi
# Create systemd override directory and restart policy for each service
for service in "${services[@]}"; do
local override_dir="/etc/systemd/system/${service}.service.d"
mkdir -p "$override_dir"
cat > "${override_dir}/restart.conf" << 'OVERRIDE'
[Unit]
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Restart=always
RestartSec=5
OVERRIDE
log "Added restart policy for $service"
done
# Reload systemd to apply overrides
systemctl daemon-reload
# Setup health monitor service
cat > /etc/systemd/system/jabali-health-monitor.service << 'SERVICE'
[Unit]
Description=Jabali Health Monitor - Automatic service recovery
Documentation=https://github.com/shukiv/jabali-panel
After=network.target jabali-agent.service
Wants=jabali-agent.service
[Service]
Type=simple
User=root
Group=root
ExecStart=/usr/bin/php /var/www/jabali/bin/jabali-health-monitor
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=jabali-health-monitor
# Resource limits
LimitNOFILE=65535
MemoryMax=128M
[Install]
WantedBy=multi-user.target
SERVICE
# Enable and start health monitor
systemctl daemon-reload
systemctl enable jabali-health-monitor
systemctl start jabali-health-monitor
log "Self-healing services configured"
log "Health monitor running - check /var/log/jabali/health-monitor.log for events"
}
# Create admin user
create_admin() {
header "Creating Admin User"
cd "$JABALI_DIR"
# Generate admin password and export for print_completion
export ADMIN_PASSWORD=$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 16)
php artisan tinker --execute="
\$user = App\Models\User::create([
'name' => 'Administrator',
'username' => 'admin',
'email' => '${ADMIN_EMAIL}',
'password' => bcrypt('${ADMIN_PASSWORD}'),
'is_admin' => true,
]);
" 2>/dev/null || true
# Save credentials
echo "ADMIN_EMAIL=${ADMIN_EMAIL}" >> /root/.jabali_db_credentials
echo "ADMIN_PASSWORD=${ADMIN_PASSWORD}" >> /root/.jabali_db_credentials
}
# Print completion message
print_completion() {
local server_ip=$(hostname -I | awk '{print $1}')
echo ""
echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}${BOLD}Jabali Panel Installation Complete!${NC}${GREEN}${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}╠════════════════════════════════════════════════════════════╣${NC}"
echo -e "${GREEN}║ ║${NC}"
printf "${GREEN}${NC} Version: ${BOLD}%-46s${NC} ${GREEN}${NC}\n" "v${JABALI_VERSION}"
printf "${GREEN}${NC} Hostname: ${BOLD}%-45s${NC} ${GREEN}${NC}\n" "${SERVER_HOSTNAME}"
printf "${GREEN}${NC} Server IP: ${BOLD}%-44s${NC} ${GREEN}${NC}\n" "${server_ip}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}╠════════════════════════════════════════════════════════════╣${NC}"
echo -e "${GREEN}${NC} ${BOLD}Admin Credentials:${NC} ${GREEN}${NC}"
printf "${GREEN}${NC} Email: ${BOLD}%-43s${NC} ${GREEN}${NC}\n" "${ADMIN_EMAIL}"
printf "${GREEN}${NC} Password: ${BOLD}%-43s${NC} ${GREEN}${NC}\n" "${ADMIN_PASSWORD}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}╠════════════════════════════════════════════════════════════╣${NC}"
echo -e "${GREEN}${NC} ${BOLD}Panel URLs:${NC} ${GREEN}${NC}"
printf "${GREEN}${NC} Admin Panel: ${BOLD}%-40s${NC} ${GREEN}${NC}\n" "https://${SERVER_HOSTNAME}/jabali-admin/"
printf "${GREEN}${NC} User Panel: ${BOLD}%-40s${NC} ${GREEN}${NC}\n" "https://${SERVER_HOSTNAME}/jabali-panel/"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}╠════════════════════════════════════════════════════════════╣${NC}"
if [[ "$PANEL_SSL_INSTALLED" == "true" ]]; then
echo -e "${GREEN}${NC} ${GREEN}SSL: Let's Encrypt certificate installed${NC} ${GREEN}${NC}"
else
echo -e "${GREEN}${NC} ${YELLOW}Note: Using self-signed SSL certificate.${NC} ${GREEN}${NC}"
echo -e "${GREEN}${NC} ${YELLOW}Browser will show a security warning - this is normal.${NC} ${GREEN}${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}${NC} ${CYAN}Get a free SSL certificate:${NC} ${GREEN}${NC}"
printf "${GREEN}${NC} ${CYAN}%-56s${NC} ${GREEN}${NC}\n" "certbot --nginx -d ${SERVER_HOSTNAME}"
fi
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}${NC} CLI Usage: ${CYAN}jabali --help${NC} ${GREEN}${NC}"
echo -e "${GREEN}${NC} Credentials: ${CYAN}/root/.jabali_db_credentials${NC} ${GREEN}${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
echo ""
}
# Uninstall Jabali Panel
uninstall() {
local force_uninstall=false
# Check for --force flag
if [[ "$1" == "--force" ]] || [[ "$1" == "-f" ]]; then
force_uninstall=true
fi
show_banner
check_root
echo -e "${RED}${BOLD}WARNING: This will completely remove Jabali Panel and all related services!${NC}"
echo ""
echo "This will remove:"
echo " - Jabali Panel files (/var/www/jabali)"
echo " - Jabali database and user"
echo " - Nginx, PHP-FPM, MariaDB, Redis"
echo " - Mail server (Postfix, Dovecot, Rspamd)"
echo " - DNS server (BIND9)"
echo " - All user home directories (/home/*)"
echo " - All virtual mail (/var/mail)"
echo " - All domains and configurations"
echo ""
echo -e "${YELLOW}This action cannot be undone!${NC}"
echo ""
if [[ "$force_uninstall" == "false" ]]; then
# First confirmation
read -p "Are you sure you want to uninstall? (y/N): " confirm1 < /dev/tty
if [[ ! "$confirm1" =~ ^[Yy]$ ]]; then
info "Uninstall cancelled"
exit 0
fi
echo ""
echo -e "${RED}${BOLD}FINAL WARNING: ALL DATA WILL BE PERMANENTLY DELETED!${NC}"
echo ""
# Second confirmation - require typing
read -p "Type 'YES DELETE EVERYTHING' to confirm: " confirm2 < /dev/tty
if [[ "$confirm2" != "YES DELETE EVERYTHING" ]]; then
info "Uninstall cancelled"
exit 0
fi
else
warn "Force mode enabled - skipping confirmations"
fi
header "Stopping Services"
systemctl stop jabali-agent 2>/dev/null || true
systemctl disable jabali-agent 2>/dev/null || true
rm -f /etc/systemd/system/jabali-agent.service
rm -rf /etc/systemd/system/jabali-agent.service.d
systemctl stop jabali-health-monitor 2>/dev/null || true
systemctl disable jabali-health-monitor 2>/dev/null || true
rm -f /etc/systemd/system/jabali-health-monitor.service
rm -rf /etc/systemd/system/jabali-health-monitor.service.d
systemctl stop jabali-queue 2>/dev/null || true
systemctl disable jabali-queue 2>/dev/null || true
rm -f /etc/systemd/system/jabali-queue.service
rm -rf /etc/systemd/system/jabali-queue.service.d
local services=(
nginx
php-fpm
php8.4-fpm
mariadb
mysql
redis-server
postfix
dovecot
rspamd
opendkim
bind9
named
fail2ban
clamav-daemon
clamav-freshclam
)
for service in "${services[@]}"; do
systemctl stop "$service" 2>/dev/null || true
systemctl disable "$service" 2>/dev/null || true
done
# Remove systemd restart overrides
rm -rf /etc/systemd/system/nginx.service.d/restart.conf
rm -rf /etc/systemd/system/mariadb.service.d/restart.conf
rm -rf /etc/systemd/system/jabali-agent.service.d/restart.conf
rm -rf /etc/systemd/system/jabali-queue.service.d/restart.conf
rm -rf /etc/systemd/system/php*.service.d/restart.conf
rm -rf /etc/systemd/system/postfix.service.d/restart.conf
rm -rf /etc/systemd/system/dovecot.service.d/restart.conf
rm -rf /etc/systemd/system/named.service.d/restart.conf
rm -rf /etc/systemd/system/bind9.service.d/restart.conf
rm -rf /etc/systemd/system/redis-server.service.d/restart.conf
rm -rf /etc/systemd/system/fail2ban.service.d/restart.conf
systemctl daemon-reload
header "Removing Jabali Panel"
rm -rf "$JABALI_DIR"
rm -f /usr/local/bin/jabali
rm -rf /var/run/jabali
rm -rf /var/log/jabali
rm -rf /var/backups/jabali
rm -f /root/.jabali_db_credentials
rm -f /root/.jabali_redis_credentials
log "Jabali Panel removed"
header "Removing Database"
mysql -e "DROP DATABASE IF EXISTS jabali;" 2>/dev/null || true
mysql -e "DROP USER IF EXISTS 'jabali'@'localhost';" 2>/dev/null || true
log "Database removed"
header "Removing Packages"
# Set non-interactive mode to prevent dialog prompts
export DEBIAN_FRONTEND=noninteractive
# Pre-configure debconf to remove databases without prompting
echo "mariadb-server mysql-server/remove-data-dir boolean true" | debconf-set-selections 2>/dev/null || true
echo "mariadb-server-10.5 mysql-server/remove-data-dir boolean true" | debconf-set-selections 2>/dev/null || true
echo "mariadb-server-10.6 mysql-server/remove-data-dir boolean true" | debconf-set-selections 2>/dev/null || true
echo "mariadb-server-10.11 mysql-server/remove-data-dir boolean true" | debconf-set-selections 2>/dev/null || true
local packages=(
# Web Server
nginx
nginx-common
# PHP
'php*'
# Database
mariadb-server
mariadb-client
mariadb-common
# Cache
redis-server
# Mail Server
postfix
postfix-mysql
dovecot-core
dovecot-imapd
dovecot-pop3d
dovecot-lmtpd
dovecot-mysql
opendkim
opendkim-tools
rspamd
# DNS
bind9
bind9-utils
# Webmail
roundcube
roundcube-core
roundcube-mysql
roundcube-sqlite3
roundcube-plugins
# Security
fail2ban
clamav
clamav-daemon
clamav-freshclam
)
for pkg in "${packages[@]}"; do
DEBIAN_FRONTEND=noninteractive apt-get purge -y -qq $pkg 2>/dev/null || true
done
DEBIAN_FRONTEND=noninteractive apt-get autoremove -y -qq
DEBIAN_FRONTEND=noninteractive apt-get autoclean -y -qq
log "Packages removed"
header "Cleaning Up Files"
# Web server
rm -rf /etc/nginx
rm -rf /var/cache/nginx
rm -rf /etc/php
rm -rf /var/lib/php
rm -rf /var/log/nginx
# Database
rm -rf /var/lib/mysql
rm -rf /var/lib/redis
rm -rf /var/log/mysql
# Mail server
rm -rf /etc/postfix
rm -rf /etc/dovecot
rm -rf /etc/opendkim
rm -rf /etc/rspamd
rm -rf /var/mail
rm -rf /var/vmail
rm -rf /var/spool/postfix
rm -rf /var/log/mail.*
# DNS
rm -rf /etc/bind
rm -rf /var/cache/bind
rm -rf /var/log/named
# Webmail (Roundcube)
rm -rf /etc/roundcube
rm -rf /var/lib/roundcube
rm -rf /var/log/roundcube
rm -rf /usr/share/roundcube
# Security
rm -rf /etc/fail2ban
rm -rf /var/lib/fail2ban
rm -rf /var/log/fail2ban.log*
rm -rf /var/lib/clamav
rm -rf /var/log/clamav
# SSL certificates (Let's Encrypt)
rm -rf /etc/letsencrypt
# PHP repository
rm -f /etc/apt/sources.list.d/php.list
rm -f /usr/share/keyrings/sury-php.gpg
# Jabali-specific configs
rm -rf /var/backups/users
rm -f /etc/logrotate.d/jabali-users
# Remove www-data cron jobs (Laravel scheduler)
crontab -u www-data -r 2>/dev/null || true
log "Configuration files cleaned"
header "Removing User Data"
read -p "Remove all user home directories? (y/N): " remove_homes < /dev/tty
if [[ "$remove_homes" =~ ^[Yy]$ ]]; then
# Get list of normal users (UID >= 1000, excluding nobody)
for user_home in /home/*; do
if [[ -d "$user_home" ]]; then
username=$(basename "$user_home")
userdel -r "$username" 2>/dev/null || rm -rf "$user_home"
log "Removed user: $username"
fi
done
fi
# Remove vmail user
userdel vmail 2>/dev/null || true
groupdel vmail 2>/dev/null || true
header "Resetting Firewall"
if command -v ufw &> /dev/null; then
ufw --force reset
ufw --force disable
else
info "UFW not installed, skipping firewall reset"
fi
echo ""
echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}${BOLD}Jabali Panel Uninstallation Complete!${NC}${GREEN}${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}${NC} All Jabali Panel components have been removed. ${GREEN}${NC}"
echo -e "${GREEN}${NC} Your server is now clean. ${GREEN}${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
echo ""
}
# Show usage
show_usage() {
echo "Jabali Panel Installer"
echo ""
echo "Usage: $0 [command] [options]"
echo ""
echo "Commands:"
echo " install Install Jabali Panel (default, interactive)"
echo " uninstall [--force] Remove Jabali Panel and all components"
echo " --help Show this help message"
echo ""
echo "Environment Variables (for non-interactive install):"
echo " SERVER_HOSTNAME Set the server hostname"
echo " JABALI_FULL Install all components (set to any value)"
echo " JABALI_MINIMAL Install only core components (set to any value)"
echo ""
echo "Installation Modes:"
echo " Full Installation - Web, Mail, DNS, Firewall, Security tools"
echo " Minimal Installation - Web server only (Nginx, PHP, MariaDB, Redis, UFW)"
echo " Custom Installation - Choose individual components interactively"
echo ""
echo "Examples:"
echo ""
echo " Interactive install (prompts for options):"
echo " curl -fsSL http://192.168.100.100:3001/shukivaknin/jabali-panel/raw/branch/main/install_from_gitea.sh | sudo bash"
echo ""
echo " Full install (non-interactive):"
echo " SERVER_HOSTNAME=panel.example.com JABALI_FULL=1 curl -fsSL ... | sudo bash"
echo ""
echo " Minimal install (non-interactive):"
echo " SERVER_HOSTNAME=panel.example.com JABALI_MINIMAL=1 curl -fsSL ... | sudo bash"
echo ""
echo " Uninstall:"
echo " curl -fsSL http://192.168.100.100:3001/shukivaknin/jabali-panel/raw/branch/main/install_from_gitea.sh | sudo bash -s -- uninstall"
echo ""
echo " Force uninstall (no prompts):"
echo " curl -fsSL http://192.168.100.100:3001/shukivaknin/jabali-panel/raw/branch/main/install_from_gitea.sh | sudo bash -s -- uninstall --force"
echo ""
}
# Main installation
main() {
show_banner
check_root
check_os
local queue_was_active=false
if systemctl is-active --quiet jabali-queue; then
queue_was_active=true
systemctl stop jabali-queue 2>/dev/null || true
fi
prompt_hostname
select_features
add_repositories
install_packages
install_geoipupdate_binary
install_composer
clone_jabali
configure_php
configure_mariadb
configure_nginx
# Optional components based on feature selection
if [[ "$INSTALL_MAIL" == "true" ]]; then
configure_mail
else
info "Skipping Mail Server configuration"
fi
if [[ "$INSTALL_DNS" == "true" ]]; then
configure_dns
else
info "Skipping DNS Server configuration"
fi
if [[ "$INSTALL_FIREWALL" == "true" ]]; then
configure_firewall
else
info "Skipping Firewall configuration"
fi
# Setup disk quotas for user space management
setup_quotas
# Always configure fail2ban and SSH jails (ClamAV is conditional inside)
configure_security
# Enable fail2ban mail jails if mail server was installed
if [[ "$INSTALL_MAIL" == "true" ]]; then
info "Enabling fail2ban mail protection jails..."
if [[ -f /etc/fail2ban/jail.d/dovecot.conf ]]; then
sed -i 's/^enabled = false/enabled = true/' /etc/fail2ban/jail.d/dovecot.conf
fi
if [[ -f /etc/fail2ban/jail.d/postfix.conf ]]; then
sed -i 's/^enabled = false/enabled = true/' /etc/fail2ban/jail.d/postfix.conf
fi
# Only enable roundcube jail if log file exists
if [[ -f /etc/fail2ban/jail.d/roundcube.conf ]]; then
# Create roundcube logs directory if roundcube is installed
if [[ -d /var/lib/roundcube ]]; then
mkdir -p /var/lib/roundcube/logs
touch /var/lib/roundcube/logs/errors.log
chown -R www-data:www-data /var/lib/roundcube/logs
fi
# Only enable if log file exists
if [[ -f /var/lib/roundcube/logs/errors.log ]]; then
sed -i 's/^enabled = false/enabled = true/' /etc/fail2ban/jail.d/roundcube.conf
fi
fi
systemctl reload fail2ban 2>/dev/null || systemctl restart fail2ban 2>/dev/null || true
log "Fail2ban mail jails enabled"
fi
configure_redis
setup_jabali
setup_agent_service
setup_queue_service
setup_scheduler_cron
setup_logrotate
setup_panel_ssl
setup_self_healing
create_admin
create_webmaster_mailbox
configure_ssh_notifications
if [[ "$queue_was_active" == "true" ]]; then
systemctl start jabali-queue 2>/dev/null || true
fi
print_completion
}
# Parse command line arguments
case "${1:-install}" in
install)
main
;;
uninstall|remove|purge)
uninstall "$2"
;;
--help|-h|help)
show_usage
;;
*)
error "Unknown command: $1"
show_usage
exit 1
;;
esac