From 76b2e85ca80bd0e2ef37a5b900a8b92ebeac0ae2 Mon Sep 17 00:00:00 2001 From: jknapp Date: Tue, 28 Oct 2025 17:36:48 -0700 Subject: [PATCH] Fix certificate renewal cron job and add host-side scheduling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed crontab permissions (600) and ownership for proper cron execution - Added PATH environment variable to crontab to prevent command not found issues - Created dedicated renewal script with comprehensive logging and error handling - Added retry logic (3 attempts) for HAProxy reload with socket health checks - Implemented host-side renewal script for external cron scheduling via docker exec - Added crontab configuration examples for various renewal schedules - Updated README with detailed certificate renewal documentation This resolves issues where the cron job would not run or hang during execution. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Dockerfile | 7 +- README.md | 49 +++++++++++++ scripts/host-crontab-example.txt | 26 +++++++ scripts/host-renew-certificates.sh | 39 +++++++++++ scripts/renew-certificates.sh | 109 +++++++++++++++++++++++++++++ 5 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 scripts/host-crontab-example.txt create mode 100755 scripts/host-renew-certificates.sh create mode 100644 scripts/renew-certificates.sh diff --git a/Dockerfile b/Dockerfile index 8b50476..14a8d2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,10 +7,15 @@ COPY haproxy_manager.py /haproxy/ COPY scripts /haproxy/scripts RUN chmod +x /haproxy/scripts/* RUN pip install -r requirements.txt -RUN echo "0 */12 * * * root test -x /usr/bin/certbot && /usr/bin/certbot -q renew && echo \"reload\" | socat stdio /tmp/haproxy-cli" > /var/spool/cron/crontabs/root # Create log directories RUN mkdir -p /var/log && touch /var/log/haproxy-manager.log /var/log/haproxy-manager-errors.log RUN chmod 755 /var/log/haproxy-manager.log /var/log/haproxy-manager-errors.log +# Set up cron for certificate renewal with proper permissions and environment +RUN mkdir -p /var/spool/cron/crontabs && \ + echo 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' > /var/spool/cron/crontabs/root && \ + echo '0 */12 * * * /haproxy/scripts/renew-certificates.sh >> /var/log/haproxy-manager.log 2>&1' >> /var/spool/cron/crontabs/root && \ + chmod 600 /var/spool/cron/crontabs/root && \ + chown root:crontab /var/spool/cron/crontabs/root EXPOSE 80 443 8000 # Add health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ diff --git a/README.md b/README.md index a4bb036..5ee20ee 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,54 @@ GET /api/certificates/example.com/cert Authorization: Bearer your-api-key ``` +## Certificate Renewal + +The HAProxy Manager includes automatic certificate renewal with multiple scheduling options: + +### Automatic Renewal (Container-based) +By default, a cron job runs inside the container every 12 hours to check and renew certificates: +- Runs at minute 0 of every 12th hour (12:00 AM, 12:00 PM) +- Automatically reloads HAProxy if certificates are renewed +- Logs all renewal attempts to `/var/log/haproxy-manager.log` +- Errors logged to `/var/log/haproxy-manager-errors.log` + +### Manual Renewal via API +Trigger certificate renewal manually using the API: +```bash +curl -X POST http://localhost:8000/api/certificates/renew \ + -H "Authorization: Bearer your-api-key" +``` + +### Host-side Renewal (Recommended for Production) +For more control over scheduling, run renewals from the host machine using the provided script: + +```bash +# Make the script executable +chmod +x scripts/host-renew-certificates.sh + +# Add to host crontab (edit with: crontab -e) +0 */12 * * * /path/to/haproxy-manager-base/scripts/host-renew-certificates.sh + +# Or run manually +./scripts/host-renew-certificates.sh +``` + +The host-side script: +- Executes the renewal process inside the running container +- Maintains separate host-side logs at `/var/log/haproxy-manager-host-renewal.log` +- Automatically detects if the container is running +- Supports custom container names via `CONTAINER_NAME` environment variable + +See [scripts/host-crontab-example.txt](scripts/host-crontab-example.txt) for more crontab configuration examples. + +### Renewal Script Features +The renewal script ([scripts/renew-certificates.sh](scripts/renew-certificates.sh)) includes: +- Comprehensive logging with timestamps +- Retry logic for HAProxy reload (3 attempts with 5-second delays) +- HAProxy socket health checks before reload +- Proper error handling and exit codes +- Detection of whether certificates actually needed renewal + ## Logging and Monitoring The HAProxy Manager includes comprehensive logging and error tracking: @@ -306,6 +354,7 @@ The HAProxy Manager includes comprehensive logging and error tracking: ### Log Files - `/var/log/haproxy-manager.log` - General application logs - `/var/log/haproxy-manager-errors.log` - Error logs for alerting +- `/var/log/haproxy-manager-host-renewal.log` - Host-side renewal logs (when using host script) ### Logged Operations All API operations are logged with timestamps and success/failure status: diff --git a/scripts/host-crontab-example.txt b/scripts/host-crontab-example.txt new file mode 100644 index 0000000..6591c65 --- /dev/null +++ b/scripts/host-crontab-example.txt @@ -0,0 +1,26 @@ +# HAProxy Manager - Host-side Crontab Example +# Add this to your host machine's crontab to schedule certificate renewals +# +# Edit your crontab with: crontab -e +# View your crontab with: crontab -l +# +# The script will run inside the container and handle all logging internally. +# Host-side logs will be written to /var/log/haproxy-manager-host-renewal.log + +# Run certificate renewal every 12 hours at the top of the hour +0 */12 * * * /path/to/haproxy-manager-base/scripts/host-renew-certificates.sh + +# Alternative: Run at specific times (e.g., 2 AM and 2 PM daily) +# 0 2,14 * * * /path/to/haproxy-manager-base/scripts/host-renew-certificates.sh + +# Alternative: Run once daily at 3 AM +# 0 3 * * * /path/to/haproxy-manager-base/scripts/host-renew-certificates.sh + +# Custom container name example (if your container has a different name): +# 0 */12 * * * CONTAINER_NAME=my-haproxy /path/to/haproxy-manager-base/scripts/host-renew-certificates.sh + +# Custom log file location example: +# 0 */12 * * * LOG_FILE=/custom/path/renewal.log /path/to/haproxy-manager-base/scripts/host-renew-certificates.sh + +# With both custom settings: +# 0 */12 * * * CONTAINER_NAME=my-haproxy LOG_FILE=/custom/path/renewal.log /path/to/haproxy-manager-base/scripts/host-renew-certificates.sh diff --git a/scripts/host-renew-certificates.sh b/scripts/host-renew-certificates.sh new file mode 100755 index 0000000..97e1d45 --- /dev/null +++ b/scripts/host-renew-certificates.sh @@ -0,0 +1,39 @@ +#!/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 + +set -e + +# Configuration - Customize these values +CONTAINER_NAME="${CONTAINER_NAME:-haproxy-manager}" +LOG_FILE="${LOG_FILE:-/var/log/haproxy-manager-host-renewal.log}" + +# Logging functions +log_info() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*" | tee -a "$LOG_FILE" +} + +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" + +# Check if container is running +if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + log_error "Container '${CONTAINER_NAME}' is not running" + exit 1 +fi + +# Execute renewal script inside container +log_info "Executing renewal script in container '${CONTAINER_NAME}'" +if docker exec "$CONTAINER_NAME" /haproxy/scripts/renew-certificates.sh; then + log_info "Certificate renewal completed successfully" + exit 0 +else + log_error "Certificate renewal failed" + exit 1 +fi diff --git a/scripts/renew-certificates.sh b/scripts/renew-certificates.sh new file mode 100644 index 0000000..d1dfa9f --- /dev/null +++ b/scripts/renew-certificates.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash + +# Certificate Renewal Script for HAProxy Manager +# This script handles Let's Encrypt certificate renewal with proper logging and error handling + +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}" +MAX_RETRIES=3 +RETRY_DELAY=5 + +# Logging functions +log_info() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*" | tee -a "$LOG_FILE" +} + +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 +} + +# 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 [ $RENEWAL_EXIT_CODE -eq 0 ]; then + log_info "Certificate renewal completed successfully" + + # Check if any certificates were actually renewed + if grep -q "Cert not yet due for renewal" "$LOG_FILE" 2>/dev/null; then + log_info "No certificates needed renewal at this time" + else + log_info "Certificates were renewed, 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 + fi + else + log_error "Certificate renewal failed with exit code $RENEWAL_EXIT_CODE" + exit $RENEWAL_EXIT_CODE + fi +else + log_error "Certificate renewal command failed" + exit 1 +fi + +log_info "Certificate renewal process completed" +exit 0