429 lines
11 KiB
Bash
Executable File
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 "$@"
|