#!/bin/bash # ═══════════════════════════════════════════════════════════════════════════════ # AelynaCP Installer # Copyright (c) 2026 AelynaCP. All rights reserved. # https://aelynacp.com # ═══════════════════════════════════════════════════════════════════════════════ set -euo pipefail # ── Constants ───────────────────────────────────────────────────────────────── AELYNACP_VERSION="0.2.2" RELEASE_SERVER="https://panel.aelynacp.com" LICENSE_API="${RELEASE_SERVER}/api/v1/license/activate" DOWNLOAD_URL="${RELEASE_SERVER}/api/v1/releases/download/${AELYNACP_VERSION}" INSTALL_DIR="/opt/aelynacp" CONFIG_DIR="/etc/aelynacp" DATA_DIR="/var/lib/aelynacp" LOG_DIR="/var/log/aelynacp" RUN_DIR="/var/run/aelynacp" WEB_DIR="${INSTALL_DIR}/web" BIN_DIR="${INSTALL_DIR}/bin" LOG_FILE="/tmp/aelynacp-install.log" DB_NAME="aelynacp" DB_USER="aelynacp" DB_PASS="" # Generated during install # ── Colors ──────────────────────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' WHITE='\033[1;37m' GRAY='\033[0;90m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' # No Color # ── Variables (set during install) ──────────────────────────────────────────── OS_TYPE="" # rhel or debian PKG_MGR="" # dnf or apt HOSTNAME_INPUT="" PRIMARY_IP="" ADMIN_EMAIL="" ADMIN_PASSWORD="" LICENSE_KEY="" NS1="ns1.aelynacp.com" NS2="ns2.aelynacp.com" JWT_SECRET="" # ═══════════════════════════════════════════════════════════════════════════════ # FUNCTIONS # ═══════════════════════════════════════════════════════════════════════════════ show_logo() { clear echo -e "${MAGENTA}" echo ' ___ __ __________ ' echo ' / | ___ / /_ ______ ____ _/ ____/ __ \' echo ' / /| | / _ \/ / / / / __ \/ __ `/ / / /_/ /' echo ' / ___ |/ __/ / /_/ / / / / /_/ / /___/ ____/ ' echo '/_/ |_|\___/_/\__, /_/ /_/\__,_/\____/_/ ' echo ' /____/ ' echo -e "${NC}" echo -e "${WHITE}${BOLD} AELYNA${CYAN}™${WHITE}CP${NC}" echo -e "${GRAY} Hosting Control Panel${NC}" echo -e "${GRAY} Version ${AELYNACP_VERSION}${NC}" echo "" echo -e "${DIM} Copyright (c) 2026 AelynaCP. All rights reserved.${NC}" echo -e "${DIM} https://aelynacp.com${NC}" echo "" echo -e "${GRAY} ─────────────────────────────────────────────────${NC}" echo "" } log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE" } info() { echo -e " ${CYAN}[INFO]${NC} $*" log "INFO: $*" } success() { echo -e " ${GREEN}[ OK ]${NC} $*" log "OK: $*" } warn() { echo -e " ${YELLOW}[ WARN ]${NC} $*" log "WARN: $*" } error() { echo -e " ${RED}[ERROR]${NC} $*" log "ERROR: $*" } fatal() { echo -e "\n ${RED}${BOLD}[FATAL]${NC} $*" echo -e " ${GRAY}Log: ${LOG_FILE}${NC}\n" log "FATAL: $*" exit 1 } TOTAL_STEPS=7 progress_bar() { local current="$1" local total="$2" local width=40 local pct=$(( current * 100 / total )) local filled=$(( current * width / total )) local empty=$(( width - filled )) local bar="" for ((i=0; i/dev/null; do local c="${chars:$i:1}" echo -ne "\r ${CYAN}${c}${NC} ${msg}..." i=$(( (i + 1) % ${#chars} )) sleep 0.1 done wait "$pid" local exit_code=$? echo -ne "\r" if [ $exit_code -eq 0 ]; then success "$msg" else error "$msg (exit code: $exit_code)" fi return $exit_code } run_quiet() { local msg="$1" shift "$@" >> "$LOG_FILE" 2>&1 & spinner $! "$msg" } prompt() { local var_name="$1" local prompt_text="$2" local default="$3" local value if [ -n "$default" ]; then echo -ne " ${WHITE}${prompt_text}${NC} ${GRAY}[${default}]${NC}: " read -r value value="${value:-$default}" else echo -ne " ${WHITE}${prompt_text}${NC}: " read -r value fi eval "$var_name='$value'" } prompt_password() { local var_name="$1" local prompt_text="$2" local pass1 pass2 while true; do echo -ne " ${WHITE}${prompt_text}${NC}: " read -rs pass1 echo "" if [ ${#pass1} -lt 8 ]; then warn "La password deve avere almeno 8 caratteri" continue fi echo -ne " ${WHITE}Conferma password${NC}: " read -rs pass2 echo "" if [ "$pass1" != "$pass2" ]; then warn "Le password non corrispondono" continue fi break done eval "$var_name='$pass1'" } generate_password() { tr -dc 'A-Za-z0-9!@#$%' < /dev/urandom | head -c 24 } generate_hex() { local length="${1:-64}" tr -dc 'a-f0-9' < /dev/urandom | head -c "$length" } detect_os() { if [ -f /etc/os-release ]; then . /etc/os-release case "$ID" in rocky|almalinux|rhel|centos|fedora|ol) OS_TYPE="rhel" PKG_MGR="dnf" ;; debian|ubuntu) OS_TYPE="debian" PKG_MGR="apt" ;; *) fatal "OS non supportato: $ID. Supportati: Rocky, AlmaLinux, RHEL 9+, Ubuntu 22.04+, Debian 12+" ;; esac else fatal "Impossibile rilevare il sistema operativo" fi } detect_ip() { # Try to get public IP local ip ip=$(curl -4 -s --max-time 5 https://ifconfig.me 2>/dev/null || \ curl -4 -s --max-time 5 https://api.ipify.org 2>/dev/null || \ curl -4 -s --max-time 5 https://icanhazip.com 2>/dev/null || \ hostname -I 2>/dev/null | awk '{print $1}' || \ echo "") echo "$ip" } pkg_install() { if [ "$PKG_MGR" = "dnf" ]; then dnf install -y "$@" >> "$LOG_FILE" 2>&1 else DEBIAN_FRONTEND=noninteractive apt-get install -y "$@" >> "$LOG_FILE" 2>&1 fi } # ═══════════════════════════════════════════════════════════════════════════════ # STEP 0 — PREFLIGHT CHECKS # ═══════════════════════════════════════════════════════════════════════════════ preflight_checks() { step_header "0" "Controlli preliminari" # Must be root if [ "$(id -u)" -ne 0 ]; then fatal "Questo script deve essere eseguito come root" fi success "Esecuzione come root" # Detect OS detect_os success "OS rilevato: ${OS_TYPE} (${ID} ${VERSION_ID})" # Check architecture local arch arch=$(uname -m) if [ "$arch" != "x86_64" ]; then fatal "Architettura non supportata: $arch. Richiesto: x86_64" fi success "Architettura: x86_64" # Check RAM local ram_mb ram_mb=$(free -m | awk '/^Mem:/ {print $2}') if [ "$ram_mb" -lt 1024 ]; then warn "RAM: ${ram_mb}MB (minimo consigliato: 1024MB)" else success "RAM: ${ram_mb}MB" fi # Check disk local disk_gb disk_gb=$(df -BG / | awk 'NR==2 {gsub("G",""); print $4}') if [ "$disk_gb" -lt 10 ]; then fatal "Spazio disco insufficiente: ${disk_gb}GB (minimo: 10GB)" fi success "Disco libero: ${disk_gb}GB" # Check internet if curl -s --max-time 5 https://panel.aelynacp.com/api/v1/health > /dev/null 2>&1; then success "Connessione internet: OK" else warn "Impossibile raggiungere panel.aelynacp.com — la verifica licenza potrebbe fallire" fi # Init log echo "=== AelynaCP Install Log ===" > "$LOG_FILE" echo "Date: $(date)" >> "$LOG_FILE" echo "OS: ${ID} ${VERSION_ID}" >> "$LOG_FILE" echo "Arch: $arch" >> "$LOG_FILE" echo "RAM: ${ram_mb}MB" >> "$LOG_FILE" echo "" >> "$LOG_FILE" } # ═══════════════════════════════════════════════════════════════════════════════ # STEP 1 — LICENSE # ═══════════════════════════════════════════════════════════════════════════════ step_license() { step_header "1" "Licenza" prompt LICENSE_KEY "Inserisci la tua license key" "" if [ -z "$LICENSE_KEY" ]; then fatal "License key obbligatoria" fi # Show license agreement echo "" echo -e " ${WHITE}${BOLD}Contratto di Licenza AelynaCP${NC}" echo -e " ${GRAY}────────────────────────────────────────────${NC}" echo -e " ${GRAY}Il software AelynaCP e' concesso in licenza, non venduto.${NC}" echo -e " ${GRAY}L'uso e' soggetto ai termini disponibili su:${NC}" echo -e " ${CYAN}https://aelynacp.com/license${NC}" echo "" echo -e " ${GRAY}Punti principali:${NC}" echo -e " ${GRAY} - Una licenza per server${NC}" echo -e " ${GRAY} - Divieto di redistribuzione o reverse engineering${NC}" echo -e " ${GRAY} - Il software richiede verifica periodica della licenza${NC}" echo -e " ${GRAY} - AelynaCP non e' responsabile per danni diretti o indiretti${NC}" echo -e " ${GRAY}────────────────────────────────────────────${NC}" echo "" echo -ne " ${WHITE}Accetti i termini e le condizioni?${NC} ${GRAY}[y/N]${NC}: " read -r accept if [[ ! "$accept" =~ ^[yYsS]$ ]]; then fatal "Devi accettare i termini per procedere" fi # Verify license with server info "Verifica licenza in corso..." local machine_id hostname_val mac_addr machine_id=$(cat /etc/machine-id 2>/dev/null || echo "unknown") hostname_val=$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo "unknown") mac_addr=$(ip link show 2>/dev/null | grep -v "lo:" | grep "link/ether" | head -1 | awk '{print $2}' || echo "00:00:00:00:00:00") local ip_addr ip_addr=$(detect_ip) local response response=$(curl -s --max-time 15 -X POST "$LICENSE_API" \ -H "Content-Type: application/json" \ -d "{ \"license_key\": \"${LICENSE_KEY}\", \"fingerprint\": { \"ip\": \"${ip_addr}\", \"mac\": \"${mac_addr}\", \"machine_id\": \"${machine_id}\", \"hostname\": \"${hostname_val}\" } }" 2>/dev/null || echo '{"valid":false,"message":"Connection failed"}') local valid valid=$(echo "$response" | python3 -c "import sys,json; print(json.load(sys.stdin).get('valid', False))" 2>/dev/null || echo "False") if [ "$valid" = "True" ] || [ "$valid" = "true" ]; then local license_type license_type=$(echo "$response" | python3 -c "import sys,json; print(json.load(sys.stdin).get('license_type', 'Standard'))" 2>/dev/null || echo "Standard") success "Licenza valida: ${license_type}" else local msg msg=$(echo "$response" | python3 -c "import sys,json; print(json.load(sys.stdin).get('message', 'Licenza non valida'))" 2>/dev/null || echo "Licenza non valida") fatal "Licenza non valida: $msg" fi } # ═══════════════════════════════════════════════════════════════════════════════ # STEP 2 — CONFIGURATION # ═══════════════════════════════════════════════════════════════════════════════ step_configuration() { step_header "2" "Configurazione" local detected_ip detected_ip=$(detect_ip) local detected_hostname detected_hostname=$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo "") prompt HOSTNAME_INPUT "Hostname del server" "$detected_hostname" prompt PRIMARY_IP "IP primario" "$detected_ip" prompt ADMIN_EMAIL "Email amministratore" "" prompt_password ADMIN_PASSWORD "Password amministratore" prompt NS1 "Nameserver 1" "ns1.aelynacp.com" prompt NS2 "Nameserver 2" "ns2.aelynacp.com" echo "" echo -e " ${WHITE}Riepilogo configurazione:${NC}" echo -e " Hostname: ${CYAN}${HOSTNAME_INPUT}${NC}" echo -e " IP: ${CYAN}${PRIMARY_IP}${NC}" echo -e " Email: ${CYAN}${ADMIN_EMAIL}${NC}" echo -e " Nameservers: ${CYAN}${NS1}, ${NS2}${NC}" echo "" echo -ne " ${WHITE}Confermi?${NC} ${GRAY}[Y/n]${NC}: " read -r confirm if [[ "$confirm" =~ ^[nN]$ ]]; then step_configuration # Recurse fi } # ═══════════════════════════════════════════════════════════════════════════════ # STEP 3 — INSTALL PACKAGES # ═══════════════════════════════════════════════════════════════════════════════ step_packages() { step_header "3" "Installazione pacchetti" if [ "$OS_TYPE" = "rhel" ]; then install_packages_rhel else install_packages_debian fi } install_packages_rhel() { info "Configurazione repository (RHEL-based)..." # EPEL run_quiet "Installazione EPEL" dnf install -y epel-release # Remi (for PHP) if ! rpm -q remi-release > /dev/null 2>&1; then run_quiet "Installazione Remi repo" dnf install -y "https://rpms.remirepo.net/enterprise/remi-release-$(rpm -E %rhel).rpm" fi success "Repository configurati" # Enable PHP 8.3 module dnf module reset php -y >> "$LOG_FILE" 2>&1 || true dnf module enable php:remi-8.3 -y >> "$LOG_FILE" 2>&1 || true # PowerDNS repo if ! rpm -q pdns > /dev/null 2>&1; then cat > /etc/yum.repos.d/powerdns-auth.repo << 'PDNSREPO' [powerdns-auth-49] name=PowerDNS Authoritative Server 4.9 - EPEL $releasever baseurl=https://repo.powerdns.com/el/$releasever/$basearch/auth-49 gpgcheck=1 gpgkey=https://repo.powerdns.com/FD380FBB-pub.asc enabled=1 module_hotfixes=1 PDNSREPO fi info "Installazione pacchetti (potrebbe richiedere qualche minuto)..." # Core run_quiet "Nginx" pkg_install nginx run_quiet "PostgreSQL" pkg_install postgresql-server postgresql run_quiet "MariaDB" pkg_install mariadb-server mariadb run_quiet "PHP 8.3" pkg_install php-fpm php-cli php-mysqlnd php-pgsql php-gd php-xml php-mbstring php-zip php-intl php-opcache php-curl php-json run_quiet "Postfix" pkg_install postfix run_quiet "Dovecot" pkg_install dovecot run_quiet "PowerDNS" pkg_install pdns pdns-backend-pgsql run_quiet "ProFTPD" pkg_install proftpd run_quiet "Valkey/Redis" pkg_install valkey || run_quiet "Redis (fallback)" pkg_install redis run_quiet "Certbot" pkg_install certbot python3-certbot-nginx run_quiet "Fail2Ban" pkg_install fail2ban run_quiet "Firewalld" pkg_install firewalld run_quiet "Monitoring" pkg_install smartmontools mdadm sysstat run_quiet "Tools" pkg_install tar gzip rsync openssl unzip wget curl python3 python3-pip # Argon2 for password hashing in installer pip3 install argon2-cffi >> "$LOG_FILE" 2>&1 || true success "Tutti i pacchetti installati" } install_packages_debian() { info "Configurazione repository (Debian/Ubuntu)..." export DEBIAN_FRONTEND=noninteractive run_quiet "Aggiornamento indici" apt-get update -y # Ondrej PHP repo if ! command -v add-apt-repository > /dev/null 2>&1; then run_quiet "software-properties-common" apt-get install -y software-properties-common fi run_quiet "Ondrej PHP repo" add-apt-repository -y ppa:ondrej/php || true apt-get update -y >> "$LOG_FILE" 2>&1 # PowerDNS repo if ! command -v pdns_server > /dev/null 2>&1; then echo "deb [signed-by=/etc/apt/keyrings/powerdns.asc] https://repo.powerdns.com/ubuntu $(lsb_release -cs)-auth-49 main" > /etc/apt/sources.list.d/powerdns.list 2>/dev/null || true curl -fsSL https://repo.powerdns.com/FD380FBB-pub.asc | tee /etc/apt/keyrings/powerdns.asc > /dev/null 2>&1 || true apt-get update -y >> "$LOG_FILE" 2>&1 || true fi info "Installazione pacchetti (potrebbe richiedere qualche minuto)..." run_quiet "Nginx" pkg_install nginx run_quiet "PostgreSQL" pkg_install postgresql postgresql-client run_quiet "MariaDB" pkg_install mariadb-server mariadb-client run_quiet "PHP 8.3" pkg_install php8.3-fpm php8.3-cli php8.3-mysql php8.3-pgsql php8.3-gd php8.3-xml php8.3-mbstring php8.3-zip php8.3-intl php8.3-opcache php8.3-curl run_quiet "Postfix" pkg_install postfix run_quiet "Dovecot" pkg_install dovecot-imapd dovecot-pop3d run_quiet "PowerDNS" pkg_install pdns-server pdns-backend-pgsql || warn "PowerDNS non installato — installare manualmente" run_quiet "ProFTPD" pkg_install proftpd-basic run_quiet "Valkey/Redis" pkg_install redis-server run_quiet "Certbot" pkg_install certbot python3-certbot-nginx run_quiet "Fail2Ban" pkg_install fail2ban run_quiet "Firewalld" pkg_install firewalld || warn "Firewalld non installato" run_quiet "Monitoring" pkg_install smartmontools mdadm sysstat run_quiet "Tools" pkg_install tar gzip rsync openssl unzip wget curl python3 python3-pip pip3 install argon2-cffi >> "$LOG_FILE" 2>&1 || true success "Tutti i pacchetti installati" } # ═══════════════════════════════════════════════════════════════════════════════ # STEP 4 — CONFIGURE SERVICES # ═══════════════════════════════════════════════════════════════════════════════ step_configure() { step_header "4" "Configurazione servizi" configure_postgresql configure_mariadb configure_nginx configure_phpfpm configure_postfix configure_dovecot configure_powerdns configure_proftpd configure_valkey configure_fail2ban configure_firewall } configure_postgresql() { info "Configurazione PostgreSQL..." if [ "$OS_TYPE" = "rhel" ]; then postgresql-setup --initdb >> "$LOG_FILE" 2>&1 || true fi systemctl enable --now postgresql >> "$LOG_FILE" 2>&1 # Allow password auth for local connections local pg_hba if [ "$OS_TYPE" = "rhel" ]; then pg_hba="/var/lib/pgsql/data/pg_hba.conf" else pg_hba=$(find /etc/postgresql -name pg_hba.conf 2>/dev/null | head -1) fi if [ -n "$pg_hba" ]; then # Change ident/peer to md5 for local connections sed -i 's/^local\s\+all\s\+all\s\+peer/local all all md5/' "$pg_hba" 2>/dev/null || true sed -i 's/^local\s\+all\s\+all\s\+ident/local all all md5/' "$pg_hba" 2>/dev/null || true sed -i 's/^host\s\+all\s\+all\s\+127.0.0.1\/32\s\+ident/host all all 127.0.0.1\/32 md5/' "$pg_hba" 2>/dev/null || true # Keep postgres user as peer for admin access sed -i '1i local all postgres peer' "$pg_hba" 2>/dev/null || true fi systemctl restart postgresql >> "$LOG_FILE" 2>&1 # Generate DB password DB_PASS=$(generate_password) # Create user and database sudo -u postgres psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';" >> "$LOG_FILE" 2>&1 || true sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};" >> "$LOG_FILE" 2>&1 || true sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};" >> "$LOG_FILE" 2>&1 || true sudo -u postgres psql -d "$DB_NAME" -c "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";" >> "$LOG_FILE" 2>&1 || true success "PostgreSQL configurato (DB: ${DB_NAME}, User: ${DB_USER})" } configure_mariadb() { info "Configurazione MariaDB..." systemctl enable --now mariadb >> "$LOG_FILE" 2>&1 || systemctl enable --now mysql >> "$LOG_FILE" 2>&1 || true # Secure MariaDB local mysql_root_pw mysql_root_pw=$(generate_password) mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '${mysql_root_pw}';" >> "$LOG_FILE" 2>&1 || true mysql -e "DELETE FROM mysql.user WHERE User='';" >> "$LOG_FILE" 2>&1 || true mysql -e "FLUSH PRIVILEGES;" >> "$LOG_FILE" 2>&1 || true # Save root password mkdir -p /root echo "[client] user=root password=${mysql_root_pw}" > /root/.my.cnf chmod 600 /root/.my.cnf success "MariaDB configurato (root password salvata in /root/.my.cnf)" } configure_nginx() { info "Configurazione Nginx..." # Create vhosts directory mkdir -p /etc/nginx/conf.d/vhosts # Default server pages mkdir -p /var/www/default cat > /var/www/default/index.html << 'DEFHTML' Server in configurazione

