From 1d22d789b85ce95260ce0e15f1ce16b3327b30a0 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Thu, 20 Nov 2025 09:56:56 -0800 Subject: [PATCH] Simplify certificate renewal scripts and add certbot cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplified all certificate renewal scripts to be more straightforward and reliable: - Scripts now just run certbot renew and copy cert+key files to HAProxy format - Removed overly complex retry logic and error handling - Both in-container and host-side scripts work with cron scheduling Added automatic certbot cleanup when domains are removed: - When a domain is deleted via API, certbot certificate is also removed - Prevents renewal errors for domains that no longer exist in HAProxy - Cleans up both HAProxy combined cert and Let's Encrypt certificate Script changes: - renew-certificates.sh: Simplified to 87 lines (from 215) - sync-certificates.sh: Simplified to 79 lines (from 200+) - host-renew-certificates.sh: Simplified to 36 lines (from 40) - All scripts use same pattern: query DB, copy certs, reload HAProxy Python changes: - remove_domain() now calls 'certbot delete' to remove certificates - Prevents orphaned certificates from causing renewal failures 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- haproxy_manager.py | 35 +++-- scripts/host-renew-certificates.sh | 15 +- scripts/renew-certificates.sh | 232 +++++++---------------------- scripts/sync-certificates.sh | 217 ++++++--------------------- 4 files changed, 128 insertions(+), 371 deletions(-) diff --git a/haproxy_manager.py b/haproxy_manager.py index 7b44d2e..8ccc932 100644 --- a/haproxy_manager.py +++ b/haproxy_manager.py @@ -682,15 +682,15 @@ def remove_domain(): with sqlite3.connect(DB_FILE) as conn: cursor = conn.cursor() - # Get domain ID and check if it exists - cursor.execute('SELECT id FROM domains WHERE domain = ?', (domain,)) + # Get domain ID and SSL status + cursor.execute('SELECT id, ssl_enabled, ssl_cert_path FROM domains WHERE domain = ?', (domain,)) domain_result = cursor.fetchone() if not domain_result: log_operation('remove_domain', False, f'Domain {domain} not found') return jsonify({'status': 'error', 'message': 'Domain not found'}), 404 - domain_id = domain_result[0] + domain_id, ssl_enabled, ssl_cert_path = domain_result # Get backend IDs associated with this domain cursor.execute('SELECT id FROM backends WHERE domain_id = ?', (domain_id,)) @@ -706,14 +706,27 @@ def remove_domain(): # Delete domain cursor.execute('DELETE FROM domains WHERE id = ?', (domain_id,)) - # Delete SSL certificate if it exists - cursor.execute('SELECT ssl_cert_path FROM domains WHERE id = ? AND ssl_enabled = 1', (domain_id,)) - cert_result = cursor.fetchone() - if cert_result and cert_result[0]: - try: - os.remove(cert_result[0]) - except OSError: - pass # Ignore errors if file doesn't exist + # Delete SSL certificate from HAProxy certs directory + if ssl_enabled and ssl_cert_path: + try: + os.remove(ssl_cert_path) + logger.info(f"Removed HAProxy certificate file: {ssl_cert_path}") + except OSError as e: + logger.warning(f"Failed to remove certificate file {ssl_cert_path}: {e}") + + # Remove certificate from certbot + if ssl_enabled: + try: + result = subprocess.run( + ['certbot', 'delete', '--cert-name', domain, '--non-interactive'], + capture_output=True, text=True + ) + if result.returncode == 0: + logger.info(f"Removed Let's Encrypt certificate for {domain}") + else: + logger.warning(f"Failed to remove Let's Encrypt certificate for {domain}: {result.stderr}") + except Exception as e: + logger.warning(f"Error removing Let's Encrypt certificate for {domain}: {e}") # Regenerate HAProxy config generate_config() diff --git a/scripts/host-renew-certificates.sh b/scripts/host-renew-certificates.sh index 97e1d45..6a820ec 100755 --- a/scripts/host-renew-certificates.sh +++ b/scripts/host-renew-certificates.sh @@ -1,16 +1,15 @@ #!/usr/bin/env bash # Host-side Certificate Renewal Script -# This script can be run from the host machine via cron to trigger certificate renewal -# inside the HAProxy Manager container using docker exec +# Run this from the host machine via cron to trigger certificate renewal inside the container set -e -# Configuration - Customize these values +# Configuration CONTAINER_NAME="${CONTAINER_NAME:-haproxy-manager}" LOG_FILE="${LOG_FILE:-/var/log/haproxy-manager-host-renewal.log}" -# Logging functions +# Logging log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*" | tee -a "$LOG_FILE" } @@ -19,8 +18,7 @@ log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" | tee -a "$LOG_FILE" } -# Main execution -log_info "Starting host-side certificate renewal process" +log_info "Starting certificate renewal" # Check if container is running if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then @@ -28,10 +26,9 @@ if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then exit 1 fi -# Execute renewal script inside container -log_info "Executing renewal script in container '${CONTAINER_NAME}'" +# Run renewal script inside container if docker exec "$CONTAINER_NAME" /haproxy/scripts/renew-certificates.sh; then - log_info "Certificate renewal completed successfully" + log_info "Certificate renewal completed" exit 0 else log_error "Certificate renewal failed" diff --git a/scripts/renew-certificates.sh b/scripts/renew-certificates.sh index 1bc41fc..9668636 100644 --- a/scripts/renew-certificates.sh +++ b/scripts/renew-certificates.sh @@ -1,18 +1,15 @@ #!/usr/bin/env bash # Certificate Renewal Script for HAProxy Manager -# This script handles Let's Encrypt certificate renewal with proper logging and error handling +# This script runs certbot renew and copies certificates to HAProxy format set -e # Configuration LOG_FILE="${LOG_FILE:-/var/log/haproxy-manager.log}" ERROR_LOG_FILE="${ERROR_LOG_FILE:-/var/log/haproxy-manager-errors.log}" -HAPROXY_SOCKET="${HAPROXY_SOCKET:-/tmp/haproxy-cli}" DB_FILE="${DB_FILE:-/etc/haproxy/haproxy_config.db}" SSL_CERTS_DIR="${SSL_CERTS_DIR:-/etc/haproxy/certs}" -MAX_RETRIES=3 -RETRY_DELAY=5 # Logging functions log_info() { @@ -23,192 +20,67 @@ log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" | tee -a "$LOG_FILE" >> "$ERROR_LOG_FILE" } -log_warning() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARNING] $*" | tee -a "$LOG_FILE" -} - -# Check if certbot is available -if ! command -v certbot &> /dev/null; then - log_error "certbot command not found" - exit 1 -fi - -# Check if HAProxy socket exists and is accessible -check_haproxy_socket() { - if [ ! -S "$HAPROXY_SOCKET" ]; then - log_warning "HAProxy socket not found at $HAPROXY_SOCKET" - return 1 - fi - - # Test socket connectivity - if ! echo "show info" | socat stdio "$HAPROXY_SOCKET" &> /dev/null; then - log_warning "HAProxy socket exists but is not responding" - return 1 - fi - - return 0 -} - -# Reload HAProxy configuration -reload_haproxy() { - local retry_count=0 - - while [ $retry_count -lt $MAX_RETRIES ]; do - if check_haproxy_socket; then - log_info "Reloading HAProxy via socket" - if echo "reload" | socat stdio "$HAPROXY_SOCKET"; then - log_info "HAProxy reloaded successfully" - return 0 - else - log_warning "HAProxy reload command failed (attempt $((retry_count + 1))/$MAX_RETRIES)" - fi - else - log_warning "HAProxy socket check failed (attempt $((retry_count + 1))/$MAX_RETRIES)" - fi - - retry_count=$((retry_count + 1)) - if [ $retry_count -lt $MAX_RETRIES ]; then - sleep $RETRY_DELAY - fi - done - - log_error "Failed to reload HAProxy after $MAX_RETRIES attempts" - return 1 -} - -# Update combined certificate files for HAProxy -update_combined_certificates() { - log_info "Updating combined certificate files for HAProxy" - - # Check if database exists - if [ ! -f "$DB_FILE" ]; then - log_error "Database file not found at $DB_FILE" - return 1 - fi - - # Check if sqlite3 is available - if ! command -v sqlite3 &> /dev/null; then - log_error "sqlite3 command not found" - return 1 - fi - - # Ensure SSL certs directory exists - mkdir -p "$SSL_CERTS_DIR" - - # Get all domains with SSL enabled from database - local domains - domains=$(sqlite3 "$DB_FILE" "SELECT domain, ssl_cert_path FROM domains WHERE ssl_enabled = 1;" 2>/dev/null) - - if [ -z "$domains" ]; then - log_info "No SSL-enabled domains found in database" - return 0 - fi - - local updated_count=0 - local error_count=0 - - # Process each domain - while IFS='|' read -r domain cert_path; do - if [ -z "$domain" ] || [ -z "$cert_path" ]; then - continue - fi - - log_info "Processing certificate for domain: $domain" - - local letsencrypt_cert="/etc/letsencrypt/live/${domain}/fullchain.pem" - local letsencrypt_key="/etc/letsencrypt/live/${domain}/privkey.pem" - - # Check if Let's Encrypt certificate files exist - if [ ! -f "$letsencrypt_cert" ]; then - log_warning "Certificate not found for $domain at $letsencrypt_cert" - error_count=$((error_count + 1)) - continue - fi - - if [ ! -f "$letsencrypt_key" ]; then - log_warning "Private key not found for $domain at $letsencrypt_key" - error_count=$((error_count + 1)) - continue - fi - - # Combine certificate and key into single file for HAProxy - # HAProxy requires fullchain.pem followed by privkey.pem in a single file - # Write to temp file first, then move to ensure atomic update - local temp_cert="${cert_path}.tmp" - if cat "$letsencrypt_cert" "$letsencrypt_key" > "$temp_cert"; then - # Verify the combined file is not empty and contains valid data - if [ -s "$temp_cert" ]; then - # Atomically move to final location - if mv "$temp_cert" "$cert_path"; then - log_info "Updated combined certificate for $domain at $cert_path" - updated_count=$((updated_count + 1)) - else - log_error "Failed to move combined certificate for $domain to $cert_path" - rm -f "$temp_cert" - error_count=$((error_count + 1)) - fi - else - log_error "Combined certificate file for $domain is empty" - rm -f "$temp_cert" - error_count=$((error_count + 1)) - fi - else - log_error "Failed to combine certificate files for $domain" - rm -f "$temp_cert" - error_count=$((error_count + 1)) - fi - done <<< "$domains" - - log_info "Certificate update completed: $updated_count updated, $error_count errors" - - if [ $error_count -gt 0 ]; then - return 1 - fi - - return 0 -} - -# Main renewal process log_info "Starting certificate renewal process" # Run certbot renewal -if certbot renew --quiet --no-random-sleep-on-renew 2>&1 | tee -a "$LOG_FILE"; then - RENEWAL_EXIT_CODE=${PIPESTATUS[0]} +if certbot renew --quiet --no-random-sleep-on-renew; then + log_info "Certbot renewal completed" +else + log_error "Certbot renewal failed with exit code $?" + exit 1 +fi - if [ $RENEWAL_EXIT_CODE -eq 0 ]; then - log_info "Certificate renewal completed successfully" +# Copy all certificates to HAProxy format +if [ ! -f "$DB_FILE" ]; then + log_error "Database file not found at $DB_FILE" + exit 1 +fi - # Always update combined certificate files after renewal - # (certbot may have renewed some certificates even if the message says otherwise) - log_info "Updating combined certificate files for HAProxy" - if update_combined_certificates; then - log_info "Combined certificates updated successfully" +# Ensure SSL certs directory exists +mkdir -p "$SSL_CERTS_DIR" - # Reload HAProxy to pick up the updated certificates - log_info "Reloading HAProxy" - if reload_haproxy; then - log_info "Certificate renewal and HAProxy reload completed successfully" - else - log_error "Certificate renewal succeeded but HAProxy reload failed" - exit 1 - fi +# Get all SSL-enabled domains from database +DOMAINS=$(sqlite3 "$DB_FILE" "SELECT domain FROM domains WHERE ssl_enabled = 1;" 2>/dev/null) + +if [ -z "$DOMAINS" ]; then + log_info "No SSL-enabled domains found" + exit 0 +fi + +# Copy certificates for each domain +UPDATED=0 +FAILED=0 + +while read -r domain; do + CERT_FILE="/etc/letsencrypt/live/${domain}/fullchain.pem" + KEY_FILE="/etc/letsencrypt/live/${domain}/privkey.pem" + COMBINED_FILE="${SSL_CERTS_DIR}/${domain}.pem" + + if [ -f "$CERT_FILE" ] && [ -f "$KEY_FILE" ]; then + # Combine cert and key into single file for HAProxy + if cat "$CERT_FILE" "$KEY_FILE" > "$COMBINED_FILE"; then + log_info "Updated certificate for $domain" + UPDATED=$((UPDATED + 1)) else - log_warning "Certificate update completed with some errors, but attempting HAProxy reload" - # Still try to reload HAProxy even if some certificates failed - if reload_haproxy; then - log_warning "HAProxy reloaded successfully despite certificate update errors" - else - log_error "Certificate update had errors and HAProxy reload failed" - exit 1 - fi + log_error "Failed to combine certificate for $domain" + FAILED=$((FAILED + 1)) fi else - log_error "Certificate renewal failed with exit code $RENEWAL_EXIT_CODE" - exit $RENEWAL_EXIT_CODE + log_error "Certificate files not found for $domain" + FAILED=$((FAILED + 1)) + fi +done <<< "$DOMAINS" + +log_info "Certificate update completed: $UPDATED updated, $FAILED failed" + +# Reload HAProxy if any certificates were updated +if [ $UPDATED -gt 0 ]; then + if echo "reload" | socat stdio /tmp/haproxy-cli 2>/dev/null; then + log_info "HAProxy reloaded successfully" + else + log_error "Failed to reload HAProxy" + exit 1 fi -else - log_error "Certificate renewal command failed" - exit 1 fi log_info "Certificate renewal process completed" diff --git a/scripts/sync-certificates.sh b/scripts/sync-certificates.sh index bda73e4..bc0a751 100755 --- a/scripts/sync-certificates.sh +++ b/scripts/sync-certificates.sh @@ -1,19 +1,15 @@ #!/usr/bin/env bash # Certificate Sync Script for HAProxy Manager -# This script syncs all Let's Encrypt certificates to HAProxy format -# Use this to update all certificates regardless of renewal status +# This script syncs all Let's Encrypt certificates to HAProxy format without running certbot renew set -e # Configuration LOG_FILE="${LOG_FILE:-/var/log/haproxy-manager.log}" ERROR_LOG_FILE="${ERROR_LOG_FILE:-/var/log/haproxy-manager-errors.log}" -HAPROXY_SOCKET="${HAPROXY_SOCKET:-/tmp/haproxy-cli}" DB_FILE="${DB_FILE:-/etc/haproxy/haproxy_config.db}" SSL_CERTS_DIR="${SSL_CERTS_DIR:-/etc/haproxy/certs}" -MAX_RETRIES=3 -RETRY_DELAY=5 # Logging functions log_info() { @@ -24,178 +20,57 @@ log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" | tee -a "$LOG_FILE" >> "$ERROR_LOG_FILE" } -log_warning() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARNING] $*" | tee -a "$LOG_FILE" -} - -# Check if HAProxy socket exists and is accessible -check_haproxy_socket() { - if [ ! -S "$HAPROXY_SOCKET" ]; then - log_warning "HAProxy socket not found at $HAPROXY_SOCKET" - return 1 - fi - - # Test socket connectivity - if ! echo "show info" | socat stdio "$HAPROXY_SOCKET" &> /dev/null; then - log_warning "HAProxy socket exists but is not responding" - return 1 - fi - - return 0 -} - -# Reload HAProxy configuration -reload_haproxy() { - local retry_count=0 - - while [ $retry_count -lt $MAX_RETRIES ]; do - if check_haproxy_socket; then - log_info "Reloading HAProxy via socket" - if echo "reload" | socat stdio "$HAPROXY_SOCKET"; then - log_info "HAProxy reloaded successfully" - return 0 - else - log_warning "HAProxy reload command failed (attempt $((retry_count + 1))/$MAX_RETRIES)" - fi - else - log_warning "HAProxy socket check failed (attempt $((retry_count + 1))/$MAX_RETRIES)" - fi - - retry_count=$((retry_count + 1)) - if [ $retry_count -lt $MAX_RETRIES ]; then - sleep $RETRY_DELAY - fi - done - - log_error "Failed to reload HAProxy after $MAX_RETRIES attempts" - return 1 -} - -# Sync all certificate files for HAProxy -sync_all_certificates() { - log_info "Syncing all certificate files to HAProxy format" - - # Check if database exists - if [ ! -f "$DB_FILE" ]; then - log_error "Database file not found at $DB_FILE" - return 1 - fi - - # Check if sqlite3 is available - if ! command -v sqlite3 &> /dev/null; then - log_error "sqlite3 command not found" - return 1 - fi - - # Ensure SSL certs directory exists - mkdir -p "$SSL_CERTS_DIR" - - # Get all domains with SSL enabled from database - local domains - domains=$(sqlite3 "$DB_FILE" "SELECT domain, ssl_cert_path FROM domains WHERE ssl_enabled = 1;" 2>/dev/null) - - if [ -z "$domains" ]; then - log_info "No SSL-enabled domains found in database" - return 0 - fi - - local updated_count=0 - local error_count=0 - local skipped_count=0 - - # Process each domain - while IFS='|' read -r domain cert_path; do - if [ -z "$domain" ] || [ -z "$cert_path" ]; then - continue - fi - - log_info "Processing certificate for domain: $domain" - - local letsencrypt_cert="/etc/letsencrypt/live/${domain}/fullchain.pem" - local letsencrypt_key="/etc/letsencrypt/live/${domain}/privkey.pem" - - # Check if Let's Encrypt certificate files exist - if [ ! -f "$letsencrypt_cert" ]; then - log_warning "Certificate not found for $domain at $letsencrypt_cert" - skipped_count=$((skipped_count + 1)) - continue - fi - - if [ ! -f "$letsencrypt_key" ]; then - log_warning "Private key not found for $domain at $letsencrypt_key" - skipped_count=$((skipped_count + 1)) - continue - fi - - # Get modification times to check if update is needed - local needs_update=false - if [ ! -f "$cert_path" ]; then - log_info "Combined certificate does not exist for $domain, creating it" - needs_update=true - else - # Check if source files are newer than the combined file - if [ "$letsencrypt_cert" -nt "$cert_path" ] || [ "$letsencrypt_key" -nt "$cert_path" ]; then - log_info "Let's Encrypt certificate is newer than combined file for $domain" - needs_update=true - else - log_info "Certificate for $domain is already up to date" - fi - fi - - # Combine certificate and key into single file for HAProxy - if [ "$needs_update" = true ]; then - if cat "$letsencrypt_cert" "$letsencrypt_key" > "$cert_path"; then - log_info "Updated combined certificate for $domain at $cert_path" - updated_count=$((updated_count + 1)) - else - log_error "Failed to combine certificate files for $domain" - error_count=$((error_count + 1)) - fi - fi - done <<< "$domains" - - log_info "Certificate sync completed: $updated_count updated, $skipped_count skipped, $error_count errors" - - if [ $error_count -gt 0 ]; then - return 1 - fi - - # Return success if we updated any certificates - if [ $updated_count -gt 0 ]; then - return 0 - fi - - # Return special code (2) if nothing needed updating - return 2 -} - -# Main sync process log_info "Starting certificate sync process" -if sync_all_certificates; then - SYNC_RESULT=$? +# Check if database exists +if [ ! -f "$DB_FILE" ]; then + log_error "Database file not found at $DB_FILE" + exit 1 +fi - if [ $SYNC_RESULT -eq 0 ]; then - log_info "Certificates were updated, reloading HAProxy" - if reload_haproxy; then - log_info "Certificate sync and HAProxy reload completed successfully" - exit 0 +# Ensure SSL certs directory exists +mkdir -p "$SSL_CERTS_DIR" + +# Get all SSL-enabled domains from database +DOMAINS=$(sqlite3 "$DB_FILE" "SELECT domain FROM domains WHERE ssl_enabled = 1;" 2>/dev/null) + +if [ -z "$DOMAINS" ]; then + log_info "No SSL-enabled domains found" + exit 0 +fi + +# Copy certificates for each domain +UPDATED=0 +FAILED=0 + +while read -r domain; do + CERT_FILE="/etc/letsencrypt/live/${domain}/fullchain.pem" + KEY_FILE="/etc/letsencrypt/live/${domain}/privkey.pem" + COMBINED_FILE="${SSL_CERTS_DIR}/${domain}.pem" + + if [ -f "$CERT_FILE" ] && [ -f "$KEY_FILE" ]; then + # Combine cert and key into single file for HAProxy + if cat "$CERT_FILE" "$KEY_FILE" > "$COMBINED_FILE"; then + log_info "Updated certificate for $domain" + UPDATED=$((UPDATED + 1)) else - log_error "Certificate sync succeeded but HAProxy reload failed" - exit 1 + log_error "Failed to combine certificate for $domain" + FAILED=$((FAILED + 1)) fi - elif [ $SYNC_RESULT -eq 2 ]; then - log_info "All certificates are already up to date, no reload needed" - exit 0 - fi -else - SYNC_RESULT=$? - - if [ $SYNC_RESULT -eq 2 ]; then - log_info "All certificates are already up to date, no reload needed" - exit 0 else - log_error "Certificate sync failed" + log_error "Certificate files not found for $domain" + FAILED=$((FAILED + 1)) + fi +done <<< "$DOMAINS" + +log_info "Certificate sync completed: $UPDATED updated, $FAILED failed" + +# Reload HAProxy if any certificates were updated +if [ $UPDATED -gt 0 ]; then + if echo "reload" | socat stdio /tmp/haproxy-cli 2>/dev/null; then + log_info "HAProxy reloaded successfully" + else + log_error "Failed to reload HAProxy" exit 1 fi fi