- Add idempotent certbot installation (skip if already installed)
- Support multiple domains with separate credential files per domain - Implement optional automatic renewal via cronjob (every 2 months) - Add interactive prompt to choose between automatic or manual renewal - Create renewal script at /usr/local/bin/certbot-renewal.sh - Add comprehensive logging to /var/log/certbot-renewal.log - Add input validation for domain, email, and API token formats - Improve error handling with explicit checks on all operations - Add helper functions for better code organization and reusability - Allow script to be run multiple times for different domains - Display certificate paths and renewal configuration after setup - Check for existing certificates and prompt before overwriting - Preserve existing crontab entries when adding renewal job
This commit is contained in:
+409
-51
@@ -1,70 +1,428 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
set -euo pipefail
|
||||
|
||||
# Ensure script is run as root
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
echo "Please run as root"
|
||||
exit 1
|
||||
fi
|
||||
# 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
|
||||
|
||||
echo "=== Let's Encrypt + Cloudflare setup ==="
|
||||
# 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"
|
||||
|
||||
# Ask for domain
|
||||
read -p "Enter the FQDN (e.g. example.com or sub.example.com): " DOMAIN
|
||||
# Helper functions
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
if [[ -z "$DOMAIN" ]]; then
|
||||
echo "Domain cannot be empty"
|
||||
exit 1
|
||||
fi
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1" >&2
|
||||
}
|
||||
|
||||
# Ask for email (NEW)
|
||||
read -p "Enter email address for Let's Encrypt notifications: " EMAIL
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
if [[ -z "$EMAIL" ]]; then
|
||||
echo "Email cannot be empty"
|
||||
exit 1
|
||||
fi
|
||||
log_debug() {
|
||||
echo -e "${BLUE}[DEBUG]${NC} $1"
|
||||
}
|
||||
|
||||
# Ask for Cloudflare API token/key
|
||||
read -p "Enter your Cloudflare API Token/Key: " CF_API_KEY
|
||||
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
|
||||
}
|
||||
|
||||
if [[ -z "$CF_API_KEY" ]]; then
|
||||
echo "API key cannot be empty"
|
||||
exit 1
|
||||
fi
|
||||
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
|
||||
}
|
||||
|
||||
echo "Installing dependencies..."
|
||||
apt update
|
||||
apt install -y python3 python3-dev python3-venv libaugeas-dev gcc
|
||||
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
|
||||
}
|
||||
|
||||
echo "Setting up virtual environment..."
|
||||
python3 -m venv /opt/certbot/
|
||||
/opt/certbot/bin/pip install --upgrade pip
|
||||
/opt/certbot/bin/pip install certbot
|
||||
/opt/certbot/bin/pip install certbot-dns-cloudflare
|
||||
certbot_installed() {
|
||||
[[ -f "$CERTBOT_BIN" ]] && [[ -d "$CERTBOT_HOME" ]]
|
||||
}
|
||||
|
||||
# Symlink certbot
|
||||
ln -sf /opt/certbot/bin/certbot /usr/bin/certbot
|
||||
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
|
||||
}
|
||||
|
||||
echo "Creating Cloudflare credentials file..."
|
||||
mkdir -p /root/certbot-cloudflare/
|
||||
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
|
||||
}
|
||||
|
||||
cat <<EOF > /root/certbot-cloudflare/cloudflare.ini
|
||||
dns_cloudflare_api_token = $CF_API_KEY
|
||||
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" <<EOF || { log_error "Failed to write credentials file"; return 1; }
|
||||
dns_cloudflare_api_token = $api_token
|
||||
EOF
|
||||
|
||||
log_info "Securing credentials file..."
|
||||
chmod 600 "$creds_file" || { log_error "Failed to set file permissions"; return 1; }
|
||||
|
||||
log_debug "Credentials stored in: $creds_file"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Secure the credentials file
|
||||
chmod 600 /root/certbot-cloudflare/cloudflare.ini
|
||||
request_certificate() {
|
||||
local domain="$1"
|
||||
local email="$2"
|
||||
local creds_file
|
||||
|
||||
creds_file=$(get_credentials_file "$domain")
|
||||
|
||||
if [[ ! -f "$creds_file" ]]; then
|
||||
log_error "Credentials file not found: $creds_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Requesting certificate for $domain..."
|
||||
|
||||
"$CERTBOT_BIN" certonly \
|
||||
--non-interactive \
|
||||
--agree-tos \
|
||||
--no-eff-email \
|
||||
--email "$email" \
|
||||
--dns-cloudflare \
|
||||
--dns-cloudflare-credentials "$creds_file" \
|
||||
-d "$domain" \
|
||||
|| { log_error "Failed to obtain certificate for $domain"; return 1; }
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
echo "Requesting certificate for $DOMAIN ..."
|
||||
certbot certonly \
|
||||
--non-interactive \
|
||||
--agree-tos \
|
||||
--no-eff-email \
|
||||
--email "$EMAIL" \
|
||||
--dns-cloudflare \
|
||||
--dns-cloudflare-credentials /root/certbot-cloudflare/cloudflare.ini \
|
||||
-d "$DOMAIN"
|
||||
create_renewal_script() {
|
||||
log_info "Creating certificate renewal script..."
|
||||
|
||||
cat > "$RENEWAL_SCRIPT" <<'RENEWAL_EOF'
|
||||
#!/bin/bash
|
||||
|
||||
echo "Done! Certificate setup completed for $DOMAIN"
|
||||
# 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 "$@"
|
||||
|
||||
Reference in New Issue
Block a user