#!/bin/bash set -euo pipefail # Color codes for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Constants CERTBOT_HOME="/opt/certbot" CREDS_BASE_DIR="/root/certbot-cloudflare" CERTBOT_BIN="${CERTBOT_HOME}/bin/certbot" RENEWAL_SCRIPT="/usr/local/bin/certbot-renewal.sh" RENEWAL_LOG="/var/log/certbot-renewal.log" # Helper functions log_info() { echo -e "${GREEN}[INFO]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2 } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } log_debug() { echo -e "${BLUE}[DEBUG]${NC} $1" } validate_input() { local input="$1" local field_name="$2" if [[ -z "$input" ]]; then log_error "$field_name cannot be empty" return 1 fi return 0 } validate_email() { local email="$1" # Basic email validation if [[ ! "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then log_error "Invalid email format: $email" return 1 fi return 0 } validate_domain() { local domain="$1" # Basic domain validation if [[ ! "$domain" =~ ^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$ ]]; then log_error "Invalid domain format: $domain" return 1 fi return 0 } certbot_installed() { [[ -f "$CERTBOT_BIN" ]] && [[ -d "$CERTBOT_HOME" ]] } check_certbot_venv() { if ! certbot_installed; then return 1 fi # Verify the venv is functional if ! "$CERTBOT_BIN" --version &>/dev/null; then log_warn "Certbot venv found but appears broken, will reinstall" return 1 fi return 0 } install_certbot() { if check_certbot_venv; then log_info "Certbot is already installed and configured" return 0 fi log_info "Installing certbot for the first time..." log_info "Updating package manager..." apt-get update || { log_error "Failed to update package list"; return 1; } log_info "Installing dependencies..." apt-get install -y --no-install-recommends \ python3 \ python3-dev \ python3-venv \ libaugeas-dev \ gcc \ || { log_error "Failed to install dependencies"; return 1; } log_info "Setting up Python virtual environment..." python3 -m venv "$CERTBOT_HOME" || { log_error "Failed to create virtual environment"; return 1; } log_info "Installing certbot packages..." "${CERTBOT_HOME}/bin/pip" install --upgrade pip setuptools wheel || { log_error "Failed to upgrade pip"; return 1; } "${CERTBOT_HOME}/bin/pip" install certbot certbot-dns-cloudflare || { log_error "Failed to install certbot"; return 1; } log_info "Creating symlink..." ln -sf "$CERTBOT_BIN" /usr/local/bin/certbot || { log_error "Failed to create symlink"; return 1; } log_info "Certbot installation completed" return 0 } get_credentials_file() { local domain="$1" # Sanitize domain name for filename (replace dots with underscores) local safe_domain="${domain//./_}" echo "${CREDS_BASE_DIR}/cloudflare_${safe_domain}.ini" } certificate_exists() { local domain="$1" [[ -d "/etc/letsencrypt/live/$domain" ]] } create_credentials_file() { local domain="$1" local api_token="$2" local creds_file creds_file=$(get_credentials_file "$domain") # Check if credentials file already exists if [[ -f "$creds_file" ]]; then read -p "Credentials file for $domain already exists. Overwrite? (y/N): " -n 1 -r CONFIRM echo if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then log_info "Keeping existing credentials file for $domain" return 0 fi fi log_info "Creating Cloudflare credentials directory..." mkdir -p "$CREDS_BASE_DIR" || { log_error "Failed to create credentials directory"; return 1; } log_info "Writing Cloudflare credentials for $domain..." cat > "$creds_file" < "$RENEWAL_SCRIPT" <<'RENEWAL_EOF' #!/bin/bash # Let's Encrypt Certificate Renewal Script # Automatically renews all certificates managed by certbot CERTBOT_BIN="/opt/certbot/bin/certbot" LOG_FILE="/var/log/certbot-renewal.log" TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') # Log function log_message() { echo "[$TIMESTAMP] $1" >> "$LOG_FILE" } # Ensure log file exists and is writable mkdir -p "$(dirname "$LOG_FILE")" touch "$LOG_FILE" chmod 644 "$LOG_FILE" log_message "=== Certificate Renewal Started ===" # Run certbot renew if "$CERTBOT_BIN" renew --quiet --non-interactive --agree-tos 2>>"$LOG_FILE"; then log_message "✓ Certificate renewal completed successfully" exit 0 else log_message "✗ Certificate renewal failed with exit code $?" exit 1 fi RENEWAL_EOF chmod 755 "$RENEWAL_SCRIPT" || { log_error "Failed to set script permissions"; return 1; } log_debug "Renewal script created: $RENEWAL_SCRIPT" return 0 } renewal_script_exists() { [[ -f "$RENEWAL_SCRIPT" ]] } cronjob_exists() { crontab -l 2>/dev/null | grep -q "$RENEWAL_SCRIPT" || return 1 } setup_cronjob() { if renewal_script_exists && cronjob_exists; then log_info "Cronjob is already configured" return 0 fi # Create renewal script if it doesn't exist if ! renewal_script_exists; then if ! create_renewal_script; then log_error "Failed to create renewal script" return 1 fi fi # Add cronjob if it doesn't exist if cronjob_exists; then log_info "Cronjob already configured in crontab" return 0 fi log_info "Adding renewal cronjob..." # Get current crontab (if exists) local crontab_content crontab_content=$(crontab -l 2>/dev/null || echo "") # Add new cron entry for every 2 months (runs on the 1st at 2 AM) # This will run on the 1st of every other month crontab_content+=$'\n'"# Let's Encrypt Certificate Renewal (every 2 months)"$'\n' crontab_content+=$'\n'"0 2 1 */2 * $RENEWAL_SCRIPT >> /var/log/certbot-renewal.log 2>&1"$'\n' # Install new crontab if echo "$crontab_content" | crontab -; then log_info "✓ Cronjob configured successfully" log_debug "Cronjob will run on the 1st of every 2 months at 2 AM" return 0 else log_error "Failed to install cronjob" return 1 fi } show_renewal_status() { echo echo "Renewal Configuration:" echo " Renewal script: $RENEWAL_SCRIPT" echo " Log file: $RENEWAL_LOG" echo " Cronjob schedule: 1st of every 2 months at 2 AM" echo echo "To manually renew all certificates:" echo " $RENEWAL_SCRIPT" echo echo "To view renewal logs:" echo " tail -f $RENEWAL_LOG" echo } show_manual_renewal_info() { echo echo "Manual Renewal Information:" echo " To manually renew certificates, run:" echo " /opt/certbot/bin/certbot renew" echo echo " Or set up a cronjob manually in the future by running this script again" echo } cleanup_on_exit() { local exit_code=$? if [[ $exit_code -ne 0 ]]; then log_error "Script failed with exit code $exit_code" fi return $exit_code } trap cleanup_on_exit EXIT # Main script main() { # Ensure script is run as root if [[ "$EUID" -ne 0 ]]; then log_error "This script must be run as root (use 'sudo')" exit 1 fi log_info "=== Let's Encrypt + Cloudflare Certificate Manager ===" # Install certbot if needed if ! install_certbot; then log_error "Failed to install certbot" exit 1 fi # Prompt for inputs with validation while true; do read -p "Enter the FQDN (e.g., example.com or sub.example.com): " DOMAIN validate_input "$DOMAIN" "Domain" && validate_domain "$DOMAIN" && break done # Check if certificate already exists if certificate_exists "$DOMAIN"; then log_warn "Certificate already exists for $DOMAIN" read -p "Do you want to renew/update it? (y/N): " -n 1 -r CONFIRM echo if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then log_info "Skipping certificate setup for $DOMAIN" show_certificate_info "$DOMAIN" return 0 fi fi while true; do read -p "Enter email address for Let's Encrypt notifications: " EMAIL validate_input "$EMAIL" "Email" && validate_email "$EMAIL" && break done while true; do read -sp "Enter your Cloudflare API Token: " CF_API_KEY echo validate_input "$CF_API_KEY" "API token" && break done # Confirm inputs echo log_info "Configuration summary:" echo " Domain: $DOMAIN" echo " Email: $EMAIL" echo " Credentials file: $(get_credentials_file "$DOMAIN")" read -p "Continue? (y/N): " -n 1 -r CONFIRM echo if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then log_warn "Setup cancelled by user" exit 0 fi # Create credentials file if ! create_credentials_file "$DOMAIN" "$CF_API_KEY"; then log_error "Failed to create credentials file" exit 1 fi # Request certificate if ! request_certificate "$DOMAIN" "$EMAIL"; then log_error "Failed to request certificate" exit 1 fi show_certificate_info "$DOMAIN" # Ask about renewal setup echo echo "Certificate renewal options:" echo " 1) Setup automatic renewal with cronjob (every 2 months)" echo " 2) Manual renewal only (you'll run renewal commands yourself)" read -p "Choose option (1 or 2): " -n 1 -r RENEWAL_CHOICE echo case "$RENEWAL_CHOICE" in 1) if ! setup_cronjob; then log_error "Failed to setup cronjob, but certificate was created successfully" show_certificate_info "$DOMAIN" show_manual_renewal_info exit 1 fi show_renewal_status ;; 2) log_info "Skipping automatic renewal setup" show_manual_renewal_info ;; *) log_warn "Invalid option. Skipping renewal setup" show_manual_renewal_info ;; esac } show_certificate_info() { local domain="$1" log_info "✓ Certificate setup completed for $domain" echo echo "Certificate Details:" echo " Location: /etc/letsencrypt/live/$domain/" echo " Full chain: /etc/letsencrypt/live/$domain/fullchain.pem" echo " Private key: /etc/letsencrypt/live/$domain/privkey.pem" echo " Credentials: $(get_credentials_file "$domain")" echo } main "$@"