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

752 lines
25 KiB
Bash
Raw Normal View History

#!/bin/bash
set -euo pipefail
# Log file setup
LOG_DIR="/var/log/mb-ssl"
LOG_FILE="$LOG_DIR/ssl-manager.log"
mkdir -p "$LOG_DIR"
chmod 0755 "$LOG_DIR"
exec > >(tee -a "$LOG_FILE") 2>&1
# Function to log messages
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1"
}
# Function to send email via Postmark
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 successfully." || log "Failed to send email."
else
log "Email not provided. Skipping email notification."
fi
}
# Function to validate IP address
validate_ip() {
local ip=$1
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1
}
# Function to validate domain
validate_domain() {
local domain=$1
[[ "$domain" =~ ^([a-zA-Z0-9](-*[a-zA-Z0-9])*\.)+[a-zA-Z]{2,}$ ]] && return 0 || return 1
}
# Function to validate email
validate_email() {
local email=$1
[[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]] && return 0 || return 1
}
# Function to validate DNS resolution
validate_dns_resolution() {
2025-03-21 18:21:43 +00:00
local domain=$1
log "Validating DNS resolution for $domain..."
RESOLVED_IPS=$(dig +short "$domain" A)
if echo "$RESOLVED_IPS" | grep -q "$PUBLIC_IP"; then
2025-03-21 18:21:43 +00:00
log "DNS validation successful. $domain resolves to the expected public IP ($PUBLIC_IP)."
return 0
else
2025-03-21 18:21:43 +00:00
log "DNS validation failed. $domain does not resolve to the expected public IP ($PUBLIC_IP)."
return 1
fi
}
# Function to validate HTTP access
validate_http_access() {
log "Validating HTTP access for $DOMAIN..."
ACME_DIR="/var/www/webroot/ROOT/.well-known/acme-challenge"
mkdir -p "$ACME_DIR"
chmod 0755 "$ACME_DIR"
TOKEN=$(openssl rand -hex 16)
echo "$TOKEN" > "$ACME_DIR/test-token"
# Test HTTP access by retrieving the token
RESPONSE=$(curl -s "http://$DOMAIN/.well-known/acme-challenge/test-token")
if [[ "$RESPONSE" == "$TOKEN" ]]; then
log "HTTP validation successful. $DOMAIN is accessible."
return 0
else
log "HTTP validation failed. Unable to retrieve the test token from $DOMAIN."
return 1
fi
}
# Function to validate the domain connection
validate_domain_connection() {
2025-03-21 18:21:43 +00:00
if validate_dns_resolution "$DOMAIN"; then
log "Domain validation succeeded via DNS."
return 0
elif validate_http_access; then
log "Domain validation succeeded via HTTP."
return 0
else
log "Domain validation failed. $DOMAIN does not point to the correct IP or is not accessible via HTTP."
send_email "SSL Setup Failed" "The domain $DOMAIN could not be validated. Ensure the DNS and HTTP settings are correct."
exit 1
fi
}
2025-03-20 18:07:43 +00:00
# Function to update LiteSpeed configuration - Updated with virtual host fix
update_litespeed_config() {
local config_file="/var/www/conf/httpd_config.xml"
local key_file="/etc/letsencrypt/live/$DOMAIN/privkey.pem"
local cert_file="/etc/letsencrypt/live/$DOMAIN/fullchain.pem"
2025-03-20 18:07:43 +00:00
local timestamp=$(date +%Y%m%d%H%M%S)
local backup_file="${config_file}.backup.${timestamp}"
log "Updating LiteSpeed configuration..."
2025-03-20 18:07:43 +00:00
# Ensure we have a backup with timestamp
cp "$config_file" "$backup_file"
log "Created backup of LiteSpeed configuration at $backup_file"
2025-03-20 18:22:22 +00:00
# Clean up any redundant listeners for this domain
2025-03-20 18:07:43 +00:00
cleanup_redundant_listeners "$config_file" "$DOMAIN"
2025-03-21 18:11:15 +00:00
# Create domain-specific virtual host
2025-03-20 18:22:22 +00:00
if ! create_domain_virtual_host "$DOMAIN"; then
log "ERROR: Failed to create virtual host for $DOMAIN. Aborting configuration update."
return 1
fi
2025-03-20 18:07:43 +00:00
2025-03-21 18:11:15 +00:00
# Create domain-specific listener
2025-03-20 18:22:22 +00:00
if ! create_domain_listener "$DOMAIN"; then
log "ERROR: Failed to create listener for $DOMAIN. Aborting configuration update."
return 1
2025-03-20 18:07:43 +00:00
fi
2025-03-20 18:22:22 +00:00
# Remove domain from shared listeners - safer to avoid certificate mismatch errors
remove_domain_from_shared_listeners
2025-03-20 18:07:43 +00:00
# Final validation of the complete file
if ! validate_xml_config "$config_file" "$backup_file"; then
log "ERROR: Configuration update failed validation. Reverting to backup."
cp "$backup_file" "$config_file"
return 1
fi
2025-03-20 18:22:22 +00:00
log "LiteSpeed configuration updated successfully with dedicated domain configuration."
2025-03-20 18:07:43 +00:00
return 0
}
# Function to set up automatic renewal
setup_cron_job() {
log "Setting up cron job for Certbot renewal..."
# Ensure crond is running
if ! systemctl is-active --quiet crond; then
log "Starting crond service..."
sudo systemctl start crond
sudo systemctl enable crond
fi
# Add cron job for Certbot renewal
if ! crontab -l 2>/dev/null | grep -q "certbot renew"; then
(crontab -l 2>/dev/null; echo "0 3 * * * /usr/bin/certbot renew --quiet") | crontab -
log "Cron job added for Certbot renewal."
else
log "Cron job for Certbot renewal already exists."
fi
# Verify cron job
log "Verifying cron job..."
if crontab -l | grep -q "certbot renew"; then
log "Cron job successfully set up."
else
log "Failed to set up cron job. Please check manually."
exit 1
fi
}
2025-03-20 18:07:43 +00:00
# Function to validate XML configuration with better error handling and fallbacks
validate_xml_config() {
local config_file="$1"
local backup_file="$2"
log "Validating XML configuration..."
2025-03-25 13:09:26 +00:00
# Check basic tag balance first
local open_listeners=$(grep -c '<listener>' "$config_file")
local close_listeners=$(grep -c '</listener>' "$config_file")
2025-03-20 18:07:43 +00:00
2025-03-25 13:09:26 +00:00
if [ "$open_listeners" -ne "$close_listeners" ]; then
log "ERROR: Listener tag mismatch (${open_listeners} open vs ${close_listeners} close)"
return 1
2025-03-20 18:07:43 +00:00
fi
2025-03-25 13:09:26 +00:00
# Use xmllint if available
if command -v xmllint >/dev/null; then
if ! xmllint --noout "$config_file"; then
log "ERROR: XML validation failed with xmllint"
return 1
fi
2025-03-20 18:07:43 +00:00
fi
2025-03-25 13:09:26 +00:00
return 0
2025-03-20 18:07:43 +00:00
}
# Function to clean up redundant listeners with more reliable pattern matching
cleanup_redundant_listeners() {
local config_file="$1"
local domain="$2"
log "Checking for redundant listeners..."
# Use grep to find the exact line numbers of redundant listeners
local line_nums=$(grep -n "HTTPS-$domain" "$config_file" | cut -d: -f1)
if [ -n "$line_nums" ]; then
log "Found redundant listener(s) for $domain. Cleaning up..."
# Create a temporary file
local temp_file=$(mktemp)
if [ ! -f "$temp_file" ]; then
log "Error: Failed to create temporary file for cleanup."
return 0
fi
# Copy the original file
cp "$config_file" "$temp_file"
# For each match, find the enclosing <listener> tags and remove the section
for line_num in $line_nums; do
# Find the start of the listener section (search backward for opening tag)
local start_line=$(head -n "$line_num" "$config_file" | grep -n "<listener>" | tail -1 | cut -d: -f1)
if [ -z "$start_line" ]; then
continue
fi
# Find the end of the listener section (search forward for closing tag)
local end_line=$(tail -n "+$line_num" "$config_file" | grep -n "</listener>" | head -1 | cut -d: -f1)
if [ -z "$end_line" ]; then
continue
fi
end_line=$((line_num + end_line - 1))
# Remove the section from the temp file
sed -i "${start_line},${end_line}d" "$temp_file"
done
# Add a comment to indicate removal
echo "<!-- Redundant listeners for $domain removed by ssl_manager.sh -->" >> "$temp_file"
# Verify the result isn't empty or corrupted
if [ -s "$temp_file" ] && grep -q "<httpServerConfig>" "$temp_file" && grep -q "</httpServerConfig>" "$temp_file"; then
cp "$temp_file" "$config_file"
log "Redundant listeners successfully removed."
else
log "Error: Generated configuration is invalid. Keeping original."
fi
rm -f "$temp_file"
else
log "No redundant listeners found."
fi
return 0
}
# Function to add domain mapping to a listener if it doesn't exist - Updated to use domain-specific virtual host
add_domain_mapping() {
local listener_name="$1"
local start_pattern="<name>$listener_name</name>"
local map_pattern="<vhostMapList>"
local config_file="/var/www/conf/httpd_config.xml"
local vhost_name="${DOMAIN%%.*}"
# Check if listener exists
if ! grep -q "$start_pattern" "$config_file"; then
log "Error: Listener $listener_name not found in configuration."
return 1
fi
# Skip if mapping already exists (safer approach)
if grep -A30 "<name>$listener_name</name>" "$config_file" | grep -q "<domain>$DOMAIN</domain>"; then
log "Domain mapping for $DOMAIN already exists in $listener_name listener. Updating virtual host mapping..."
# Update existing mapping to point to the correct virtual host
sed -i "/<domain>$DOMAIN<\/domain>/,/<\/vhostMap>/s/<vhost>Jelastic<\/vhost>/<vhost>$vhost_name<\/vhost>/g" "$config_file"
log "Updated existing domain mapping to use correct virtual host."
return 0
fi
log "Adding domain mapping for $DOMAIN to $listener_name listener..."
# Create a temporary file for processing
local temp_file=$(mktemp)
if [ $? -ne 0 ]; then
log "Error: Failed to create temporary file. Skipping domain mapping for $listener_name."
return 1
fi
# Process the file to add domain mapping with correct virtual host
awk -v domain="$DOMAIN" -v vhost="$vhost_name" -v start="$start_pattern" -v pattern="$map_pattern" '
{
print $0
if ($0 ~ start) {
in_listener = 1
} else if (in_listener && $0 ~ /<\/listener>/) {
in_listener = 0
} else if (in_listener && $0 ~ pattern) {
print " <vhostMap>"
print " <vhost>" vhost "</vhost>"
print " <domain>" domain "</domain>"
print " </vhostMap>"
}
}
' "$config_file" > "$temp_file"
# Verify the processed file is valid and not empty
if [ ! -s "$temp_file" ]; then
log "Error: Generated configuration is empty. Keeping original configuration."
rm -f "$temp_file"
return 1
fi
# Check if the XML structure looks valid (basic check)
if ! grep -q "<vhostMapList>" "$temp_file"; then
log "Error: Generated configuration appears invalid. Keeping original configuration."
rm -f "$temp_file"
return 1
fi
# Compare to ensure we didn't remove anything important (line count check)
original_lines=$(wc -l < "$config_file")
new_lines=$(wc -l < "$temp_file")
if [ $new_lines -lt $(($original_lines - 5)) ]; then
log "Error: New configuration is significantly smaller than original. Aborting."
rm -f "$temp_file"
return 1
fi
# Replace original with processed file
cp "$temp_file" "$config_file"
if [ $? -ne 0 ]; then
log "Error: Failed to update configuration file. Keeping original configuration."
rm -f "$temp_file"
return 1
fi
# Clean up
rm -f "$temp_file"
log "Domain mapping added successfully to $listener_name listener with correct virtual host."
return 0
}
# Function to check if domain mapping already exists in a listener
domain_mapping_exists() {
local listener_name="$1"
local config_file="/var/www/conf/httpd_config.xml"
# Check if the domain is already mapped in this listener
if grep -A20 "<name>$listener_name</name>" "$config_file" | grep -q "<domain>$DOMAIN</domain>"; then
return 0 # Domain mapping exists
else
return 1 # Domain mapping doesn't exist
fi
}
# Ensure XML validation tools are installed
install_xml_tools() {
log "Checking for XML validation tools..."
if ! command -v xmllint > /dev/null; then
log "Installing XML validation tools..."
if grep -q "AlmaLinux" /etc/os-release; then
dnf install -y libxml2
elif [[ -f /etc/debian_version ]]; then
apt-get update && apt-get install -y libxml2-utils
elif [[ -f /etc/redhat-release ]]; then
yum install -y libxml2
else
log "WARNING: Cannot install XML validation tools automatically. Manual validation will be skipped."
return 1
fi
fi
log "XML validation tools are available."
return 0
}
2025-03-20 18:22:22 +00:00
# Function to create or update a domain-specific HTTPS listener
2025-03-20 18:07:43 +00:00
create_domain_listener() {
2025-03-21 18:21:43 +00:00
local domain=$1
2025-03-20 18:22:22 +00:00
local config_file="/var/www/conf/httpd_config.xml"
local vhost_name="${domain//[.]/_}"
2025-03-20 18:07:43 +00:00
local key_file="/etc/letsencrypt/live/$domain/privkey.pem"
local cert_file="/etc/letsencrypt/live/$domain/fullchain.pem"
2025-03-20 18:22:22 +00:00
local timestamp=$(date +%Y%m%d%H%M%S)
local backup_file="${config_file}.backup.${timestamp}"
2025-03-25 13:09:26 +00:00
2025-03-20 18:22:22 +00:00
log "Creating/updating domain-specific HTTPS listener for $domain..."
2025-03-20 18:07:43 +00:00
2025-03-20 18:22:22 +00:00
# Create backup if not already done
2025-03-25 13:09:26 +00:00
[ -f "$backup_file" ] || cp "$config_file" "$backup_file"
# Check for existing listener
2025-03-20 18:07:43 +00:00
if grep -q "<name>HTTPS-$domain</name>" "$config_file"; then
2025-03-25 13:09:26 +00:00
log "Updating existing listener for $domain..."
# Use full XML scope for replacements
sed -i "/<name>HTTPS-$domain<\/name>/,/<\/listener>/ {
s|<keyFile>.*</keyFile>|<keyFile>$key_file</keyFile>|;
s|<certFile>.*</certFile>|<certFile>$cert_file</certFile>|;
}" "$config_file"
2025-03-20 18:07:43 +00:00
return 0
fi
2025-03-25 13:09:26 +00:00
2025-03-20 18:22:22 +00:00
log "Creating new HTTPS listener for $domain..."
2025-03-25 13:09:26 +00:00
# Generate properly indented XML block
listener_xml=$(cat <<EOF
<listener>
<name>HTTPS-${domain}</name>
<address>*:443</address>
<secure>1</secure>
<vhostMapList>
<vhostMap>
<vhost>${vhost_name}</vhost>
<domain>${domain}</domain>
</vhostMap>
</vhostMapList>
<keyFile>${key_file}</keyFile>
<certFile>${cert_file}</certFile>
<sslProtocol>24</sslProtocol>
<ciphers>ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384</ciphers>
</listener>
EOF
)
# Insert new listener before the listenerList closing tag
awk -v xml="$listener_xml" '
2025-03-20 18:07:43 +00:00
/<\/listenerList>/ {
2025-03-25 13:09:26 +00:00
print xml
2025-03-20 18:22:22 +00:00
print $0
2025-03-25 13:09:26 +00:00
inserted=1
2025-03-20 18:22:22 +00:00
next
}
{ print }
2025-03-25 13:09:26 +00:00
END {
if (!inserted) {
print "ERROR: Failed to find listenerList closing tag"
exit 1
}
}' "$config_file" > "${config_file}.tmp" && mv "${config_file}.tmp" "$config_file"
# Validate XML structure after modification
if ! validate_xml_config "$config_file" "$backup_file"; then
log "ERROR: Failed to create valid listener for $domain"
2025-03-20 18:22:22 +00:00
return 1
fi
2025-03-25 13:09:26 +00:00
2025-03-20 18:22:22 +00:00
log "Domain-specific HTTPS listener for $domain created successfully."
return 0
}
# Function to create or update domain-specific virtual host
create_domain_virtual_host() {
local domain="$1"
local config_file="/var/www/conf/httpd_config.xml"
local vhost_name="${domain//[.]/_}"
log "Checking if virtual host for $domain needs to be created..."
# Check if virtual host already exists
if grep -q "<name>$vhost_name</name>" "$config_file"; then
log "Virtual host '$vhost_name' already exists, skipping creation."
return 0
fi
log "Creating virtual host for $domain..."
local temp_file=$(mktemp)
if [ ! -f "$temp_file" ]; then
log "ERROR: Failed to create temporary file for virtual host creation."
return 1
fi
# Insert new virtual host before virtualHostList end tag
awk -v vhost="$vhost_name" '
/<\/virtualHostList>/ {
print " <virtualHost>"
print " <name>" vhost "</name>"
print " <vhRoot>/var/www/webroot/</vhRoot>"
print " <configFile>$SERVER_ROOT/conf/vhconf.xml</configFile>"
print " <allowSymbolLink>1</allowSymbolLink>"
print " <enableScript>1</enableScript>"
print " <restrained>1</restrained>"
print " <setUIDMode>0</setUIDMode>"
print " <chrootMode>0</chrootMode>"
print " </virtualHost>"
print $0
next
2025-03-20 18:07:43 +00:00
}
{ print }
' "$config_file" > "$temp_file"
2025-03-20 18:22:22 +00:00
# Validate the temporary file
if [ ! -s "$temp_file" ]; then
log "ERROR: Generated virtual host configuration is empty. Keeping original configuration."
rm -f "$temp_file"
return 1
fi
# Apply changes
cp "$temp_file" "$config_file"
if [ $? -ne 0 ]; then
log "ERROR: Failed to update configuration with new virtual host. Keeping original configuration."
rm -f "$temp_file"
return 1
fi
# Clean up
rm -f "$temp_file"
log "Virtual host for $domain created successfully."
return 0
}
# Function to remove domain from shared listeners to avoid certificate mismatch
remove_domain_from_shared_listeners() {
local config_file="/var/www/conf/httpd_config.xml"
local domain="$DOMAIN"
log "Removing $domain from shared listeners to prevent certificate mismatch..."
# Create temporary file
local temp_file=$(mktemp)
if [ ! -f "$temp_file" ]; then
log "ERROR: Failed to create temporary file for shared listener cleanup."
return 1
fi
# For HTTPS listener
awk -v domain="$domain" '
/<name>HTTPS<\/name>/,/<\/listener>/ {
if ($0 ~ /<vhostMap>/) {
in_vhostmap = 1
vhostmap_buffer = $0 "\n"
next
}
if (in_vhostmap) {
vhostmap_buffer = vhostmap_buffer $0 "\n"
if ($0 ~ /<\/vhostMap>/) {
if (vhostmap_buffer !~ domain) {
printf "%s", vhostmap_buffer
}
in_vhostmap = 0
vhostmap_buffer = ""
}
next
}
}
{ print }
' "$config_file" > "$temp_file"
# Check if changes were made correctly
if [ ! -s "$temp_file" ]; then
log "ERROR: Generated configuration is empty after domain removal. Keeping original configuration."
rm -f "$temp_file"
return 1
fi
cp "$temp_file" "$config_file"
rm -f "$temp_file"
# For HTTPS-ipv6 listener - repeat the process
temp_file=$(mktemp)
if [ ! -f "$temp_file" ]; then
log "ERROR: Failed to create temporary file for shared listener cleanup."
return 1
fi
awk -v domain="$domain" '
/<name>HTTPS-ipv6<\/name>/,/<\/listener>/ {
if ($0 ~ /<vhostMap>/) {
in_vhostmap = 1
vhostmap_buffer = $0 "\n"
next
}
if (in_vhostmap) {
vhostmap_buffer = vhostmap_buffer $0 "\n"
if ($0 ~ /<\/vhostMap>/) {
if (vhostmap_buffer !~ domain) {
printf "%s", vhostmap_buffer
}
in_vhostmap = 0
vhostmap_buffer = ""
}
next
}
}
{ print }
' "$config_file" > "$temp_file"
if [ ! -s "$temp_file" ]; then
log "ERROR: Generated configuration is empty after domain removal. Keeping original configuration."
rm -f "$temp_file"
return 1
fi
cp "$temp_file" "$config_file"
rm -f "$temp_file"
log "Domain successfully removed from shared listeners."
return 0
}
2025-03-25 13:09:26 +00:00
# Revised service restart with pre-check
2025-03-20 18:22:22 +00:00
restart_litespeed() {
log "Restarting LiteSpeed web server..."
2025-03-25 13:09:26 +00:00
# Configuration test first
if /usr/local/lsws/bin/lshttpd -t 2>&1 | grep -q "Configuration file check failed"; then
log "ERROR: Configuration test failed, not restarting"
return 1
2025-03-20 18:22:22 +00:00
fi
2025-03-25 13:09:26 +00:00
systemctl restart lsws
sleep 2
if ! systemctl is-active --quiet lsws; then
log "ERROR: LiteSpeed failed to start"
return 1
2025-03-20 18:22:22 +00:00
fi
2025-03-25 13:09:26 +00:00
log "LiteSpeed successfully restarted"
2025-03-20 18:22:22 +00:00
return 0
2025-03-20 18:07:43 +00:00
}
# Parse input parameters
2025-03-21 18:21:43 +00:00
declare -a DOMAINS
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#*=}"
;;
*)
echo "Invalid argument: $arg"
exit 1
;;
esac
done
# Input validation
log "Validating inputs..."
2025-03-21 18:21:43 +00:00
if [[ -z "${PUBLIC_IP:-}" || ${#DOMAINS[@]} -eq 0 ]]; then
echo "Error: --public-ip and --domain(s) are mandatory."
exit 1
fi
validate_ip "$PUBLIC_IP" || { echo "Invalid public IP: $PUBLIC_IP"; exit 1; }
2025-03-21 18:21:43 +00:00
for domain in "${DOMAINS[@]}"; do
validate_domain "$domain" || { echo "Invalid domain: $domain"; exit 1; }
done
if [[ -n "${EMAIL:-}" ]]; then
validate_email "$EMAIL" || { echo "Invalid email: $EMAIL"; exit 1; }
fi
2025-03-21 18:21:43 +00:00
# Main execution loop
for DOMAIN in "${DOMAINS[@]}"; do
log "Processing domain: $DOMAIN"
2025-03-21 18:21:43 +00:00
# Validate the domain connection
validate_domain_connection
# Install Certbot
log "Installing Certbot..."
if ! command -v certbot > /dev/null; then
if [[ -f /etc/debian_version ]]; then
apt-get update && apt-get install -y certbot
elif [[ -f /etc/redhat-release ]]; then
# Check if it's AlmaLinux or other RHEL derivatives
if grep -q "AlmaLinux" /etc/os-release; then
log "Detected AlmaLinux. Installing EPEL repository and Certbot..."
# Install EPEL repository first
dnf install -y epel-release
# Install Certbot and Python modules for the webroot plugin
dnf install -y certbot python3-certbot-apache
else
# Fallback for other RHEL-based systems
yum install -y certbot
fi
2025-03-20 18:07:43 +00:00
else
2025-03-21 18:21:43 +00:00
echo "Unsupported OS. Install Certbot manually."
exit 1
2025-03-20 18:07:43 +00:00
fi
fi
2025-03-21 18:21:43 +00:00
# Check for existing certificate before requesting
if [[ -d "/etc/letsencrypt/live/$DOMAIN" ]]; then
log "Certificate for $DOMAIN already exists. Checking expiry..."
EXPIRY=$(openssl x509 -enddate -noout -in "/etc/letsencrypt/live/$DOMAIN/cert.pem" | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))
if [[ $DAYS_LEFT -gt 30 ]]; then
log "Certificate still valid for $DAYS_LEFT days. Skipping renewal."
update_litespeed_config
setup_cron_job
continue
else
log "Certificate expires in $DAYS_LEFT days. Proceeding with renewal."
fi
2025-03-20 18:07:43 +00:00
fi
2025-03-21 18:21:43 +00:00
# Modify Certbot command to include all domains
CERTBOT_CMD="certbot certonly --webroot -w /var/www/webroot/ROOT"
for domain in "${DOMAINS[@]}"; do
CERTBOT_CMD+=" -d $domain"
done
CERTBOT_CMD+=" --agree-tos --non-interactive"
[[ -n "${EMAIL:-}" ]] && CERTBOT_CMD+=" --email $EMAIL"
2025-03-20 18:07:43 +00:00
2025-03-21 18:21:43 +00:00
# After Certbot installation and before existing certificate check
install_xml_tools
2025-03-20 18:07:43 +00:00
2025-03-21 18:21:43 +00:00
# Replace the simple reload with the improved function
if $CERTBOT_CMD; then
log "SSL certificate issued successfully for $DOMAIN."
# Update LiteSpeed config with enhanced safety
if update_litespeed_config; then
restart_litespeed
send_email "$DOMAIN SSL Certificate Issued Successfully" "The SSL certificate for $DOMAIN has been successfully installed."
setup_cron_job
else
log "ERROR: Failed to update LiteSpeed configuration. Manually check your configuration."
send_email "SSL Certificate Installation Warning" "The SSL certificate for $DOMAIN was issued successfully, but there was an error updating the LiteSpeed configuration. Please check your server configuration manually."
fi
2025-03-20 18:07:43 +00:00
else
2025-03-21 18:21:43 +00:00
log "Certbot failed."
send_email "SSL Certificate Installation Failed" "An error occurred while installing the SSL certificate for $DOMAIN."
exit 1
2025-03-20 18:07:43 +00:00
fi
2025-03-21 18:21:43 +00:00
done