CloudPanel Domain Management: Domain Migration Script for Nginx & SSL

DomainShift CP: CloudPanel Domain Update Automation Tool

  1. Enhanced Error Handling:

    • Uses set -euo pipefail for strict error handling
    • Comprehensive error checking for all critical operations
    • Detailed error messages with logging
  2. Logging System:

    • Added timestamp-based logging
    • Logs are written to both console and log file
    • Maintains audit trail of all domain updates
  3. Backup System:

    • Automatically creates timestamped backups before making changes
    • Backs up nginx config, SSL certificates, and basic auth files
    • Allows for easy rollback if needed
  4. Better Organization:

    • Modular design with separate functions for each major operation
    • Clear section separation with comments
    • Improved readability and maintainability
  5. Security Improvements:

    • Proper permission setting for SSL keys
    • Input validation for domain names
    • Checks for required privileges and dependencies
  6. SQLite Operations:

    • Uses transactions for database operations
    • Better error handling for database queries
    • More robust certificate management
  7. Prerequisites Checking:

    • Verifies all required tools are installed
    • Checks for necessary directories and files
    • Validates running privileges

To use the script:

  1. Save it as update_domain.sh

  2. Make it executable:

    chmod +x update_domain.sh
    
  3. Make a Directory name cloudpanel in var/log
    mkdir /var/log/cloudpanel

  4. Create a file named domain_updates.log
    touch /var/log/cloudpanel/domain_updates.log

  5. Run it as root:

    sudo ./update_domain.sh old.domain.com new.domain.com
    

update_domain

The script will:

  • Create a backup of all relevant files
  • Update all necessary configurations
  • Generate new SSL certificates
  • Update the CloudPanel database
  • Log all actions
  • Provide detailed feedback about the process

Logs can be found at /var/log/cloudpanel/domain_updates.log for audit purposes.

#!/bin/bash

# =============================================================================
# DomainShift CP - CloudPanel Domain Migration Tool
# -----------------------------------------------------------------------------
# Purpose: Updates domain names across nginx configuration, SSL certificates,
#          and CloudPanel database while maintaining all associated settings.
# Usage: ./update_domain.sh old.domain.com new.domain.com
# Author: hhf
# Last Updated: 2024-11-09
# =============================================================================

set -euo pipefail  # Exit on error, undefined vars, and pipe failures

# =============================================================================
# Configuration
# =============================================================================

readonly DB_FILE="/home/clp/htdocs/app/data/db.sq3"
readonly NGINX_CONF_DIR="/etc/nginx/sites-enabled"
readonly NGINX_SSL_DIR="/etc/nginx/ssl-certificates"
readonly NGINX_AUTH_DIR="/etc/nginx/basic-auth"
readonly LOG_FILE="/var/log/cloudpanel/domain_updates.log"
readonly DOMAIN_REGEX="^([a-zA-Z0-9](([a-zA-Z0-9\-]){0,61}[a-zA-Z0-9])?\.)+([a-zA-Z]{2,}|xn--[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])$"

# =============================================================================
# Helper Functions
# =============================================================================

log() {
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[$timestamp] $1" | tee -a "$LOG_FILE"
}

error_exit() {
    log "ERROR: $1"
    exit 1
}

check_prerequisites() {
    # Check if running as root
    [[ $EUID -eq 0 ]] || error_exit "This script must be run as root"
    
    # Check if required tools are installed
    command -v sqlite3 >/dev/null 2>&1 || error_exit "sqlite3 is required but not installed"
    command -v openssl >/dev/null 2>&1 || error_exit "openssl is required but not installed"
    command -v clpctl >/dev/null 2>&1 || error_exit "clpctl is required but not installed"
    
    # Check if required directories exist
    [[ -d "$NGINX_CONF_DIR" ]] || error_exit "Nginx config directory not found: $NGINX_CONF_DIR"
    [[ -d "$NGINX_SSL_DIR" ]] || error_exit "SSL directory not found: $NGINX_SSL_DIR"
    [[ -f "$DB_FILE" ]] || error_exit "Database file not found: $DB_FILE"
}

validate_domains() {
    local old_domain="$1"
    local new_domain="$2"

    [[ -z "$old_domain" || -z "$new_domain" ]] && {
        echo "Usage: $0 old.domain.com new.domain.com"
        error_exit "Domain name parameters not set"
    }

    [[ "$old_domain" =~ $DOMAIN_REGEX ]] || error_exit "Invalid old domain format: $old_domain"
    [[ "$new_domain" =~ $DOMAIN_REGEX ]] || error_exit "Invalid new domain format: $new_domain"
}