Sito in fase di configurazione

Questo server e' gestito da AelynaCP

DEFHTML # Ensure main nginx.conf includes conf.d and vhosts if ! grep -q "conf.d/vhosts" /etc/nginx/nginx.conf 2>/dev/null; then sed -i '/include.*conf\.d.*\.conf/a\ include /etc/nginx/conf.d/vhosts/*.conf;' /etc/nginx/nginx.conf 2>/dev/null || true fi systemctl enable nginx >> "$LOG_FILE" 2>&1 success "Nginx configurato" } configure_phpfpm() { info "Configurazione PHP-FPM..." # Create run directory for sockets mkdir -p /var/run/php-fpm systemctl enable php-fpm >> "$LOG_FILE" 2>&1 || systemctl enable php8.3-fpm >> "$LOG_FILE" 2>&1 || true systemctl start php-fpm >> "$LOG_FILE" 2>&1 || systemctl start php8.3-fpm >> "$LOG_FILE" 2>&1 || true success "PHP-FPM configurato" } configure_postfix() { info "Configurazione Postfix..." postconf -e "myhostname = ${HOSTNAME_INPUT}" >> "$LOG_FILE" 2>&1 || true postconf -e "mydomain = ${HOSTNAME_INPUT}" >> "$LOG_FILE" 2>&1 || true postconf -e "inet_interfaces = all" >> "$LOG_FILE" 2>&1 || true systemctl enable --now postfix >> "$LOG_FILE" 2>&1 || true success "Postfix configurato" } configure_dovecot() { info "Configurazione Dovecot..." # Create virtual mail user groupadd -g 5000 vmail >> "$LOG_FILE" 2>&1 || true useradd -u 5000 -g vmail -d /var/mail/vhosts -s /sbin/nologin vmail >> "$LOG_FILE" 2>&1 || true mkdir -p /var/mail/vhosts chown -R vmail:vmail /var/mail/vhosts # Create users file touch /etc/dovecot/users chown dovecot:dovecot /etc/dovecot/users 2>/dev/null || true systemctl enable --now dovecot >> "$LOG_FILE" 2>&1 || true success "Dovecot configurato" } configure_powerdns() { info "Configurazione PowerDNS..." # Generate API key local pdns_api_key pdns_api_key=$(generate_hex 32) # Create separate DB for PowerDNS sudo -u postgres psql -c "CREATE DATABASE pdns OWNER ${DB_USER};" >> "$LOG_FILE" 2>&1 || true # PowerDNS PostgreSQL schema sudo -u postgres psql -d pdns << 'PDNSSCHEMA' >> "$LOG_FILE" 2>&1 || true CREATE TABLE IF NOT EXISTS domains (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL UNIQUE, master VARCHAR(128), last_check INT, type VARCHAR(8) NOT NULL DEFAULT 'NATIVE', notified_serial BIGINT, account VARCHAR(40)); CREATE TABLE IF NOT EXISTS records (id BIGSERIAL PRIMARY KEY, domain_id INT REFERENCES domains(id) ON DELETE CASCADE, name VARCHAR(255), type VARCHAR(10), content VARCHAR(65535), ttl INT, prio INT, disabled BOOL DEFAULT false, ordername VARCHAR(255), auth BOOL DEFAULT true); CREATE TABLE IF NOT EXISTS supermasters (ip VARCHAR(64) NOT NULL, nameserver VARCHAR(255) NOT NULL, account VARCHAR(40) NOT NULL); CREATE TABLE IF NOT EXISTS comments (id SERIAL PRIMARY KEY, domain_id INT NOT NULL, name VARCHAR(255) NOT NULL, type VARCHAR(10) NOT NULL, modified_at INT NOT NULL, account VARCHAR(40) NOT NULL, comment TEXT NOT NULL); CREATE TABLE IF NOT EXISTS domainmetadata (id SERIAL PRIMARY KEY, domain_id INT REFERENCES domains(id) ON DELETE CASCADE, kind VARCHAR(32), content TEXT); CREATE TABLE IF NOT EXISTS cryptokeys (id SERIAL PRIMARY KEY, domain_id INT REFERENCES domains(id) ON DELETE CASCADE, flags INT NOT NULL, active BOOL, published BOOL DEFAULT true, content TEXT); CREATE TABLE IF NOT EXISTS tsigkeys (id SERIAL PRIMARY KEY, name VARCHAR(255), algorithm VARCHAR(50), secret VARCHAR(255)); CREATE INDEX IF NOT EXISTS rec_name_index ON records(name); CREATE INDEX IF NOT EXISTS rec_domain_id ON records(domain_id); CREATE INDEX IF NOT EXISTS dom_name_index ON domains(name); PDNSSCHEMA # Grant permissions sudo -u postgres psql -d pdns -c "GRANT ALL ON ALL TABLES IN SCHEMA public TO ${DB_USER};" >> "$LOG_FILE" 2>&1 || true sudo -u postgres psql -d pdns -c "GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO ${DB_USER};" >> "$LOG_FILE" 2>&1 || true # Save API key mkdir -p "$CONFIG_DIR" echo "$pdns_api_key" > "${CONFIG_DIR}/pdns_api_key" chmod 600 "${CONFIG_DIR}/pdns_api_key" # Configure PowerDNS cat > /etc/pdns/pdns.conf << PDNSCONF launch=gpgsql gpgsql-host=127.0.0.1 gpgsql-dbname=pdns gpgsql-user=${DB_USER} gpgsql-password=${DB_PASS} local-address=0.0.0.0 local-port=53 api=yes api-key=${pdns_api_key} webserver=yes webserver-address=127.0.0.1 webserver-port=8081 webserver-allow-from=127.0.0.1 default-soa-content=${NS1}. admin.${HOSTNAME_INPUT}. 0 10800 3600 604800 3600 PDNSCONF systemctl enable --now pdns >> "$LOG_FILE" 2>&1 || true success "PowerDNS configurato (API key salvata)" } configure_proftpd() { info "Configurazione ProFTPD..." # Create virtual user password file touch /etc/proftpd/ftpd.passwd 2>/dev/null || mkdir -p /etc/proftpd && touch /etc/proftpd/ftpd.passwd chmod 600 /etc/proftpd/ftpd.passwd systemctl enable proftpd >> "$LOG_FILE" 2>&1 || true success "ProFTPD configurato" } configure_valkey() { info "Configurazione Valkey/Redis..." # Ensure bind to localhost only local redis_conf redis_conf=$(find /etc -name "valkey.conf" -o -name "redis.conf" 2>/dev/null | head -1) if [ -n "$redis_conf" ]; then sed -i 's/^bind .*/bind 127.0.0.1 ::1/' "$redis_conf" 2>/dev/null || true fi systemctl enable --now valkey >> "$LOG_FILE" 2>&1 || systemctl enable --now redis >> "$LOG_FILE" 2>&1 || true success "Valkey/Redis configurato" } configure_fail2ban() { info "Configurazione Fail2Ban..." # AelynaCP custom jail mkdir -p /etc/fail2ban/jail.d cat > /etc/fail2ban/jail.d/aelynacp.conf << 'F2BJAIL' [sshd] enabled = true maxretry = 5 bantime = 3600 [proftpd] enabled = true maxretry = 5 bantime = 3600 [postfix] enabled = true maxretry = 5 bantime = 3600 [dovecot] enabled = true maxretry = 5 bantime = 3600 F2BJAIL systemctl enable --now fail2ban >> "$LOG_FILE" 2>&1 || true success "Fail2Ban configurato (4 jail)" } configure_firewall() { info "Configurazione Firewall..." systemctl enable --now firewalld >> "$LOG_FILE" 2>&1 || true # Open all hosting ports local ports="80/tcp 443/tcp 21/tcp 25/tcp 110/tcp 143/tcp 465/tcp 587/tcp 993/tcp 995/tcp 53/tcp 53/udp 9443/tcp 9447/tcp 9448/tcp 8450/tcp" for port in $ports; do firewall-cmd --permanent --add-port="$port" >> "$LOG_FILE" 2>&1 || true done # FTP passive ports firewall-cmd --permanent --add-port=49152-65534/tcp >> "$LOG_FILE" 2>&1 || true firewall-cmd --reload >> "$LOG_FILE" 2>&1 || true success "Firewall configurato (porte hosting aperte)" } # ═══════════════════════════════════════════════════════════════════════════════ # STEP 5 — DEPLOY AELYNACP # ═══════════════════════════════════════════════════════════════════════════════ step_deploy() { step_header "5" "Deploy AelynaCP" # Create directories mkdir -p "$INSTALL_DIR" "$BIN_DIR" "$WEB_DIR" "$CONFIG_DIR" "$DATA_DIR" "$LOG_DIR" "$RUN_DIR" mkdir -p "${DATA_DIR}/backups" # Download release package via panel API (verifies license) info "Download AelynaCP v${AELYNACP_VERSION}..." local pkg_file="/tmp/aelynacp-${AELYNACP_VERSION}.tar.gz" local http_code http_code=$(curl -fsSL -o "$pkg_file" -w "%{http_code}" \ -H "Authorization: LicenseKey ${LICENSE_KEY}" \ "${DOWNLOAD_URL}" 2>> "$LOG_FILE") if [ ! -f "$pkg_file" ] || [ ! -s "$pkg_file" ] || [ "$http_code" != "200" ]; then # Fallback: direct download from get.aelynacp.com info "Tentativo download diretto..." curl -fsSL -o "$pkg_file" \ "https://get.aelynacp.com/aelynacp-${AELYNACP_VERSION}-linux-x86_64.tar.gz" >> "$LOG_FILE" 2>&1 fi if [ ! -f "$pkg_file" ] || [ ! -s "$pkg_file" ]; then fatal "Download fallito. Verifica la licenza e la connessione internet." fi # Verify checksum if available local expected_checksum expected_checksum=$(curl -s "${RELEASE_SERVER}/api/v1/updates/check?version=0.0.0&license_key=${LICENSE_KEY}&channel=stable" 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('checksum',''))" 2>/dev/null || echo "") if [ -n "$expected_checksum" ]; then local actual_checksum actual_checksum=$(sha256sum "$pkg_file" | awk '{print $1}') if [ "$expected_checksum" != "$actual_checksum" ]; then warn "Checksum mismatch — il file potrebbe essere stato alterato" else success "Checksum SHA256 verificato" fi fi success "Download completato ($(du -sh "$pkg_file" | awk '{print $1}'))" # Extract info "Estrazione pacchetto..." tar xzf "$pkg_file" -C "$INSTALL_DIR" >> "$LOG_FILE" 2>&1 success "Pacchetto estratto in ${INSTALL_DIR}" # Set permissions chmod +x "${BIN_DIR}/aelynacp-web" "${BIN_DIR}/aelynacp-agent" 2>/dev/null || true # Generate JWT secret JWT_SECRET=$(generate_hex 64) # Generate config info "Generazione configurazione..." cat > "${CONFIG_DIR}/aelynacp.toml" << TOMLCONFIG [server] hostname = "${HOSTNAME_INPUT}" primary_ip = "${PRIMARY_IP}" admin_port = 9443 user_port = 9447 api_port = 8450 nameservers = ["${NS1}", "${NS2}"] nameserver_ips = [] webserver = "nginx" max_upload_mb = 200 [database] url = "postgres://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}" max_connections = 20 [redis] url = "redis://127.0.0.1:6379" [license] key = "${LICENSE_KEY}" server_url = "${RELEASE_SERVER}" check_interval = 43200 [security] jwt_secret = "${JWT_SECRET}" jwt_expiration = 3600 session_expiration = 86400 max_login_attempts = 5 lockout_duration = 900 [paths] config_dir = "${CONFIG_DIR}" data_dir = "${DATA_DIR}" log_dir = "${LOG_DIR}" bin_dir = "${BIN_DIR}" web_dir = "${WEB_DIR}" home_dir = "/home" backup_dir = "${DATA_DIR}/backups" agent_socket = "${RUN_DIR}/agent.sock" [update] channel = "stable" auto_update = true auto_update_patch_only = true preferred_hour = 3 backup_before_update = true [notifications] enabled = true admin_email = "${ADMIN_EMAIL}" disk_full = true service_down = true update_available = true login_failed = true ssl_expiring = true backup_completed = true backup_failed = true daily_report = false [smtp] use_local = true host = "" port = 587 username = "" password = "" tls = true from_address = "noreply@${HOSTNAME_INPUT}" from_name = "AelynaCP" [defaults] php_version = "8.3" shell = "/sbin/nologin" auto_dns_zone = true TOMLCONFIG chmod 600 "${CONFIG_DIR}/aelynacp.toml" success "Configurazione generata" # Save primary IP for license fingerprinting echo "$PRIMARY_IP" > "${CONFIG_DIR}/primary_ip" # Run database migrations info "Esecuzione migrations database..." for migration in "${INSTALL_DIR}/migrations"/*.sql; do if [ -f "$migration" ]; then PGPASSWORD="$DB_PASS" psql -U "$DB_USER" -d "$DB_NAME" -f "$migration" >> "$LOG_FILE" 2>&1 || true fi done success "Schema database creato (21 tabelle)" # Create admin account with hashed password info "Creazione account admin..." local admin_hash admin_hash=$(python3 -c " import argon2 ph = argon2.PasswordHasher() print(ph.hash('${ADMIN_PASSWORD}')) " 2>/dev/null) if [ -n "$admin_hash" ]; then PGPASSWORD="$DB_PASS" psql -U "$DB_USER" -d "$DB_NAME" << ADMINSQL >> "$LOG_FILE" 2>&1 DELETE FROM admins WHERE username = 'admin'; INSERT INTO admins (username, password_hash, role, email) VALUES ('admin', '${admin_hash}', 'admin', '${ADMIN_EMAIL}'); ADMINSQL success "Account admin creato" else warn "Impossibile creare admin (argon2 non disponibile) — usare il setup wizard" fi # Set setup_completed = false for wizard PGPASSWORD="$DB_PASS" psql -U "$DB_USER" -d "$DB_NAME" -c \ "INSERT INTO server_settings (key, value) VALUES ('setup_completed', 'false') ON CONFLICT (key) DO UPDATE SET value = 'false';" >> "$LOG_FILE" 2>&1 || true # Version file echo "$AELYNACP_VERSION" > "${CONFIG_DIR}/.current_version" success "AelynaCP v${AELYNACP_VERSION} deployato" } # ═══════════════════════════════════════════════════════════════════════════════ # STEP 6 — SSL + START SERVICES # ═══════════════════════════════════════════════════════════════════════════════ step_start() { step_header "6" "SSL e avvio servizi" # Deploy Nginx panel configs (HTTP first, then HTTPS after cert) deploy_nginx_panels "http" # Start Nginx for certbot systemctl start nginx >> "$LOG_FILE" 2>&1 # Request SSL certificate info "Richiesta certificato SSL per ${HOSTNAME_INPUT}..." certbot certonly --webroot -w /var/www/default \ -d "$HOSTNAME_INPUT" \ --non-interactive --agree-tos \ --register-unsafely-without-email >> "$LOG_FILE" 2>&1 if [ -f "/etc/letsencrypt/live/${HOSTNAME_INPUT}/fullchain.pem" ]; then success "Certificato SSL ottenuto" # Reconfigure Nginx with HTTPS deploy_nginx_panels "https" else warn "Certificato SSL non ottenuto — pannello in HTTP" fi # Install systemd services info "Installazione servizi systemd..." cat > /etc/systemd/system/aelynacp-agent.service << AGENTSVC [Unit] Description=AelynaCP Agent - System Management Daemon After=network.target postgresql.service Wants=postgresql.service [Service] Type=simple User=root WorkingDirectory=${INSTALL_DIR} ExecStart=${BIN_DIR}/aelynacp-agent Restart=on-failure RestartSec=5 StandardOutput=append:${LOG_DIR}/agent.log StandardError=append:${LOG_DIR}/agent.log Environment=RUST_LOG=aelynacp_agent=info [Install] WantedBy=multi-user.target AGENTSVC cat > /etc/systemd/system/aelynacp-web.service << WEBSVC [Unit] Description=AelynaCP Web Server - Admin & User Panel After=network.target postgresql.service aelynacp-agent.service Wants=postgresql.service aelynacp-agent.service [Service] Type=simple User=root WorkingDirectory=${INSTALL_DIR} ExecStart=${BIN_DIR}/aelynacp-web Restart=on-failure RestartSec=5 StandardOutput=append:${LOG_DIR}/web.log StandardError=append:${LOG_DIR}/web.log Environment=RUST_LOG=aelynacp_web=info,tower_http=info [Install] WantedBy=multi-user.target WEBSVC systemctl daemon-reload run_quiet "Avvio AelynaCP Agent" systemctl enable --now aelynacp-agent sleep 2 run_quiet "Avvio AelynaCP Web" systemctl enable --now aelynacp-web sleep 2 # Reload Nginx with final config systemctl reload nginx >> "$LOG_FILE" 2>&1 # Health check sleep 3 if curl -s --max-time 5 http://localhost:8450/api/v1/health | grep -q '"ok"'; then success "Health check: OK" else warn "Health check fallito — verificare i log in ${LOG_DIR}/" fi } deploy_nginx_panels() { local mode="$1" # http or https if [ "$mode" = "https" ] && [ -f "/etc/letsencrypt/live/${HOSTNAME_INPUT}/fullchain.pem" ]; then cat > /etc/nginx/conf.d/aelynacp-panels.conf << NGINXHTTPS # Admin Panel (HTTPS) server { listen 9443 ssl; server_name ${HOSTNAME_INPUT}; ssl_certificate /etc/letsencrypt/live/${HOSTNAME_INPUT}/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/${HOSTNAME_INPUT}/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; root ${WEB_DIR}/admin; index index.html; add_header X-Frame-Options "DENY" always; add_header X-Content-Type-Options "nosniff" always; add_header Strict-Transport-Security "max-age=31536000" always; location / { try_files \$uri \$uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8450; proxy_http_version 1.1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } } # User Panel (HTTPS) server { listen 9447 ssl; client_max_body_size 256m; server_name ${HOSTNAME_INPUT}; ssl_certificate /etc/letsencrypt/live/${HOSTNAME_INPUT}/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/${HOSTNAME_INPUT}/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; root ${WEB_DIR}/user; index index.html; add_header X-Frame-Options "DENY" always; add_header X-Content-Type-Options "nosniff" always; location / { try_files \$uri \$uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8450; proxy_http_version 1.1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; } } # Webmail (HTTPS) server { listen 9448 ssl; server_name ${HOSTNAME_INPUT}; ssl_certificate /etc/letsencrypt/live/${HOSTNAME_INPUT}/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/${HOSTNAME_INPUT}/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; root ${WEB_DIR}/webmail; index index.html; location / { try_files \$uri \$uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8450; proxy_http_version 1.1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; } } # HTTP redirect for certbot + panel redirect server { listen 80; server_name ${HOSTNAME_INPUT}; location /.well-known/acme-challenge/ { root /var/www/default; allow all; } location / { return 301 https://\$host:9443\$request_uri; } } NGINXHTTPS else # HTTP-only fallback cat > /etc/nginx/conf.d/aelynacp-panels.conf << NGINXHTTP server { listen 9443; server_name _; root ${WEB_DIR}/admin; index index.html; location / { try_files \$uri \$uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8450; proxy_http_version 1.1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; } } server { listen 9447; client_max_body_size 256m; server_name _; root ${WEB_DIR}/user; index index.html; location / { try_files \$uri \$uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8450; proxy_http_version 1.1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; } } server { listen 9448; server_name _; root ${WEB_DIR}/webmail; index index.html; location / { try_files \$uri \$uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8450; proxy_http_version 1.1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; } } NGINXHTTP fi nginx -t >> "$LOG_FILE" 2>&1 || warn "Nginx config test failed" } # ═══════════════════════════════════════════════════════════════════════════════ # STEP 7 — COMPLETION # ═══════════════════════════════════════════════════════════════════════════════ step_complete() { step_header "7" "Installazione completata" local protocol="http" if [ -f "/etc/letsencrypt/live/${HOSTNAME_INPUT}/fullchain.pem" ]; then protocol="https" fi echo "" echo -e " ${MAGENTA}${BOLD}═══════════════════════════════════════════════════${NC}" echo -e " ${WHITE}${BOLD} AELYNA™CP installato con successo!${NC}" echo -e " ${MAGENTA}${BOLD}═══════════════════════════════════════════════════${NC}" echo "" echo -e " ${WHITE}Admin Panel:${NC} ${CYAN}${protocol}://${HOSTNAME_INPUT}:9443${NC}" echo -e " ${WHITE}User Panel:${NC} ${CYAN}${protocol}://${HOSTNAME_INPUT}:9447${NC}" echo -e " ${WHITE}Webmail:${NC} ${CYAN}${protocol}://${HOSTNAME_INPUT}:9448${NC}" echo "" echo -e " ${WHITE}Username:${NC} ${GREEN}admin${NC}" echo -e " ${WHITE}Password:${NC} ${GREEN}(quella inserita durante il setup)${NC}" echo "" echo -e " ${WHITE}Versione:${NC} ${AELYNACP_VERSION}" echo -e " ${WHITE}Licenza:${NC} ${LICENSE_KEY}" echo -e " ${WHITE}Log install:${NC} ${LOG_FILE}" echo "" echo -e " ${MAGENTA}${BOLD}═══════════════════════════════════════════════════${NC}" echo "" echo -e " ${YELLOW}Completa il setup iniziale accedendo al pannello admin.${NC}" echo "" } # ═══════════════════════════════════════════════════════════════════════════════ # MAIN # ═══════════════════════════════════════════════════════════════════════════════ main() { show_logo preflight_checks step_license step_configuration step_packages step_configure step_deploy step_start step_complete } main "$@"