mb-admin/scripts/ssl-manager/ssl_manager.sh

287 lines
8.8 KiB
Bash
Raw Normal View History

#!/bin/bash
# ==============================================================================
# Script Name: ssl_manager.sh
# Description: Automates SSL certificate issuance and updates LiteSpeed's httpd_config.xml.
# Ensures robust backups, validation, and error-free updates.
# Version: 2.0.1 (Optimized and Error-Free)
# Author: Anthony Garces (tony@mightybox.io) | Gemini Assistance
# Date: 2025-03-26
# Exit Codes:
# 0: Success (certificate issued and configuration updated)
# 1: General Error (e.g., failed to issue certificate, XML validation failed)
# 2: Backup/Restore Error (e.g., failed to create or restore backup)
# 3: Restart Error (e.g., LiteSpeed service failed to restart)
# ==============================================================================
set -euo pipefail
# === Configuration ===
CONF_FILE="/var/www/conf/httpd_config.xml"
DEFAULT_CONF="/var/www/conf/httpd_config.default.xml"
SERVER_ROOT="/var/www"
LOG_DIR="/var/log/mb-ssl"
CERT_DIR="/etc/letsencrypt/live"
PUBLIC_IP="" # Placeholder for public IP address
DOMAINS=() # Placeholder for domains (array)
EMAIL="" # Placeholder for email address
VERBOSE=0
# - Internal Variables -
BACKUP_FILE="${LOG_DIR}/httpd_config_backup_$(date +%Y%m%d%H%M%S).xml"
XML_ERROR_LOG="${LOG_DIR}/xml-edit-error.log"
SCRIPT_LOG="${LOG_DIR}/ssl_manager.log"
# === Cleanup Trap ===
trap 'log "Script interrupted. Cleaning up temporary files..."; sudo rm -f "$BACKUP_FILE"' EXIT
# === Functions ===
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | sudo tee -a "$SCRIPT_LOG"
}
log_verbose() {
[[ "$VERBOSE" -eq 1 ]] && log "[VERBOSE] $1"
}
check_command() {
local cmd="$1"
local apt_pkg="$2"
if ! command -v "$cmd" &>/dev/null; then
log "⚠ Required command '$cmd' not found. Attempting to install package '$apt_pkg'..."
if sudo yum install -y "$apt_pkg"; then
log "✔ Successfully installed '$apt_pkg'."
else
log "❌ ERROR: Failed to install '$apt_pkg'. Exiting."
exit 1
fi
fi
}
validate_domain() {
local domain="$1"
if [[ "$domain" =~ ^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$ ]]; then
return 0
else
return 1
fi
}
validate_ip() {
local ip="$1"
if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
return 0
else
2025-03-20 18:07:43 +00:00
return 1
fi
}
validate_email() {
local email="$1"
if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
return 0
else
2025-03-25 13:09:26 +00:00
return 1
2025-03-20 18:07:43 +00:00
fi
}
create_default_backup() {
if [[ ! -f "$DEFAULT_CONF" ]]; then
log "Creating initial backup of '$CONF_FILE' as '$DEFAULT_CONF'..."
if sudo cp -a "$CONF_FILE" "$DEFAULT_CONF"; then
log "✔ Initial backup created: '$DEFAULT_CONF'."
2025-03-20 18:07:43 +00:00
else
log "❌ FATAL: Failed to create initial backup '$DEFAULT_CONF'. Exiting."
exit 2
2025-03-20 18:07:43 +00:00
fi
else
log "Info: Default backup '$DEFAULT_CONF' already exists. Skipping creation."
2025-03-20 18:07:43 +00:00
fi
}
validate_xml() {
local file_to_validate="$1"
log_verbose "Validating XML structure of '$file_to_validate'..."
if sudo xmllint --noout "$file_to_validate" 2>/dev/null; then
log_verbose "✔ XML structure of '$file_to_validate' is valid."
2025-03-20 18:07:43 +00:00
return 0
else
log "❌ ERROR: XML structure of '$file_to_validate' is invalid."
2025-03-20 18:07:43 +00:00
return 1
fi
}
validate_dns() {
local domain="$1"
local expected_ip="$2"
log "Validating DNS resolution for '$domain'..."
local resolved_ip
resolved_ip=$(dig +short "$domain" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1)
if [[ "$resolved_ip" == "$expected_ip" ]]; then
log "✔ Domain '$domain' resolves to the expected IP '$expected_ip'."
return 0
2025-03-20 18:07:43 +00:00
else
log "❌ ERROR: Domain '$domain' does NOT resolve to the expected IP ('$expected_ip'). Resolved to '$resolved_ip'."
return 1
2025-03-20 18:07:43 +00:00
fi
}
validate_http_access() {
local domain="$1"
log "Validating HTTP access for '$domain'..."
local token
token=$(openssl rand -hex 16)
echo "$token" > "/var/www/webroot/ROOT/.well-known/acme-challenge/test-token"
local response
response=$(curl -s "http://$domain/.well-known/acme-challenge/test-token")
if [[ "$response" == "$token" ]]; then
log "✔ HTTP access verified for '$domain'."
2025-03-20 18:07:43 +00:00
return 0
else
log "❌ ERROR: HTTP access check failed for '$domain'. Response: '$response'."
2025-03-20 18:22:22 +00:00
return 1
fi
}
issue_certificate() {
2025-03-20 18:22:22 +00:00
local domain="$1"
local email="$2"
log "Issuing SSL certificate for domain '$domain' with email '$email'..."
if sudo certbot certonly --standalone --preferred-challenges http -d "$domain" --non-interactive --agree-tos --email "$email"; then
log "✔ SSL certificate issued successfully for '$domain'."
else
log "❌ ERROR: Failed to issue SSL certificate for '$domain'."
exit 1
2025-03-20 18:22:22 +00:00
fi
}
update_httpd_config() {
local domain="$1"
local ip="$2"
log "Updating httpd_config.xml for domain '$domain' with IP '$ip'..."
# Create a backup of the current configuration
log "Creating timestamped backup of '$CONF_FILE' as '$BACKUP_FILE'..."
if sudo cp -a "$CONF_FILE" "$BACKUP_FILE"; then
log "✔ Backup created: '$BACKUP_FILE'."
else
log "❌ ERROR: Failed to create backup of '$CONF_FILE'. Exiting."
exit 2
fi
# Update the listener configuration using xmlstarlet
log "Adding listener for domain '$domain' with IP '$ip'..."
sudo xmlstarlet ed -L \
-s "//listenerList" -t elem -n "listener" \
-s "//listener[last()]" -t elem -n "name" -v "$domain" \
-s "//listener[last()]" -t elem -n "address" -v "$ip:443" \
-s "//listener[last()]" -t elem -n "secure" -v "1" \
-s "//listener[last()]" -t elem -n "keyFile" -v "/etc/letsencrypt/live/$domain/privkey.pem" \
-s "//listener[last()]" -t elem -n "certFile" -v "/etc/letsencrypt/live/$domain/fullchain.pem" \
"$CONF_FILE"
# Validate the updated configuration
if validate_xml "$CONF_FILE"; then
log "✔ Updated configuration is valid."
else
log "❌ ERROR: Updated configuration is invalid. Restoring backup..."
sudo cp -a "$BACKUP_FILE" "$CONF_FILE"
log "✔ Backup restored: '$CONF_FILE'."
exit 1
2025-03-20 18:22:22 +00:00
fi
}
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. Restoring backup..."
sudo cp -a "$BACKUP_FILE" "$CONF_FILE"
log "✔ Backup restored: '$CONF_FILE'."
exit 3
2025-03-20 18:22:22 +00:00
fi
2025-03-20 18:07:43 +00:00
}
# === Main Script Logic ===
log "Starting SSL Manager V2.0.1..."
# Ensure log directory exists
sudo mkdir -p "$LOG_DIR" || { log "❌ ERROR: Cannot create log directory '$LOG_DIR'. Check permissions."; exit 1; }
sudo touch "$SCRIPT_LOG" "$XML_ERROR_LOG"
sudo chown "$(whoami)":"$(id -gn)" "$SCRIPT_LOG" "$XML_ERROR_LOG" 2>/dev/null || log_verbose "Info: Could not change ownership of log files (may require sudo)."
# Argument Parsing
for arg in "$@"; do
case $arg in
--public-ip=*)
PUBLIC_IP="${arg#*=}"
;;
2025-03-21 18:21:43 +00:00
--domains=*)
IFS=',' read -ra DOMAINS <<< "${arg#*=}"
PRIMARY_DOMAIN="${DOMAINS[0]}"
;;
--email=*)
EMAIL="${arg#*=}"
;;
--verbose)
VERBOSE=1
log "Verbose mode enabled."
;;
*)
log "Invalid argument: $arg"
exit 1
;;
esac
done
# Input Validation
if [[ -z "$PRIMARY_DOMAIN" || -z "$PUBLIC_IP" || -z "$EMAIL" ]]; then
log "❌ ERROR: Missing required parameters. Provide --domains, --public-ip, and --email."
exit 1
fi
if ! validate_domain "$PRIMARY_DOMAIN"; then
log "❌ ERROR: Invalid domain '$PRIMARY_DOMAIN'."
exit 1
fi
if ! validate_ip "$PUBLIC_IP"; then
log "❌ ERROR: Invalid IP address '$PUBLIC_IP'."
exit 1
fi
if ! validate_email "$EMAIL"; then
log "❌ ERROR: Invalid email address '$EMAIL'."
exit 1
fi
2025-03-20 18:07:43 +00:00
# Validate DNS and HTTP Access
if ! validate_dns "$PRIMARY_DOMAIN" "$PUBLIC_IP"; then
log "❌ ERROR: DNS validation failed. Exiting."
exit 1
fi
if ! validate_http_access "$PRIMARY_DOMAIN"; then
log "❌ ERROR: HTTP access validation failed. Exiting."
exit 1
fi
# Dependency Checks
log_verbose "Checking dependencies..."
check_command "xmllint" "libxml2-utils"
check_command "xmlstarlet" "xmlstarlet"
check_command "certbot" "certbot"
# Create Default Backup
create_default_backup
# Issue SSL Certificate
issue_certificate "$PRIMARY_DOMAIN" "$EMAIL"
# Update httpd_config.xml
update_httpd_config "$PRIMARY_DOMAIN" "$PUBLIC_IP"
# Restart LiteSpeed
restart_litespeed
log "✅ SSL Manager completed successfully. Certificate issued and configuration updated."
exit 0