backup_files() {
    local timestamp
    timestamp=$(date +%Y%m%d_%H%M%S)
    local backup_dir="/root/domain_update_backup_${timestamp}"
    
    mkdir -p "$backup_dir"
    
    # Backup nginx configuration
    cp "$NGINX_CONF_DIR/$OLD_DOMAIN.conf" "$backup_dir/" 2>/dev/null || true
    
    # Backup SSL certificates
    cp "$NGINX_SSL_DIR/$OLD_DOMAIN".* "$backup_dir/" 2>/dev/null || true
    
    # Backup basic auth file
    [[ -f "$NGINX_AUTH_DIR/$OLD_DOMAIN" ]] && cp "$NGINX_AUTH_DIR/$OLD_DOMAIN" "$backup_dir/" || true
    
    log "Backup created at: $backup_dir"
}

generate_ssl_certificate() {
    local domain="$1"
    
    openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
        -subj "/CN=$domain/" \
        -addext "subjectAltName=DNS:$domain" \
        -keyout "$NGINX_SSL_DIR/$domain.key" \
        -out "$NGINX_SSL_DIR/$domain.crt" || error_exit "Failed to generate SSL certificate"
        
    chmod 600 "$NGINX_SSL_DIR/$domain.key"
}

update_nginx_config() {
    local old_domain="$1"
    local new_domain="$2"
    
    # Update nginx configuration content
    sed -i "s/$old_domain/$new_domain/g" "$NGINX_CONF_DIR/$old_domain.conf" || \
        error_exit "Failed to update nginx configuration content"
    
    # Rename nginx configuration file
    mv "$NGINX_CONF_DIR/$old_domain.conf" "$NGINX_CONF_DIR/$new_domain.conf" || \
        error_exit "Failed to rename nginx configuration file"
}

update_database() {
    local old_domain="$1"
    local new_domain="$2"
    local site_id
    
    # Get site ID
    site_id=$(sqlite3 "$DB_FILE" "SELECT id FROM site WHERE domain_name = '$old_domain';") || \
        error_exit "Failed to query site ID"
    
    [[ -z "$site_id" ]] && error_exit "Domain $old_domain not found in CloudPanel database"
    
    # Update site information
    sqlite3 "$DB_FILE" "BEGIN TRANSACTION;
        UPDATE site 
        SET domain_name = '$new_domain',
            vhost_template = REPLACE(vhost_template, '$old_domain', '$new_domain')
        WHERE id = $site_id;
        
        DELETE FROM certificate 
        WHERE site_id = $site_id 
        AND default_certificate = 1 
        AND type = 1;
        COMMIT;" || error_exit "Failed to update database"
        
    return "$site_id"
}

# =============================================================================
# Main Script
# =============================================================================

main() {
    local OLD_DOMAIN="$1"
    local NEW_DOMAIN="$2"
    
    log "Starting domain update process: $OLD_DOMAIN -> $NEW_DOMAIN"
    
    # Preliminary checks
    check_prerequisites
    validate_domains "$OLD_DOMAIN" "$NEW_DOMAIN"
    
    # Create backup
    backup_files
    
    # Update nginx configuration
    log "Updating nginx configuration..."
    update_nginx_config "$OLD_DOMAIN" "$NEW_DOMAIN"
    
    # Handle SSL certificates
    log "Managing SSL certificates..."
    rm -f "$NGINX_SSL_DIR/$OLD_DOMAIN".{key,crt}
    generate_ssl_certificate "$NEW_DOMAIN"
    
    # Update basic auth if exists
    if [[ -f "$NGINX_AUTH_DIR/$OLD_DOMAIN" ]]; then
        log "Updating nginx basic auth..."
        mv "$NGINX_AUTH_DIR/$OLD_DOMAIN" "$NGINX_AUTH_DIR/$NEW_DOMAIN" || \
            error_exit "Failed to update basic auth"
    fi
    
    # Update database
    log "Updating CloudPanel database..."
    local site_id
    site_id=$(update_database "$OLD_DOMAIN" "$NEW_DOMAIN")
    
    # Install new certificate
    log "Installing new self-signed certificate..."
    clpctl site:install:certificate \
        --domainName="$NEW_DOMAIN" \
        --privateKey="$NGINX_SSL_DIR/$NEW_DOMAIN.key" \
        --certificate="$NGINX_SSL_DIR/$NEW_DOMAIN.crt" || \
        error_exit "Failed to install new certificate"
    
    # Update certificate settings
    local cert_id
    cert_id=$(sqlite3 "$DB_FILE" "SELECT certificate_id FROM site WHERE id = $site_id")
    sqlite3 "$DB_FILE" "UPDATE certificate SET default_certificate = 1, type = 1 WHERE id = $cert_id" || \
        error_exit "Failed to update certificate settings"
    
    # Restart nginx
    log "Restarting nginx..."
    systemctl restart nginx || error_exit "Failed to restart nginx"
    
    log "Domain update completed successfully"
}

# Execute main function with all arguments
main "$@"