Files

429 lines
11 KiB
Bash
Executable File

#!/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" <<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
}
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
}
create_renewal_script() {
log_info "Creating certificate renewal script..."
cat > "$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 "$@"