#!/bin/bash # # SecZim - Official Installation Script (Secure) # Copyright (c) 2024 SecZim. All rights reserved. # # Usage: curl -fsSL https://get.seczim.com/install.sh | sudo bash -s -- YOUR-LICENSE-KEY # curl -fsSL https://get.seczim.com/install.sh | sudo bash -s -- --verbose YOUR-LICENSE-KEY # curl -fsSL https://get.seczim.com/install.sh | sudo bash -s -- --upgrade # set -e # Change to a known-good directory to avoid "getcwd: cannot access parent directories" errors # This happens when the script is run from a directory that was deleted (e.g., after uninstall) cd /tmp 2>/dev/null || cd / # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' # Verbose mode (off by default) VERBOSE=false # Upgrade mode (off by default) UPGRADE_MODE=false # Configuration SECZIM_VERSION="3.0.0" SECZIM_API_URL="https://licenses.seczim.com/api/v1" INSTALL_DIR="/opt/seczim" CONFIG_DIR="/etc/seczim" LOG_DIR="/var/log/seczim" DATA_DIR="/var/lib/seczim" # Get public IP (try multiple services) get_public_ip() { local ip="" # Try ipinfo.io first ip=$(curl -s --max-time 5 https://ipinfo.io/ip 2>/dev/null) if [ -z "$ip" ]; then # Fallback to ifconfig.me ip=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null) fi if [ -z "$ip" ]; then # Fallback to icanhazip.com ip=$(curl -s --max-time 5 https://icanhazip.com 2>/dev/null) fi if [ -z "$ip" ]; then # Last resort: use local IP ip=$(hostname -I | awk '{print $1}') fi echo "$ip" } # Generate unique installation ID based on machine characteristics get_installation_id() { local machine_id="" # Try to get machine-id (unique per system) if [ -f /etc/machine-id ]; then machine_id=$(cat /etc/machine-id) elif [ -f /var/lib/dbus/machine-id ]; then machine_id=$(cat /var/lib/dbus/machine-id) else # Fallback: generate from hostname + first MAC address machine_id=$(hostname)$(ip link show 2>/dev/null | grep -o 'link/ether [^ ]*' | head -1 | awk '{print $2}' | tr -d ':') fi # Create a hash of the machine_id echo "$machine_id" | sha256sum | cut -c1-32 } # Print functions print_info() { echo -e "${BLUE}[INFO]${NC} $1"; } print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } print_error() { echo -e "${RED}[ERROR]${NC} $1"; } print_verbose() { if [ "$VERBOSE" = true ]; then echo -e "${CYAN}[DEBUG]${NC} $1" fi } # Mask sensitive data for verbose output mask_license_key() { local key="$1" if [ ${#key} -gt 8 ]; then echo "${key:0:4}****${key: -4}" else echo "****" fi } mask_token() { local token="$1" if [ ${#token} -gt 8 ]; then echo "${token:0:8}..." else echo "***" fi } print_banner() { echo "" echo -e "${BLUE}========================================================${NC}" echo -e "${BLUE} ${NC}" echo -e "${BLUE} SecZim Installer v$SECZIM_VERSION ${NC}" echo -e "${BLUE} Email Security System for Zimbra/Postfix ${NC}" echo -e "${BLUE} ${NC}" echo -e "${BLUE}========================================================${NC}" echo "" } # Check if running as root check_root() { if [ "$EUID" -ne 0 ]; then print_error "This script must be run as root" exit 1 fi } # Check operating system compatibility check_os_compatibility() { local os_name="" local os_version="" local kernel_version="" # Get kernel version kernel_version=$(uname -r | cut -d. -f1-2) # Try to detect OS if [ -f /etc/os-release ]; then . /etc/os-release os_name="$ID" os_version="$VERSION_ID" elif [ -f /etc/redhat-release ]; then # CentOS 6 and older don't have /etc/os-release if grep -qi "centos" /etc/redhat-release; then os_name="centos" os_version=$(grep -oE '[0-9]+\.[0-9]+' /etc/redhat-release | head -1) elif grep -qi "red hat" /etc/redhat-release; then os_name="rhel" os_version=$(grep -oE '[0-9]+\.[0-9]+' /etc/redhat-release | head -1) fi fi print_verbose "Detected OS: $os_name $os_version (kernel $kernel_version)" # Check for unsupported old systems case "$os_name" in centos|rhel) local major_version=$(echo "$os_version" | cut -d. -f1) if [ -n "$major_version" ] && [ "$major_version" -lt 7 ]; then echo "" print_error "Unsupported operating system: $os_name $os_version" echo "" echo -e "${RED}SecZim requires CentOS/RHEL 7 or later.${NC}" echo "" echo "Your system ($os_name $os_version) is too old because:" echo " - Docker requires kernel 3.10+ (you have kernel $kernel_version)" echo " - CentOS 6 reached End of Life in November 2020" echo " - Modern security features are not available" echo "" echo "Supported operating systems:" echo " - CentOS 7, 8 Stream" echo " - RHEL 7, 8, 9" echo " - Rocky Linux 8, 9" echo " - AlmaLinux 8, 9" echo " - Ubuntu 18.04, 20.04, 22.04" echo " - Debian 10, 11, 12" echo "" exit 1 fi ;; ubuntu) local major_version=$(echo "$os_version" | cut -d. -f1) if [ -n "$major_version" ] && [ "$major_version" -lt 18 ]; then print_error "Unsupported Ubuntu version: $os_version (requires 18.04+)" exit 1 fi ;; debian) local major_version=$(echo "$os_version" | cut -d. -f1) if [ -n "$major_version" ] && [ "$major_version" -lt 10 ]; then print_error "Unsupported Debian version: $os_version (requires 10+)" exit 1 fi ;; esac # Check kernel version (Docker requires 3.10+) local kernel_major=$(echo "$kernel_version" | cut -d. -f1) local kernel_minor=$(echo "$kernel_version" | cut -d. -f2) if [ "$kernel_major" -lt 3 ] || ([ "$kernel_major" -eq 3 ] && [ "$kernel_minor" -lt 10 ]); then print_error "Kernel version $kernel_version is too old (requires 3.10+)" echo "Docker and containerization require a modern kernel." exit 1 fi print_verbose "OS compatibility check passed" } # Check if port 10035 is already in use check_port_available() { print_info "Checking if port 10035 is available..." local port_in_use="" # Try ss first (modern systems) if command -v ss &> /dev/null; then port_in_use=$(ss -tlnp 2>/dev/null | grep ":10035 " || true) # Fallback to netstat elif command -v netstat &> /dev/null; then port_in_use=$(netstat -tlnp 2>/dev/null | grep ":10035 " || true) fi if [ -n "$port_in_use" ]; then print_error "Port 10035 is already in use!" echo "" echo "Another service is currently using port 10035:" echo "$port_in_use" echo "" echo "Please stop the service using this port before installing SecZim." echo "If this is a previous SecZim installation, run the uninstall script first:" echo " sudo seczim-uninstall" echo "" exit 1 fi print_success "Port 10035 is available" } # Check if port 10031 is in use (cbpolicyd or similar) check_existing_policy_server() { local port_10031_in_use="" if command -v ss &> /dev/null; then port_10031_in_use=$(ss -tlnp 2>/dev/null | grep ":10031 " || true) elif command -v netstat &> /dev/null; then port_10031_in_use=$(netstat -tlnp 2>/dev/null | grep ":10031 " || true) fi if [ -n "$port_10031_in_use" ]; then print_warning "Detected existing policy server on port 10031 (possibly cbpolicyd)" echo "" echo "Service on port 10031:" echo "$port_10031_in_use" echo "" echo "SecZim uses port 10035 and can coexist with existing policy servers." echo "However, you may want to disable the existing policy server to avoid conflicts:" echo "" echo " For Zimbra:" echo " su - zimbra -c 'zmprov ms \$(hostname) zimbraCBPolicydEnabled FALSE'" echo " su - zimbra -c 'zmcbpolicydctl stop'" echo "" echo " For Postfix:" echo " Remove 'check_policy_service inet:127.0.0.1:10031' from /etc/postfix/main.cf" echo " systemctl stop cbpolicyd # or the service name shown above" echo "" print_info "Continuing installation on port 10035..." echo "" fi } # Prompt for admin dashboard password prompt_admin_password() { echo "" print_info "Setting up admin dashboard credentials..." echo "" echo -e "${CYAN}The SecZim dashboard requires an admin password.${NC}" echo -e "${CYAN}This password will be used to login at http://your-server:8880${NC}" echo "" while true; do echo -n "Enter admin password (min 8 characters): " read -s ADMIN_PASSWORD < /dev/tty echo "" if [ ${#ADMIN_PASSWORD} -lt 8 ]; then print_error "Password must be at least 8 characters" continue fi echo -n "Confirm admin password: " read -s ADMIN_PASSWORD_CONFIRM < /dev/tty echo "" if [ "$ADMIN_PASSWORD" != "$ADMIN_PASSWORD_CONFIRM" ]; then print_error "Passwords do not match" continue fi break done # Store password temporarily (will be hashed and saved to DB later) export ADMIN_PASSWORD print_success "Admin password set" } # Configure admin user in database configure_admin_user() { print_info "Configuring admin user..." local MYSQL_ROOT_PASS=$(grep 'MYSQL_ROOT_PASSWORD' "$CONFIG_DIR/.credentials" 2>/dev/null | cut -d'=' -f2) if [ -z "$MYSQL_ROOT_PASS" ]; then print_warning "Could not find MySQL credentials - skipping admin user setup" return 1 fi if [ -z "$ADMIN_PASSWORD" ]; then print_warning "No admin password set - skipping admin user setup" return 1 fi # Generate bcrypt hash local PASSWORD_HASH="" print_verbose "Generating password hash..." # Method 1: Try htpasswd (most common on servers) if [ -z "$PASSWORD_HASH" ] && command -v htpasswd &> /dev/null; then PASSWORD_HASH=$(htpasswd -bnBC 10 "" "$ADMIN_PASSWORD" 2>/dev/null | tr -d ':\n') fi # Method 2: Try Python with bcrypt if [ -z "$PASSWORD_HASH" ] && command -v python3 &> /dev/null; then PASSWORD_HASH=$(python3 -c " try: import bcrypt password = '''$ADMIN_PASSWORD''' hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(10)) print(hashed.decode('utf-8')) except: pass " 2>/dev/null) fi # Method 3: Use Docker to generate hash if [ -z "$PASSWORD_HASH" ]; then print_info "Using Docker to generate password hash..." PASSWORD_HASH=$(docker run --rm --entrypoint htpasswd httpd:2-alpine -bnBC 10 "" "$ADMIN_PASSWORD" 2>/dev/null | tr -d ':\n') fi if [ -z "$PASSWORD_HASH" ]; then print_error "Could not generate password hash" print_warning "You can set the admin password later using the web interface" return 1 fi print_verbose "Password hash generated successfully" # Wait for admin_users table to exist (init.sql may still be running) print_verbose "Waiting for admin_users table..." local table_retries=15 while [ $table_retries -gt 0 ]; do if docker exec -e MYSQL_PWD="${MYSQL_ROOT_PASS}" seczim-mysql mysql -uroot -e "SELECT 1 FROM seczim.admin_users LIMIT 1" &>/dev/null; then break fi sleep 2 table_retries=$((table_retries - 1)) done if [ $table_retries -eq 0 ]; then print_warning "admin_users table not ready, retrying..." sleep 5 fi # Insert or update admin user in database # Use printf to avoid heredoc variable interpolation issues with $ in bcrypt hash printf "INSERT INTO admin_users (Username, PasswordHash, Email, IsActive) VALUES ('admin', '%s', 'admin@localhost', 1) ON DUPLICATE KEY UPDATE PasswordHash = '%s';\n" "$PASSWORD_HASH" "$PASSWORD_HASH" > /tmp/set_admin_pass.sql if docker exec -i -e MYSQL_PWD="${MYSQL_ROOT_PASS}" seczim-mysql mysql -uroot seczim < /tmp/set_admin_pass.sql 2>/dev/null; then print_success "Admin user configured" else print_warning "Could not configure admin user - table may not exist yet" print_info "You can set the admin password later via: docker exec -i seczim-mysql mysql -uroot -p seczim" fi rm -f /tmp/set_admin_pass.sql # Clear password from environment unset ADMIN_PASSWORD } # Validate license with actual API call validate_license() { local LICENSE_KEY=$1 if [ -z "$LICENSE_KEY" ]; then print_error "License key is required" echo "Usage: $0 [--verbose] YOUR-LICENSE-KEY" echo "" echo "Options:" echo " --verbose Show detailed debug information (safe for sharing)" echo "" echo "Don't have a license? Get one at https://seczim.com/pricing" exit 1 fi print_info "Validating license key..." print_verbose "License key: $(mask_license_key "$LICENSE_KEY")" # Get server info local SERVER_IP=$(get_public_ip) local INSTALLATION_ID=$(get_installation_id) print_verbose "Server hostname: $(hostname)" print_verbose "Server IP: $SERVER_IP" print_verbose "Installation ID: $INSTALLATION_ID" print_verbose "SecZim version: $SECZIM_VERSION" print_verbose "API endpoint: $SECZIM_API_URL/license/verify" # Call license server API print_verbose "Sending license verification request..." local HTTP_CODE local RESPONSE if [ "$VERBOSE" = true ]; then # Capture both response and HTTP code RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$SECZIM_API_URL/license/verify" \ -H "Content-Type: application/json" \ -d "{ \"license_key\": \"$LICENSE_KEY\", \"server_hostname\": \"$(hostname)\", \"server_ip\": \"$SERVER_IP\", \"installation_id\": \"$INSTALLATION_ID\", \"version\": \"$SECZIM_VERSION\" }") HTTP_CODE=$(echo "$RESPONSE" | tail -1) RESPONSE=$(echo "$RESPONSE" | sed '$d') print_verbose "HTTP response code: $HTTP_CODE" else RESPONSE=$(curl -s -X POST "$SECZIM_API_URL/license/verify" \ -H "Content-Type: application/json" \ -d "{ \"license_key\": \"$LICENSE_KEY\", \"server_hostname\": \"$(hostname)\", \"server_ip\": \"$SERVER_IP\", \"installation_id\": \"$INSTALLATION_ID\", \"version\": \"$SECZIM_VERSION\" }") fi # Check if license is valid local VALID=$(echo "$RESPONSE" | grep -o '"valid":true' || true) print_verbose "License valid: $([ -n "$VALID" ] && echo 'yes' || echo 'no')" if [ -z "$VALID" ]; then print_error "Invalid or expired license key" if [ "$VERBOSE" = true ]; then # Show response but mask any sensitive data local SAFE_RESPONSE=$(echo "$RESPONSE" | sed 's/"license_key":"[^"]*"/"license_key":"***"/g') print_verbose "API response: $SAFE_RESPONSE" fi echo "$RESPONSE" exit 1 fi # Extract download token DOWNLOAD_TOKEN=$(echo "$RESPONSE" | grep -o '"download_token":"[^"]*"' | cut -d'"' -f4) print_verbose "Download token received: $([ -n "$DOWNLOAD_TOKEN" ] && echo "$(mask_token "$DOWNLOAD_TOKEN")" || echo 'none')" if [ -z "$DOWNLOAD_TOKEN" ]; then print_error "Failed to obtain download token" print_verbose "Full response: $RESPONSE" exit 1 fi # Extract license info LICENSE_PLAN=$(echo "$RESPONSE" | grep -o '"plan":"[^"]*"' | cut -d'"' -f4) LICENSE_EXPIRES=$(echo "$RESPONSE" | grep -o '"expires_at":"[^"]*"' | cut -d'"' -f4) LICENSE_EMAIL=$(echo "$RESPONSE" | grep -o '"email":"[^"]*"' | cut -d'"' -f4) LICENSE_COMPANY=$(echo "$RESPONSE" | grep -o '"company":"[^"]*"' | cut -d'"' -f4) LICENSE_MAX_DOMAINS=$(echo "$RESPONSE" | grep -o '"max_domains":[0-9]*' | cut -d':' -f2) print_verbose "Plan: $LICENSE_PLAN" print_verbose "Expires: $LICENSE_EXPIRES" print_verbose "Max domains: $LICENSE_MAX_DOMAINS" print_success "License validated: $LICENSE_PLAN plan (expires: $LICENSE_EXPIRES)" # Save license key temporarily for download echo "$LICENSE_KEY" > /tmp/seczim_license.key # Save full license info for later export SECZIM_LICENSE_KEY="$LICENSE_KEY" export SECZIM_LICENSE_PLAN="$LICENSE_PLAN" export SECZIM_LICENSE_EXPIRES="$LICENSE_EXPIRES" export SECZIM_LICENSE_EMAIL="$LICENSE_EMAIL" export SECZIM_LICENSE_COMPANY="$LICENSE_COMPANY" export SECZIM_LICENSE_MAX_DOMAINS="$LICENSE_MAX_DOMAINS" } # Save license info to file save_license_info() { print_info "Saving license information..." # Get server info local SERVER_IP=$(get_public_ip) local INSTALLATION_ID=$(get_installation_id) cat > "$CONFIG_DIR/license.json" < "$CONFIG_DIR/.license" chmod 600 "$CONFIG_DIR/.license" print_success "License information saved to $CONFIG_DIR/license.json" } # Install Docker install_docker() { # Check if Docker is installed and has compose plugin if command -v docker &> /dev/null; then if command -v docker-compose &> /dev/null || docker compose version &> /dev/null; then print_success "Docker already installed" return fi fi print_info "Installing Docker..." if [ -f /etc/os-release ]; then . /etc/os-release DISTRO=$ID fi case "$DISTRO" in ubuntu|debian) apt-get update -qq apt-get install -y ca-certificates curl gnupg curl -fsSL https://get.docker.com | bash ;; centos|rhel|fedora) curl -fsSL https://get.docker.com | bash systemctl start docker systemctl enable docker ;; rocky|almalinux) # Rocky Linux / AlmaLinux specific installation yum install -y yum-utils yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin systemctl start docker systemctl enable docker ;; *) print_error "Unsupported distribution: $DISTRO" print_info "Trying generic Docker installation..." curl -fsSL https://get.docker.com | bash || { print_error "Failed to install Docker" exit 1 } systemctl start docker 2>/dev/null || true systemctl enable docker 2>/dev/null || true ;; esac print_success "Docker installed" } # Create directories create_directories() { print_info "Creating directories..." mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR/mysql" "$DATA_DIR/redis" print_success "Directories created" } # Download database schema with authentication download_schema() { print_info "Downloading database schema..." # Use download token for authentication if ! curl -fsSL "https://downloads.seczim.com/database/schema.sql" \ -H "Authorization: Bearer $DOWNLOAD_TOKEN" \ -o "$INSTALL_DIR/init.sql"; then print_error "Failed to download database schema" print_error "Your license may not have download permissions" exit 1 fi if ! grep -q "CREATE TABLE" "$INSTALL_DIR/init.sql"; then print_error "Invalid schema file" exit 1 fi local TABLE_COUNT=$(grep -c 'CREATE TABLE' "$INSTALL_DIR/init.sql") print_success "Schema downloaded ($TABLE_COUNT tables)" } # Generate docker-compose.yml generate_docker_compose() { print_info "Generating docker-compose.yml..." LICENSE_KEY=$(cat /tmp/seczim_license.key) # Check if credentials already exist (reinstall scenario) if [ -f "$CONFIG_DIR/.credentials" ]; then print_info "Found existing credentials, reusing them..." MYSQL_ROOT_PASS=$(grep 'MYSQL_ROOT_PASSWORD' "$CONFIG_DIR/.credentials" | cut -d'=' -f2) MYSQL_USER_PASS=$(grep 'MYSQL_USER_PASSWORD' "$CONFIG_DIR/.credentials" | cut -d'=' -f2) REDIS_PASS=$(grep 'REDIS_PASSWORD' "$CONFIG_DIR/.credentials" | cut -d'=' -f2) fi # Generate new passwords only if not found if [ -z "$MYSQL_ROOT_PASS" ]; then MYSQL_ROOT_PASS=$(openssl rand -hex 16) fi if [ -z "$MYSQL_USER_PASS" ]; then MYSQL_USER_PASS=$(openssl rand -hex 16) fi if [ -z "$REDIS_PASS" ]; then REDIS_PASS=$(openssl rand -hex 16) fi cat > "$INSTALL_DIR/docker-compose.yml" < "$CONFIG_DIR/.credentials" </dev/null)" ]; then print_info "Found existing MySQL data, checking credentials..." EXISTING_DATA_DETECTED=true # Start containers docker compose up -d sleep 5 # Test if we can connect with the new credentials local MYSQL_ROOT_PASS=$(grep 'MYSQL_ROOT_PASSWORD' "$CONFIG_DIR/.credentials" | cut -d'=' -f2) if ! docker exec -e MYSQL_PWD="${MYSQL_ROOT_PASS}" seczim-mysql mysql -uroot -e "SELECT 1" &>/dev/null; then print_warning "Existing MySQL data has different credentials" print_info "Cleaning up old data for fresh install..." # Stop and remove containers and volumes docker compose down -v 2>/dev/null || true # Remove old MySQL data rm -rf "$DATA_DIR/mysql"/* # Start fresh - this is now a clean install EXISTING_DATA_DETECTED=false docker compose up -d fi else docker compose up -d fi print_info "Waiting for MySQL to be ready..." # Wait for MySQL to accept connections (up to 60 seconds) local retries=30 while [ $retries -gt 0 ]; do if docker exec seczim-mysql mysqladmin ping -h localhost --silent 2>/dev/null; then break fi sleep 2 retries=$((retries - 1)) done if [ $retries -eq 0 ]; then print_error "MySQL failed to start" docker compose logs mysql exit 1 fi # Read credentials from file local MYSQL_ROOT_PASS=$(grep 'MYSQL_ROOT_PASSWORD' "$CONFIG_DIR/.credentials" | cut -d'=' -f2) local MYSQL_USER_PASS=$(grep 'MYSQL_USER_PASSWORD' "$CONFIG_DIR/.credentials" | cut -d'=' -f2) # Wait for MySQL to finish initializing (init.sql scripts) print_info "Waiting for database initialization..." retries=30 while [ $retries -gt 0 ]; do if docker exec -e MYSQL_PWD="${MYSQL_ROOT_PASS}" seczim-mysql mysql -uroot -e "SELECT 1 FROM seczim.policies LIMIT 1" &>/dev/null; then break fi sleep 2 retries=$((retries - 1)) done if [ $retries -eq 0 ]; then print_warning "Database initialization taking longer than expected, continuing..." fi print_success "MySQL is ready" if docker compose ps | grep -q "Up"; then print_success "Services started successfully" # Grant MySQL user access from host (needed for policy server running outside Docker) print_info "Configuring database access..." # Create SQL file with commands (passwords are hex, no special chars) cat > /tmp/grant_access.sql </dev/null; then print_success "Database access configured" break fi sleep 2 grant_retries=$((grant_retries - 1)) done if [ $grant_retries -eq 0 ]; then print_warning "Could not configure database access (may already be configured)" fi rm -f /tmp/grant_access.sql # Upgrade database schema if needed (add new columns) print_info "Checking for schema updates..." upgrade_database_schema else print_error "Failed to start services" docker compose logs exit 1 fi } # Upgrade database schema (for existing installations) upgrade_database_schema() { print_info "Downloading database migrations..." local MIGRATIONS_URL="https://downloads.seczim.com/database/migrations.sql" print_verbose "Migrations URL: $MIGRATIONS_URL" print_verbose "Using token: $(mask_token "$DOWNLOAD_TOKEN")" if ! curl -fsSL "$MIGRATIONS_URL" \ -H "Authorization: Bearer $DOWNLOAD_TOKEN" \ -o /tmp/schema_upgrade.sql 2>/dev/null; then print_error "Failed to download migrations file" return 1 fi # Verify we got a SQL file, not an error response if ! grep -q "ALTER TABLE\|CREATE TABLE\|CREATE INDEX" /tmp/schema_upgrade.sql 2>/dev/null; then print_error "Downloaded file does not appear to be valid SQL" print_verbose "Content: $(head -5 /tmp/schema_upgrade.sql)" rm -f /tmp/schema_upgrade.sql return 1 fi print_verbose "Downloaded migrations.sql ($(wc -c < /tmp/schema_upgrade.sql) bytes)" local MYSQL_ROOT_PASS=$(grep 'MYSQL_ROOT_PASSWORD' "$CONFIG_DIR/.credentials" 2>/dev/null | cut -d'=' -f2) if [ -z "$MYSQL_ROOT_PASS" ]; then print_warning "Could not find MySQL credentials - skipping schema upgrade" rm -f /tmp/schema_upgrade.sql return 1 fi print_info "Applying database migrations..." if docker exec -i -e MYSQL_PWD="${MYSQL_ROOT_PASS}" seczim-mysql mysql -uroot seczim < /tmp/schema_upgrade.sql 2>/dev/null; then print_success "Database schema updated" else print_warning "Could not update schema (may already be up to date)" fi rm -f /tmp/schema_upgrade.sql } # Detect MTA type detect_mta() { print_info "Detecting Mail Transfer Agent..." if [ -d "/opt/zimbra" ]; then MTA_TYPE="zimbra" ZIMBRA_USER=$(ls -ld /opt/zimbra 2>/dev/null | awk '{print $3}') print_success "Detected Zimbra mail server" return 0 elif [ -f "/etc/postfix/main.cf" ]; then MTA_TYPE="postfix" print_success "Detected Postfix mail server" return 0 else print_warning "No supported MTA detected (Zimbra/Postfix)" print_warning "You will need to manually configure your mail server" MTA_TYPE="none" return 1 fi } # Download SecZim binary download_seczim_binary() { print_info "Downloading SecZim policy server binary..." local DOWNLOAD_URL="https://downloads.seczim.com/binaries/seczim-daemon-v3.0.0-linux-amd64" print_verbose "Download URL: $DOWNLOAD_URL" print_verbose "Target: $INSTALL_DIR/seczim-daemon" print_verbose "Using token: $(mask_token "$DOWNLOAD_TOKEN")" # Remove existing binary if present (curl can't overwrite) rm -f "$INSTALL_DIR/seczim-daemon" local CURL_OUTPUT if [ "$VERBOSE" = true ]; then if ! CURL_OUTPUT=$(curl -fsSL -w "\nHTTP_CODE:%{http_code}" "$DOWNLOAD_URL" \ -H "Authorization: Bearer $DOWNLOAD_TOKEN" \ -o "$INSTALL_DIR/seczim-daemon" 2>&1); then print_error "Failed to download SecZim binary" print_verbose "Curl output: $CURL_OUTPUT" print_error "Your license may not have download permissions" exit 1 fi print_verbose "Download completed: $(echo "$CURL_OUTPUT" | grep HTTP_CODE || echo 'OK')" else if ! curl -fsSL "$DOWNLOAD_URL" \ -H "Authorization: Bearer $DOWNLOAD_TOKEN" \ -o "$INSTALL_DIR/seczim-daemon"; then print_error "Failed to download SecZim binary" print_error "Your license may not have download permissions" exit 1 fi fi chmod +x "$INSTALL_DIR/seczim-daemon" print_verbose "Set executable permissions" # Verify binary print_verbose "Verifying binary..." if ! "$INSTALL_DIR/seczim-daemon" --version &>/dev/null; then print_error "Downloaded binary is not valid" print_verbose "Binary size: $(ls -lh "$INSTALL_DIR/seczim-daemon" 2>/dev/null | awk '{print $5}')" exit 1 fi print_verbose "Binary size: $(ls -lh "$INSTALL_DIR/seczim-daemon" | awk '{print $5}')" print_success "SecZim binary downloaded and verified" } # Download SecZim API server binary download_seczim_api() { print_info "Downloading SecZim API server..." local DOWNLOAD_URL="https://downloads.seczim.com/binaries/seczim-api-v3.0.0-linux-amd64" print_verbose "Download URL: $DOWNLOAD_URL" print_verbose "Target: $INSTALL_DIR/seczim-api" # Remove existing binary if present rm -f "$INSTALL_DIR/seczim-api" if ! curl -fsSL "$DOWNLOAD_URL" \ -H "Authorization: Bearer $DOWNLOAD_TOKEN" \ -o "$INSTALL_DIR/seczim-api"; then print_error "Failed to download SecZim API binary" print_verbose "Check network connectivity and token validity" exit 1 fi chmod +x "$INSTALL_DIR/seczim-api" # Verify binary print_verbose "Verifying API binary..." if ! "$INSTALL_DIR/seczim-api" --version &>/dev/null; then print_error "Downloaded API binary is not valid" print_verbose "Binary size: $(ls -lh "$INSTALL_DIR/seczim-api" 2>/dev/null | awk '{print $5}')" exit 1 fi print_verbose "API binary size: $(ls -lh "$INSTALL_DIR/seczim-api" | awk '{print $5}')" print_success "SecZim API server downloaded and verified" } # Download and install web frontend download_frontend() { print_info "Downloading SecZim Web UI..." FRONTEND_DIR="$INSTALL_DIR/web/frontend" mkdir -p "$FRONTEND_DIR/dist" print_verbose "Frontend directory: $FRONTEND_DIR/dist" # Remove existing dist contents if present rm -rf "$FRONTEND_DIR/dist/*" local DOWNLOAD_URL="https://downloads.seczim.com/binaries/seczim-frontend-v3.0.0.tar.gz" print_verbose "Download URL: $DOWNLOAD_URL" if ! curl -fsSL "$DOWNLOAD_URL" \ -H "Authorization: Bearer $DOWNLOAD_TOKEN" \ -o "/tmp/seczim-frontend.tar.gz"; then print_error "Failed to download SecZim frontend" print_verbose "Check network connectivity and token validity" exit 1 fi print_verbose "Archive size: $(ls -lh /tmp/seczim-frontend.tar.gz | awk '{print $5}')" # Extract frontend into dist directory print_verbose "Extracting frontend..." tar -xzf /tmp/seczim-frontend.tar.gz -C "$FRONTEND_DIR/dist" rm -f /tmp/seczim-frontend.tar.gz print_verbose "Frontend files: $(ls -1 "$FRONTEND_DIR/dist" | wc -l) items" print_success "SecZim Web UI installed to $FRONTEND_DIR/dist" } # Generate systemd service for API server generate_api_systemd_service() { print_info "Creating API server systemd service..." cat > /etc/systemd/system/seczim-api.service </dev/null || true retries=$((retries - 1)) done print_error "Failed to start SecZim API server" systemctl status seczim-api --no-pager cat /var/log/seczim/seczim-api.log 2>/dev/null | tail -10 exit 1 } # Generate SecZim configuration generate_config() { print_info "Generating SecZim configuration..." LICENSE_KEY=$(cat /tmp/seczim_license.key) REDIS_PASS=$(grep 'REDIS_PASSWORD' "$CONFIG_DIR/.credentials" | cut -d'=' -f2) MYSQL_PASS=$(grep 'MYSQL_USER_PASSWORD' "$CONFIG_DIR/.credentials" | cut -d'=' -f2) # Build MySQL DSN MYSQL_DSN="seczim:${MYSQL_PASS}@tcp(127.0.0.1:3306)/seczim?parseTime=true&charset=utf8mb4" # Build Redis URL REDIS_URL="redis://:${REDIS_PASS}@127.0.0.1:6379/0" # Generate JWT secret JWT_SECRET=$(openssl rand -hex 32) cat > "$CONFIG_DIR/seczim.yaml" < /etc/systemd/system/seczim.service </dev/null") || true echo "$CURRENT_RESTRICTIONS" > "$CONFIG_DIR/zimbra-smtpd-restrictions.backup" print_info "Backed up smtpd_recipient_restrictions" # Check if policy server already configured if echo "$CURRENT_RESTRICTIONS" | grep -q "check_policy_service inet:127.0.0.1:10035"; then print_warning "Zimbra already has SecZim policy server configured" return 0 fi print_info "Adding SecZim policy server to Postfix restrictions..." if [ -n "$CURRENT_RESTRICTIONS" ]; then # Insert policy check BEFORE permit_sasl_authenticated so all mail goes through SecZim # This ensures authenticated users (webmail) are also filtered # Remove any existing policy check first local CLEAN_RESTRICTIONS=$(echo "$CURRENT_RESTRICTIONS" | sed 's/,*\s*check_policy_service inet:127.0.0.1:10035//g') # Insert after reject_* rules but before permit_sasl_authenticated if echo "$CLEAN_RESTRICTIONS" | grep -q "permit_sasl_authenticated"; then # Insert before permit_sasl_authenticated local NEW_RESTRICTIONS=$(echo "$CLEAN_RESTRICTIONS" | sed 's/permit_sasl_authenticated/check_policy_service inet:127.0.0.1:10035, permit_sasl_authenticated/') elif echo "$CLEAN_RESTRICTIONS" | grep -q "permit_mynetworks"; then # Insert before permit_mynetworks local NEW_RESTRICTIONS=$(echo "$CLEAN_RESTRICTIONS" | sed 's/permit_mynetworks/check_policy_service inet:127.0.0.1:10035, permit_mynetworks/') else # Fallback: add at the beginning after any reject rules local NEW_RESTRICTIONS="check_policy_service inet:127.0.0.1:10035, ${CLEAN_RESTRICTIONS}" fi else # Default restrictions with policy check BEFORE permit_sasl_authenticated local NEW_RESTRICTIONS="reject_non_fqdn_recipient, reject_invalid_helo_hostname, reject_non_fqdn_sender, check_policy_service inet:127.0.0.1:10035, permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, permit" fi # Apply using postconf if su - "$ZIMBRA_USER" -c "/opt/zimbra/bin/postconf -e \"smtpd_recipient_restrictions = $NEW_RESTRICTIONS\"" >/dev/null 2>&1; then print_success "Policy server added to smtpd_recipient_restrictions" else print_warning "Could not configure postfix restrictions automatically" print_info "Please manually add: check_policy_service inet:127.0.0.1:10035" fi # Add policy service timeout settings su - "$ZIMBRA_USER" -c "/opt/zimbra/bin/postconf -e 'smtpd_policy_service_max_idle=300s'" >/dev/null 2>&1 || true su - "$ZIMBRA_USER" -c "/opt/zimbra/bin/postconf -e 'smtpd_policy_service_max_ttl=1000s'" >/dev/null 2>&1 || true # Configure smtpd_recipient_restrictions.cf for policy check print_info "Configuring Zimbra recipient restrictions..." RESTRICTIONS_FILE="/opt/zimbra/conf/zmconfigd/smtpd_recipient_restrictions.cf" if [ -f "$RESTRICTIONS_FILE" ]; then # Backup current config cp "$RESTRICTIONS_FILE" "$CONFIG_DIR/zimbra-recipient-restrictions.backup" print_info "Backed up $RESTRICTIONS_FILE" # Remove hardcoded 10031 entries ONLY if nothing is listening on that port # (to preserve cbpolicyd if customer is actively using it) port_10031_active="" if command -v ss &> /dev/null; then port_10031_active=$(ss -tlnp 2>/dev/null | grep ":10031 " || true) elif command -v netstat &> /dev/null; then port_10031_active=$(netstat -tlnp 2>/dev/null | grep ":10031 " || true) fi if [ -z "$port_10031_active" ]; then # No service on 10031, safe to remove stale config if grep -q "^check_policy_service inet:127.0.0.1:10031$" "$RESTRICTIONS_FILE"; then sed -i "/^check_policy_service inet:127.0.0.1:10031$/d" "$RESTRICTIONS_FILE" print_info "Removed stale cbpolicyd entry (10031) from restrictions file" fi # Also remove from current postconf value (in case restored from backup) CURRENT_RESTRICTIONS=$(su - "$ZIMBRA_USER" -c "/opt/zimbra/bin/postconf -h smtpd_recipient_restrictions 2>/dev/null") || true if echo "$CURRENT_RESTRICTIONS" | grep -q "check_policy_service inet:127.0.0.1:10031"; then CLEANED_RESTRICTIONS=$(echo "$CURRENT_RESTRICTIONS" | sed 's/, *check_policy_service inet:127.0.0.1:10031//g' | sed 's/check_policy_service inet:127.0.0.1:10031, *//g') su - "$ZIMBRA_USER" -c "/opt/zimbra/bin/postconf -e \"smtpd_recipient_restrictions = $CLEANED_RESTRICTIONS\"" >/dev/null 2>&1 || true print_info "Removed stale cbpolicyd entry (10031) from postconf" fi else print_info "Port 10031 is active (cbpolicyd), keeping its configuration" fi # Check if policy already configured if ! grep -q "check_policy_service inet:127.0.0.1:10035" "$RESTRICTIONS_FILE"; then # Add policy check BEFORE permit_sasl_authenticated (so authenticated users go through policy) sed -i '/^permit_sasl_authenticated$/i check_policy_service inet:127.0.0.1:10035' "$RESTRICTIONS_FILE" print_success "Policy server added to smtpd_recipient_restrictions.cf" else print_warning "Policy server already configured in smtpd_recipient_restrictions.cf" fi # CRITICAL: Ensure reject_unauth_destination is present before permit # Postfix requires this to prevent being an open relay if grep -q "^permit$" "$RESTRICTIONS_FILE" && ! grep -q "^reject_unauth_destination$" "$RESTRICTIONS_FILE"; then sed -i '/^permit$/i reject_unauth_destination' "$RESTRICTIONS_FILE" print_success "Added reject_unauth_destination to prevent open relay" fi else print_warning "Zimbra restrictions file not found: $RESTRICTIONS_FILE" fi # NOTE: We do NOT modify master.cf.in for submission service (port 587) # Adding "check_policy_service inet:..." causes Postfix to misparse the -o argument due to the space # Policy is applied globally via smtpd_recipient_restrictions.cf which covers all mail including submission # Reload Zimbra MTA print_info "Reloading Zimbra MTA..." su - "$ZIMBRA_USER" -c "/opt/zimbra/bin/zmmtactl reload" >/dev/null 2>&1 || \ su - "$ZIMBRA_USER" -c "/opt/zimbra/bin/postfix reload" >/dev/null 2>&1 || true print_success "Zimbra configured to use SecZim policy server" print_info "Backups saved to $CONFIG_DIR/" } # Configure Postfix integration configure_postfix() { print_info "Configuring Postfix integration..." # Backup current config cp /etc/postfix/main.cf "$CONFIG_DIR/postfix-main.cf.backup" # Check if policy server already configured if grep -q "check_policy_service inet:127.0.0.1:10035" /etc/postfix/main.cf; then print_warning "Postfix already has policy server configured" return 0 fi # Add policy server FIRST in smtpd_recipient_restrictions to see all traffic if grep -q "^smtpd_recipient_restrictions" /etc/postfix/main.cf; then # Prepend to existing restrictions (must be first to see all traffic) sed -i '/^smtpd_recipient_restrictions/s/= /= check_policy_service inet:127.0.0.1:10035, /' /etc/postfix/main.cf else # Add new restrictions with policy server first echo "smtpd_recipient_restrictions = check_policy_service inet:127.0.0.1:10035, permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination" >> /etc/postfix/main.cf fi # Reload Postfix postfix reload print_success "Postfix configured to use SecZim policy server" print_info "Backup saved to $CONFIG_DIR/postfix-main.cf.backup" } # Start SecZim daemon start_seczim_daemon() { print_info "Starting SecZim policy server..." systemctl enable seczim systemctl start seczim # Wait for service to start with retries (MySQL may need time) local retries=6 while [ $retries -gt 0 ]; do sleep 5 if systemctl is-active --quiet seczim; then print_success "SecZim policy server started successfully" return 0 fi print_info "Waiting for policy server to start... ($(($retries - 1)) retries left)" systemctl restart seczim 2>/dev/null || true retries=$((retries - 1)) done print_error "Failed to start SecZim policy server" systemctl status seczim --no-pager journalctl -u seczim -n 10 --no-pager exit 1 } # Test policy server test_policy_server() { print_info "Testing policy server connection..." # Test if port 10035 is listening if netstat -tlnp 2>/dev/null | grep -q ":10035" || ss -tlnp 2>/dev/null | grep -q ":10035"; then print_success "Policy server is listening on port 10035" else print_warning "Policy server port 10035 not detected" fi # Test simple policy query POLICY_RESPONSE=$(timeout 5 bash -c "echo -e 'request=smtpd_access_policy\nprotocol_state=RCPT\nprotocol_name=ESMTP\nclient_address=1.2.3.4\nclient_name=unknown\nreverse_client_name=unknown\nsender=test@example.com\nrecipient=test@example.com\n' | nc 127.0.0.1 10035" 2>/dev/null || echo "") if echo "$POLICY_RESPONSE" | grep -q "action="; then print_success "Policy server is responding correctly" else print_warning "Policy server response test inconclusive" fi } # Create uninstall script create_uninstall_script() { print_info "Creating uninstall script..." cat > /usr/local/bin/seczim-uninstall <<'UNINSTALL_EOF' #!/bin/bash # # SecZim Uninstall Script # set -e RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' print_info() { echo -e "${BLUE}[INFO]${NC} $1"; } print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } print_error() { echo -e "${RED}[ERROR]${NC} $1"; } # Check root if [ "$EUID" -ne 0 ]; then print_error "This script must be run as root" exit 1 fi echo "" echo -e "${RED}========================================================${NC}" echo -e "${RED} SecZim Uninstaller ${NC}" echo -e "${RED}========================================================${NC}" echo "" # Confirm uninstall while true; do echo -n "Are you sure you want to uninstall SecZim? [y/n]: " read -r REPLY < /dev/tty if [[ $REPLY =~ ^[Yy]$ ]]; then break elif [[ $REPLY =~ ^[Nn]$ ]]; then print_info "Uninstall cancelled" exit 0 else print_warning "Please enter 'y' for yes or 'n' for no" fi done # Ask about data preservation KEEP_DATA=true echo "" echo -e "${YELLOW}Do you want to keep your data for a future reinstall?${NC}" echo "" echo " [K] Keep data - Data preserved for reinstall" echo " [D] Delete data - All data permanently deleted" echo "" while true; do echo -n "Your choice [K/d]: " read -r REPLY < /dev/tty # Default to Keep if just Enter pressed if [[ -z $REPLY ]] || [[ $REPLY =~ ^[Kk]$ ]]; then KEEP_DATA=true print_info "Data will be preserved" break elif [[ $REPLY =~ ^[Dd]$ ]]; then # Double confirm for data deletion echo "" echo -n -e "${RED}Are you SURE you want to DELETE all data? This cannot be undone! [y/N]: ${NC}" read -r CONFIRM < /dev/tty if [[ $CONFIRM =~ ^[Yy]$ ]]; then KEEP_DATA=false print_warning "All data will be permanently deleted" break else print_info "Deletion cancelled, keeping data" KEEP_DATA=true break fi else print_warning "Please enter 'K' to keep or 'D' to delete" fi done # Stop services print_info "Stopping SecZim services..." systemctl stop seczim-api 2>/dev/null || true systemctl stop seczim 2>/dev/null || true systemctl disable seczim-api 2>/dev/null || true systemctl disable seczim 2>/dev/null || true # Stop Docker containers print_info "Stopping Docker containers..." if [ -f /opt/seczim/docker-compose.yml ]; then cd /opt/seczim if [ "$KEEP_DATA" = true ]; then # Stop containers but keep volumes (data) docker compose down 2>/dev/null || true else # Remove containers AND volumes (all data) docker compose down -v 2>/dev/null || true fi fi # Remove systemd services print_info "Removing systemd services..." rm -f /etc/systemd/system/seczim.service rm -f /etc/systemd/system/seczim-api.service systemctl daemon-reload # Restore MTA configuration print_info "Checking MTA configuration..." # Restore Zimbra if it exists if [ -d /opt/zimbra ]; then print_info "Restoring Zimbra configuration..." ZIMBRA_USER=$(stat -c '%U' /opt/zimbra 2>/dev/null || echo "zimbra") # Restore smtpd_recipient_restrictions from backup if [ -f /etc/seczim/zimbra-smtpd-restrictions.backup ]; then BACKUP_RESTRICTIONS=$(cat /etc/seczim/zimbra-smtpd-restrictions.backup 2>/dev/null) if [ -n "$BACKUP_RESTRICTIONS" ]; then print_info "Restoring smtpd_recipient_restrictions from backup..." su - "$ZIMBRA_USER" -c "/opt/zimbra/bin/postconf -e \"smtpd_recipient_restrictions = $BACKUP_RESTRICTIONS\"" >/dev/null 2>&1 || true fi else # Fallback: remove SecZim policy from current config CURRENT=$(su - "$ZIMBRA_USER" -c "/opt/zimbra/bin/postconf -h smtpd_recipient_restrictions" 2>/dev/null || true) if [ -n "$CURRENT" ]; then CLEANED=$(echo "$CURRENT" | sed 's/,*\s*check_policy_service inet:127.0.0.1:10035//g' | sed 's/^,\s*//' | sed 's/,\s*$//') if [ -n "$CLEANED" ]; then su - "$ZIMBRA_USER" -c "/opt/zimbra/bin/postconf -e \"smtpd_recipient_restrictions = $CLEANED\"" >/dev/null 2>&1 || true fi fi fi # Restore smtpd_recipient_restrictions.cf from backup if [ -f /etc/seczim/zimbra-recipient-restrictions.backup ]; then print_info "Restoring smtpd_recipient_restrictions.cf from backup..." cp /etc/seczim/zimbra-recipient-restrictions.backup /opt/zimbra/conf/zmconfigd/smtpd_recipient_restrictions.cf else # Fallback: remove SecZim policy from file RESTRICTIONS_FILE="/opt/zimbra/conf/zmconfigd/smtpd_recipient_restrictions.cf" if [ -f "$RESTRICTIONS_FILE" ]; then sed -i '/check_policy_service inet:127.0.0.1:10035/d' "$RESTRICTIONS_FILE" fi fi # Restore master.cf.in from backup if [ -f /etc/seczim/zimbra-master.cf.in.backup ]; then print_info "Restoring master.cf.in from backup..." cp /etc/seczim/zimbra-master.cf.in.backup /opt/zimbra/common/conf/master.cf.in else # Fallback: remove SecZim policy from master.cf.in MASTER_CF_IN="/opt/zimbra/common/conf/master.cf.in" if [ -f "$MASTER_CF_IN" ]; then sed -i 's/check_policy_service inet:127.0.0.1:10035,*//' "$MASTER_CF_IN" fi fi # Reload MTA su - "$ZIMBRA_USER" -c "/opt/zimbra/bin/zmmtactl reload" >/dev/null 2>&1 || true print_success "Zimbra configuration restored" fi # Restore Postfix if backup exists if [ -f /etc/seczim/postfix-main.cf.backup ]; then print_info "Restoring Postfix configuration..." cp /etc/seczim/postfix-main.cf.backup /etc/postfix/main.cf postfix reload 2>/dev/null || true print_success "Postfix configuration restored" fi # Remove installation directories print_info "Removing SecZim files..." if [ "$KEEP_DATA" = true ]; then # Keep data and credentials, only remove binaries rm -f /opt/seczim/seczim-daemon rm -f /opt/seczim/seczim-api rm -rf /opt/seczim/frontend # Keep: /opt/seczim/docker-compose.yml, /opt/seczim/init.sql # Keep: /etc/seczim/.credentials, /etc/seczim/seczim.yaml # Keep: /var/lib/seczim (MySQL and Redis data) print_info "Data preserved in /var/lib/seczim, /etc/seczim, and Docker volumes" print_info "Credentials saved in /etc/seczim/.credentials" else # Remove everything rm -rf /opt/seczim rm -rf /etc/seczim rm -rf /var/log/seczim rm -rf /var/lib/seczim fi # Remove this script and symlink rm -f /usr/local/bin/seczim-uninstall rm -f /sbin/seczim-uninstall echo "" echo -e "${GREEN}========================================================${NC}" echo -e "${GREEN} SecZim Uninstalled Successfully ${NC}" echo -e "${GREEN}========================================================${NC}" echo "" if [ "$KEEP_DATA" = true ]; then print_info "Your data has been preserved. Reinstall SecZim to restore functionality." print_info "To completely remove all data, run uninstall again and choose 'No'." else print_info "All SecZim data has been removed." fi echo "" print_info "Docker images may still exist. Run 'docker image prune' to clean up." echo "" UNINSTALL_EOF chmod +x /usr/local/bin/seczim-uninstall # Create symlink in /sbin for easier access ln -sf /usr/local/bin/seczim-uninstall /sbin/seczim-uninstall print_success "Uninstall script created: seczim-uninstall" } # Print completion print_completion() { echo "" echo -e "${GREEN}========================================================${NC}" echo -e "${GREEN} ${NC}" echo -e "${GREEN} SecZim Installation Complete! ${NC}" echo -e "${GREEN} ${NC}" echo -e "${GREEN}========================================================${NC}" echo "" echo -e "${BLUE}SecZim is now protecting your mail server!${NC}" echo "" echo "Installation directory: $INSTALL_DIR" echo "Configuration: $CONFIG_DIR/seczim.yaml" echo "Logs: $LOG_DIR" echo "Credentials: $CONFIG_DIR/.credentials" echo "" # Get server IP for web UI access SERVER_IP=$(hostname -I | awk '{print $1}') echo "Services running:" echo " - MySQL: localhost:3306" echo " - Redis: localhost:6379" echo " - SecZim Policy Server: localhost:10035" echo " - SecZim API Server: localhost:8880" if [ "$MTA_TYPE" = "zimbra" ]; then echo " - Zimbra MTA: Integrated" elif [ "$MTA_TYPE" = "postfix" ]; then echo " - Postfix MTA: Integrated" fi echo "" echo -e "${GREEN}Web Administration Panel:${NC}" echo -e " ${BLUE}http://${SERVER_IP}:8880${NC}" echo -e " Username: ${CYAN}admin${NC}" echo -e " Password: ${CYAN}(the password you entered during installation)${NC}" echo "" echo -e "${YELLOW}IMPORTANT: Keep $CONFIG_DIR/.credentials secure!${NC}" echo "" echo "Useful commands:" echo " - Check status: systemctl status seczim seczim-api" echo " - View logs: tail -f $LOG_DIR/seczim.log" echo " - View API logs: tail -f $LOG_DIR/seczim-api.log" echo " - Restart services: systemctl restart seczim seczim-api" echo " - Uninstall: sudo seczim-uninstall" echo "" echo "Documentation: https://docs.seczim.com" echo "Support: support@seczim.com" echo "" } # Main main() { # Parse arguments LICENSE_KEY="" while [[ $# -gt 0 ]]; do case $1 in --verbose|-v) VERBOSE=true shift ;; --upgrade|-u) UPGRADE_MODE=true shift ;; --help|-h) echo "Usage: $0 [OPTIONS] YOUR-LICENSE-KEY" echo "" echo "Options:" echo " --verbose, -v Show detailed debug information (safe for sharing)" echo " --upgrade, -u Upgrade existing installation (apply migrations only)" echo " --help, -h Show this help message" echo "" echo "Examples:" echo " # Fresh install" echo " curl -fsSL https://get.seczim.com/install.sh | sudo bash -s -- YOUR-LICENSE-KEY" echo "" echo " # Upgrade existing installation" echo " curl -fsSL https://get.seczim.com/install.sh | sudo bash -s -- --upgrade" echo "" echo " # Verbose install for debugging" echo " curl -fsSL https://get.seczim.com/install.sh | sudo bash -s -- --verbose YOUR-LICENSE-KEY" exit 0 ;; -*) print_error "Unknown option: $1" echo "Use --help for usage information" exit 1 ;; *) LICENSE_KEY="$1" shift ;; esac done print_banner if [ "$VERBOSE" = true ]; then print_info "Verbose mode enabled - debug output will be shown" print_verbose "System: $(uname -a)" print_verbose "Date: $(date)" fi # Upgrade mode - only apply database migrations if [ "$UPGRADE_MODE" = true ]; then print_info "Running in upgrade mode..." check_root # Check if SecZim is installed if [ ! -f "$CONFIG_DIR/.license" ]; then print_error "SecZim is not installed. Run without --upgrade to install." exit 1 fi # Read existing license key LICENSE_KEY=$(cat "$CONFIG_DIR/.license" 2>/dev/null) if [ -z "$LICENSE_KEY" ]; then print_error "Could not read license key from $CONFIG_DIR/.license" exit 1 fi print_info "Found existing license: $(mask_license_key "$LICENSE_KEY")" # Validate license and get download token validate_license "$LICENSE_KEY" # Apply database migrations upgrade_database_schema print_success "Upgrade complete!" exit 0 fi # Fresh installation mode # Pre-installation checks check_root check_os_compatibility check_port_available check_existing_policy_server validate_license "$LICENSE_KEY" detect_mta # Prompt for admin password early (before long installation process) prompt_admin_password # Install dependencies install_docker create_directories save_license_info # Database setup download_schema generate_docker_compose start_services # Apply migrations only if reinstalling with existing data # (init.sql doesn't run when MySQL already has data) if [ "$EXISTING_DATA_DETECTED" = true ]; then print_info "Existing data detected, applying migrations..." upgrade_database_schema fi # Configure admin user with password configure_admin_user # SecZim policy server setup download_seczim_binary download_seczim_api download_frontend generate_config generate_systemd_service generate_api_systemd_service start_seczim_daemon start_seczim_api # MTA Integration if [ "$MTA_TYPE" = "zimbra" ]; then configure_zimbra elif [ "$MTA_TYPE" = "postfix" ]; then configure_postfix else print_warning "Skipping MTA integration - no supported MTA detected" print_info "Please manually configure your mail server to use policy server at 127.0.0.1:10035" fi # Testing test_policy_server # Final setup create_uninstall_script # Cleanup rm -f /tmp/seczim_license.key # Summary print_completion } main "$@"