#!/bin/bash # FileWave Let's Encrypt installer (Debian 12/13) # Supports: # - HTTP-01 validation (standalone, requires inbound TCP/80) # - DNS-01 validation via Cloudflare plugin (no TCP/80 requirement) # # UX-focused Debian script with modular challenge handlers. set -euo pipefail LOG_FILE="/var/log/filewave-letsencrypt.log" RENEW_HOOK="/etc/letsencrypt/renewal-hooks/deploy/filewave-server-cert.sh" CRON_FILE="/etc/cron.daily/letsencrypt-filewave" CF_SECRETS_DIR="/etc/letsencrypt/secrets" CF_CREDENTIALS_FILE="${CF_SECRETS_DIR}/cloudflare.ini" METHOD="" HOSTNAME="" EMAIL="" CLOUDFLARE_TOKEN="" OS_VERSION="unknown" STEP_INDEX=0 TOTAL_STEPS=10 # ---------- Output formatting (no dependencies) ---------- if [ -t 1 ] && [ "${NO_COLOR:-}" = "" ]; then C_RESET='\033[0m' C_BOLD='\033[1m' C_DIM='\033[2m' C_BLUE='\033[34m' C_GREEN='\033[32m' C_YELLOW='\033[33m' C_RED='\033[31m' else C_RESET='' C_BOLD='' C_DIM='' C_BLUE='' C_GREEN='' C_YELLOW='' C_RED='' fi banner() { echo echo -e "${C_BOLD}==============================================================${C_RESET}" echo -e "${C_BOLD} FileWave Let's Encrypt Installer (Debian 12/13)${C_RESET}" echo -e "${C_BOLD}==============================================================${C_RESET}" echo } log() { # Always log plain text (no ANSI codes) to file. echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" } info() { echo -e "${C_BLUE}[INFO]${C_RESET} $*"; } ok() { echo -e "${C_GREEN}[OK]${C_RESET} $*"; } warn() { echo -e "${C_YELLOW}[WARN]${C_RESET} $*"; } err() { echo -e "${C_RED}[ERR]${C_RESET} $*"; } step() { STEP_INDEX=$((STEP_INDEX + 1)) echo echo -e "${C_BOLD}[${STEP_INDEX}/${TOTAL_STEPS}]${C_RESET} $*" } die() { err "$*" log "ERROR: $*" exit 1 } usage() { cat </dev/null 2>&1; then info "Installing dnsutils..." apt-get update -y apt-get install -y dnsutils ok "dnsutils installed" fi } check_public_dns_resolution() { local hostname="$1" ensure_dnsutils if nslookup "$hostname" 8.8.8.8 >/dev/null 2>&1; then ok "Public DNS resolution check passed for $hostname via 8.8.8.8" return 0 fi warn "$hostname did not resolve via 8.8.8.8; trying system resolver" nslookup "$hostname" >/dev/null 2>&1 || die "$hostname did not resolve via 8.8.8.8 or system resolver" ok "DNS resolution check passed for $hostname via system resolver" } update_system_packages() { info "Updating package metadata..." apt-get update -y info "Upgrading packages (noninteractive)..." DEBIAN_FRONTEND=noninteractive apt-get upgrade -y -o Dpkg::Options::="--force-confold" ok "System package update/upgrade complete" } ensure_certbot_base() { if command -v certbot >/dev/null 2>&1; then ok "certbot already present" return 0 fi info "Installing snapd and certbot..." apt-get install -y snapd snap install core snap refresh core || true apt-get remove -y certbot python3-certbot-apache 2>/dev/null || true snap install --classic certbot ln -sf /snap/bin/certbot /usr/bin/certbot command -v certbot >/dev/null 2>&1 || die "Certbot installation failed" ok "certbot installed" } ensure_cloudflare_plugin() { info "Ensuring certbot-dns-cloudflare plugin is installed (snap stack)..." if ! snap install certbot-dns-cloudflare 2>/dev/null; then snap refresh certbot-dns-cloudflare >/dev/null 2>&1 || true fi certbot plugins 2>/dev/null | grep -qi 'dns-cloudflare' || { die "Cloudflare DNS plugin not detected by certbot (snap). Keep certbot/plugin on the same packaging stack." } ok "Cloudflare DNS plugin available" } # ---------- Cert + FileWave tasks ---------- setup_cloudflare_credentials() { mkdir -p "$CF_SECRETS_DIR" chmod 700 "$CF_SECRETS_DIR" cat > "$CF_CREDENTIALS_FILE" </dev/null; then ok "Existing certificates backed up to $backup_dir" else warn "Existing certificates not found for backup (continuing)" fi } request_certificate_http() { info "Requesting certificate via HTTP-01 (standalone)..." certbot -n --agree-tos --standalone certonly -d "$HOSTNAME" -m "$EMAIL" || { echo err "Certificate request failed (HTTP-01)." echo "Likely causes:" echo " - TCP/80 not reachable from internet" echo " - DNS record not pointing to this server" echo echo "Manual retry:" echo "sudo certbot -n --agree-tos --standalone certonly -d \"$HOSTNAME\" -m \"$EMAIL\"" die "Stopping after HTTP-01 failure" } ok "Certificate issued (HTTP-01)" } request_certificate_dns_cloudflare() { verify_cloudflare_credentials_exists info "Requesting certificate via DNS-01 (Cloudflare)..." certbot -n --agree-tos --dns-cloudflare --dns-cloudflare-credentials "$CF_CREDENTIALS_FILE" certonly -d "$HOSTNAME" -m "$EMAIL" || { echo err "Certificate request failed (DNS-01 Cloudflare)." echo "Likely causes:" echo " - invalid Cloudflare token" echo " - token lacks DNS edit permission on this zone" echo " - FQDN not in a zone accessible by that token" echo echo "Manual retry:" echo "sudo certbot -n --agree-tos --dns-cloudflare --dns-cloudflare-credentials $CF_CREDENTIALS_FILE certonly -d \"$HOSTNAME\" -m \"$EMAIL\"" die "Stopping after DNS-01 failure" } ok "Certificate issued (DNS-01 Cloudflare)" } set_mdm_cert_trusted() { info "Updating iOS preferences in PostgreSQL (mdm_cert_trusted=true)..." if /usr/local/filewave/postgresql/bin/psql -d mdm -U django -c \ "INSERT INTO ios_preferences (key, value) VALUES ('mdm_cert_trusted', TRUE) ON CONFLICT (key) DO NOTHING; UPDATE ios_preferences SET value='true' WHERE key='mdm_cert_trusted';" \ 2>/dev/null; then ok "iOS preferences updated" else warn "Failed to update iOS preferences (continuing)" fi } create_renew_hook() { mkdir -p "$(dirname "$RENEW_HOOK")" rm -f "$RENEW_HOOK" cat > "$RENEW_HOOK" </dev/null || echo root) CERT_GROUP=\$(stat -c '%G' /usr/local/filewave/certs/server.crt 2>/dev/null || echo root) else CERT_OWNER=\$(stat -c '%U' /usr/local/filewave/certs 2>/dev/null || echo root) CERT_GROUP=\$(stat -c '%G' /usr/local/filewave/certs 2>/dev/null || echo root) fi cp /etc/letsencrypt/live/"\$HOSTNAME"/fullchain.pem /usr/local/filewave/certs/server.crt cp /etc/letsencrypt/live/"\$HOSTNAME"/privkey.pem /usr/local/filewave/certs/server.key chown "\$CERT_OWNER":"\$CERT_GROUP" /usr/local/filewave/certs/server.* /usr/local/bin/fwcontrol server restart (yes 2>/dev/null) | /usr/local/filewave/python/bin/python /usr/local/filewave/django/manage.pyc update_dep_profile_certs 2>/dev/null || true HOOK chmod +x "$RENEW_HOOK" bash -n "$RENEW_HOOK" || die "Renewal hook syntax check failed" ok "Renewal hook created: $RENEW_HOOK" } inject_certificate_now() { info "Injecting certificate into FileWave now..." "$RENEW_HOOK" || die "Initial certificate injection failed" ok "Certificate injected into FileWave" } create_renew_cron() { cat > "$CRON_FILE" <<'CRON' #!/bin/bash sleep $((RANDOM % 7200)) /usr/bin/certbot renew --quiet CRON chmod +x "$CRON_FILE" ok "Renewal cron created: $CRON_FILE" } uninstall_files() { step "Removing FileWave Let's Encrypt integration files" log "Starting uninstall flow" rm -f "$RENEW_HOOK" rm -f "$CRON_FILE" if [ -f "$CF_CREDENTIALS_FILE" ]; then if command -v shred >/dev/null 2>&1; then shred -vfz -n 3 "$CF_CREDENTIALS_FILE" || rm -f "$CF_CREDENTIALS_FILE" else rm -f "$CF_CREDENTIALS_FILE" fi ok "Removed Cloudflare credential file" else info "No Cloudflare credential file found (nothing to remove)" fi ok "Uninstall complete" echo echo "certbot package was intentionally left installed." } # ---------- Prompting ---------- prompt_hostname() { while true; do read -r -p "Enter fully qualified domain name (FQDN): " HOSTNAME if validate_hostname "$HOSTNAME"; then return 0 fi warn "Invalid hostname format. Example: fw.example.com" done } prompt_email() { while true; do read -r -p "Enter email address for Let's Encrypt notifications: " EMAIL if validate_email "$EMAIL"; then return 0 fi warn "Invalid email format. Example: admin@example.com" done } prompt_method() { while true; do echo echo "Select Let's Encrypt validation method:" echo " 1) HTTP-01 (standalone certbot)" echo " - Requires inbound TCP 80 from internet" echo " - Simpler setup, no DNS API token" echo echo " 2) DNS-01 (Cloudflare)" echo " - Does NOT require inbound TCP 80" echo " - Requires Cloudflare API token with Zone.DNS edit rights" echo read -r -p "Choice [1/2]: " method_choice case "$method_choice" in 1) METHOD="http"; return 0 ;; 2) METHOD="dns-cloudflare"; return 0 ;; *) warn "Invalid choice. Enter 1 or 2." ;; esac done } prompt_cloudflare_token_if_needed() { if [ "$METHOD" = "dns-cloudflare" ]; then while true; do read -r -s -p "Enter Cloudflare API token: " CLOUDFLARE_TOKEN echo if [ -n "$CLOUDFLARE_TOKEN" ]; then return 0 fi warn "Cloudflare token cannot be empty." done fi } collect_install_inputs() { step "Collecting install inputs" prompt_hostname prompt_email prompt_method prompt_cloudflare_token_if_needed echo echo -e "${C_BOLD}Review before continuing:${C_RESET}" echo " Hostname: $HOSTNAME" echo " Email: $EMAIL" if [ "$METHOD" = "http" ]; then echo " Method: HTTP-01 (standalone)" echo " Note: TCP 80 must be publicly reachable" else echo " Method: DNS-01 (Cloudflare)" echo " Note: Cloudflare token will be stored at $CF_CREDENTIALS_FILE (mode 600)" fi if ! prompt_yes_no "Proceed with installation"; then die "Aborted by user" fi } print_install_summary() { echo echo "--------------------------------------------------------------" echo -e "${C_BOLD}Install completed successfully${C_RESET}" echo "--------------------------------------------------------------" echo "Files created/updated:" echo "- $RENEW_HOOK" echo "- $CRON_FILE" if [ "$METHOD" = "dns-cloudflare" ]; then echo "- $CF_CREDENTIALS_FILE" fi echo "- $LOG_FILE" echo if [ "$METHOD" = "http" ]; then echo "Manual retry (HTTP-01):" echo "sudo certbot -n --agree-tos --standalone certonly -d \"$HOSTNAME\" -m \"$EMAIL\"" else echo "Manual retry (DNS-01 Cloudflare):" echo "sudo certbot -n --agree-tos --dns-cloudflare --dns-cloudflare-credentials $CF_CREDENTIALS_FILE certonly -d \"$HOSTNAME\" -m \"$EMAIL\"" fi echo echo "Optional renewal test:" echo "sudo certbot renew --force-renewal" echo } install_flow() { log "Starting install flow" collect_install_inputs step "Validating FileWave server prerequisites" ensure_filewave_server step "Validating DNS resolution" check_public_dns_resolution "$HOSTNAME" step "Backing up existing FileWave certs" backup_existing_certs step "Updating system packages" update_system_packages step "Ensuring certbot is installed" ensure_certbot_base if [ "$METHOD" = "dns-cloudflare" ]; then step "Preparing DNS-01 (Cloudflare) prerequisites" ensure_cloudflare_plugin setup_cloudflare_credentials step "Requesting certificate" request_certificate_dns_cloudflare else step "Requesting certificate" warn "HTTP-01 requires public reachability on TCP 80 during validation" request_certificate_http fi step "Applying certificate to FileWave" set_mdm_cert_trusted create_renew_hook inject_certificate_now step "Configuring auto-renewal" create_renew_cron print_install_summary } main() { local action="${1:-}" if [ "$action" != "--install" ] && [ "$action" != "--uninstall" ]; then usage exit 1 fi require_root banner require_debian case "$action" in --install) install_flow ;; --uninstall) uninstall_files ;; esac } main "$@"