#!/bin/bash # ============================================================================== # Script Name: ssl_remover.sh # Description: Removes SSL certificates and cleans up LiteSpeed configurations. # Ensures safe removal of listeners, virtual hosts, and certificates. # Version: 2.0.0 (Professional-Grade Optimized) # Author: Anthony Garces (tony@mightybox.io) # Date: 2025-03-26 # Exit Codes: # 0: Success (SSL removed and configuration cleaned up) # 1: General Error (e.g., invalid parameters, XML validation failed) # 2: Backup/Restore Error (e.g., failed to create backup) # 3: Restart Error (e.g., LiteSpeed service failed to restart) # ============================================================================== set -euo pipefail # === Configuration === CONF_FILE="/var/www/conf/httpd_config.xml" BACKUP_DIR="/var/www/conf/backups" LOG_DIR="/var/log/mb-ssl" CERT_DIR="/etc/letsencrypt/live" SCRIPT_LOG="${LOG_DIR}/ssl-remover.log" ERROR_LOG="${LOG_DIR}/ssl-remover-error.log" DEBUG_LOG="${LOG_DIR}/ssl-remover-debug.log" VERBOSE=0 # === Functions === setup_logging() { # Create log directory if it doesn't exist sudo mkdir -p "$LOG_DIR" || { echo "❌ ERROR: Cannot create log directory '$LOG_DIR'. Check permissions."; exit 1; } # Set proper permissions sudo chown -R "$(whoami)":"$(id -gn)" "$LOG_DIR" sudo chmod 755 "$LOG_DIR" # Create log files with proper permissions touch "$SCRIPT_LOG" "$ERROR_LOG" "$DEBUG_LOG" chmod 644 "$SCRIPT_LOG" "$ERROR_LOG" "$DEBUG_LOG" # Add log rotation if log files are too large (>10MB) for log_file in "$SCRIPT_LOG" "$ERROR_LOG" "$DEBUG_LOG"; do if [ -f "$log_file" ] && [ "$(stat -f%z "$log_file" 2>/dev/null || stat -c%s "$log_file")" -gt 10485760 ]; then mv "$log_file" "${log_file}.$(date +%Y%m%d)" touch "$log_file" chmod 644 "$log_file" gzip "${log_file}.$(date +%Y%m%d)" fi done } log() { local level="INFO" local message="$1" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') # Log to main log file echo "[$timestamp] [$level] $message" | tee -a "$SCRIPT_LOG" # Log errors to error log file if [[ "$message" == *"ERROR"* ]] || [[ "$message" == *"❌"* ]]; then echo "[$timestamp] [$level] $message" >> "$ERROR_LOG" fi } log_verbose() { if [[ "$VERBOSE" -eq 1 ]]; then local timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo "[$timestamp] [DEBUG] $1" | tee -a "$DEBUG_LOG" fi } log_error() { local message="$1" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo "[$timestamp] [ERROR] $message" | tee -a "$ERROR_LOG" "$SCRIPT_LOG" } log_success() { local message="$1" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo "[$timestamp] [SUCCESS] $message" | tee -a "$SCRIPT_LOG" } send_email() { local subject="$1" local body="$2" local recipient="${EMAIL:-}" if [[ -n "$recipient" ]]; then log "Sending email notification to $recipient..." curl -s "https://api.postmarkapp.com/email" \ -X POST \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -H "X-Postmark-Server-Token: d88b25c4-2fdb-43d3-9097-f6c655a9742b" \ -d "{ \"From\": \"admin@mightybox.io\", \"To\": \"$recipient\", \"Subject\": \"$subject\", \"HtmlBody\": \"$body\", \"MessageStream\": \"outbound\" }" > /dev/null && log "Email sent." || log "Email failed." fi } validate_domain() { local domain="$1" if [[ "$domain" =~ ^([a-zA-Z0-9](-*[a-zA-Z0-9])*\.)+[a-zA-Z]{2,}$ ]]; then return 0 else return 1 fi } backup_config() { local timestamp=$(date +%Y%m%d%H%M%S) local backup_file="${BACKUP_DIR}/httpd_config.pre-removal-${timestamp}.xml" mkdir -p "$BACKUP_DIR" if cp "$CONF_FILE" "$backup_file"; then log "Config backup saved to $backup_file" else log "❌ ERROR: Failed to create backup '$backup_file'. Exiting." exit 2 fi } remove_certificate() { local domain="$1" log "Checking for certificate for domain '$domain'..." if certbot certificates | grep -q "Domains: $domain"; then log "Found certificate for '$domain'. Proceeding with removal..." if certbot delete --cert-name "$domain" --non-interactive; then # Remove all certificate files rm -rf "/etc/letsencrypt/live/$domain"* rm -rf "/etc/letsencrypt/archive/$domain"* log_success "Certificate successfully removed for '$domain'" log_verbose "Removed certificate files from /etc/letsencrypt/live/$domain and /etc/letsencrypt/archive/$domain" else log_error "Failed to remove certificate for '$domain'" return 1 fi else log "No certificate found for '$domain'. Skipping removal." fi } cleanup_listeners() { local domain="$1" local listener_name="HTTPS-$domain" log "Starting cleanup for listener '$listener_name'..." # First backup the config local backup_file="${BACKUP_DIR}/httpd_config.pre-removal-$(date +%Y%m%d%H%M%S).xml" cp "$CONF_FILE" "$backup_file" || { log_error "Failed to create backup before cleanup" return 1 } log_verbose "Created backup at: $backup_file" # Remove the entire listener element with all its children sudo xmlstarlet ed -L \ -d "//listenerList/listener[name='$listener_name']" \ "$CONF_FILE" || { log_error "Failed to remove listener '$listener_name'" cp "$backup_file" "$CONF_FILE" return 1 } log_verbose "Removed listener element: $listener_name" # Validate XML after removal if ! xmllint --noout "$CONF_FILE" 2>/dev/null; then log_error "Invalid XML structure after listener removal. Restoring backup..." cp "$backup_file" "$CONF_FILE" return 1 fi log_verbose "XML validation passed after listener removal" # Clean up any orphaned vhostMapList elements sudo xmlstarlet ed -L \ -d "//vhostMapList[not(parent::listener)]" \ "$CONF_FILE" || { log_error "Failed to clean up orphaned vhostMapList" cp "$backup_file" "$CONF_FILE" return 1 } log_verbose "Cleaned up orphaned vhostMapList elements" # Validate XML after cleanup if ! xmllint --noout "$CONF_FILE" 2>/dev/null; then log_error "Invalid XML structure after cleanup. Restoring backup..." cp "$backup_file" "$CONF_FILE" return 1 fi log_verbose "XML validation passed after cleanup" log_success "Successfully removed listener '$listener_name' and cleaned up XML structure" rm -f "$backup_file" return 0 } validate_xml() { log "Validating XML configuration..." if ! sudo xmllint --noout "$CONF_FILE" 2>/dev/null; then log "❌ ERROR: Invalid XML configuration after cleanup. Check backups." return 1 fi log "✔ XML configuration is valid." return 0 } restart_litespeed() { log "Restarting LiteSpeed server..." if sudo systemctl restart lsws; then log "✔ LiteSpeed server restarted successfully." else log "❌ ERROR: Failed to restart LiteSpeed server." return 3 fi } # === Main Script Logic === main() { declare -a DOMAINS EMAIL="" # Setup logging first setup_logging log "Starting SSL Removal Process" # Parse parameters while [[ $# -gt 0 ]]; do case "$1" in --domains=*) IFS=',' read -ra DOMAINS <<< "${1#*=}" log_verbose "Parsed domains: ${DOMAINS[*]}" ;; --email=*) EMAIL="${1#*=}" log_verbose "Set email notification to: $EMAIL" ;; --verbose) VERBOSE=1 log "Verbose mode enabled" ;; *) log_error "Invalid parameter: $1" exit 1 ;; esac shift done # Validate input if [[ ${#DOMAINS[@]} -eq 0 ]]; then log_error "--domains parameter is required" exit 1 fi # Process each domain for domain in "${DOMAINS[@]}"; do log "Processing domain: $domain" # Validate domain format if ! validate_domain "$domain"; then log_error "Invalid domain '$domain'. Skipping." continue fi # Remove certificate first if ! remove_certificate "$domain"; then log "⚠ Warning: Failed to remove certificate for '$domain'. Continuing with cleanup..." fi # Clean up listeners and configurations if ! cleanup_listeners "$domain"; then log_error "Failed to clean up listeners for '$domain'" continue fi done # Validate final XML configuration if validate_xml; then restart_litespeed log_success "SSL Removal completed successfully for domains: ${DOMAINS[*]}" send_email "SSL Removal Complete" "Successfully removed SSL for domains: ${DOMAINS[*]}" else log_error "SSL removed but configuration validation failed for domains: ${DOMAINS[*]}" send_email "SSL Removal Warning" "SSL removed but configuration validation failed for domains: ${DOMAINS[*]}" exit 1 fi } # === Entry Point === main "$@"