From b8de5968db34667f3e4397bcfbabec46b2d4cb54 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Fri, 5 Dec 2025 20:00:30 +0100 Subject: [PATCH] Add Raspberry Pi 5 Nextcloud deployment scripts - raspberry_pi_flash_sd.sh: Flash RPi OS to SD card (local/remote) - Auto-discovers SD cards on local or remote systems - Configures headless SSH access with auto-generated passwords - Supports flashing via remote laptop with SD card reader - raspberry_pi_nextcloud.sh: Install and configure Nextcloud - Automated installation of Nextcloud with Apache, MariaDB, Redis - DuckDNS + Let's Encrypt for publicly trusted HTTPS - Security hardening: HSTS, cron jobs, default phone region - Auto-generated admin credentials stored in config file - fix command for security warnings - setup-ssl-remote for Let's Encrypt via DuckDNS Includes shellcheck compliance with appropriate directives. --- scripts/features/raspberry_pi_flash_sd.sh | 661 ++++++++++ scripts/features/raspberry_pi_nextcloud.sh | 1275 ++++++++++++++++++++ 2 files changed, 1936 insertions(+) create mode 100755 scripts/features/raspberry_pi_flash_sd.sh create mode 100755 scripts/features/raspberry_pi_nextcloud.sh diff --git a/scripts/features/raspberry_pi_flash_sd.sh b/scripts/features/raspberry_pi_flash_sd.sh new file mode 100755 index 0000000..ac77096 --- /dev/null +++ b/scripts/features/raspberry_pi_flash_sd.sh @@ -0,0 +1,661 @@ +#!/bin/bash +# Raspberry Pi SD Card Flash Script +# This script flashes Raspberry Pi OS to an SD card (locally or on a remote laptop) +# +# Usage: +# ./raspberry_pi_flash_sd.sh - Flash SD card locally +# ./raspberry_pi_flash_sd.sh remote - Flash SD card on remote laptop via SSH + +set -euo pipefail + +# Script directory for config file +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="${SCRIPT_DIR}/.raspberry_pi.conf" + +# Load configuration from gitignored config file if it exists +if [[ -f "$CONFIG_FILE" ]]; then + # shellcheck source=/dev/null + source "$CONFIG_FILE" +fi + +# Configuration - Customize these values (or set in config file) +PI_HOSTNAME="${PI_HOSTNAME:-nextcloud-pi}" +PI_USER="${PI_USER:-pi}" +PI_PASSWORD="${PI_PASSWORD:-}" +PI_TIMEZONE="${PI_TIMEZONE:-Europe/Warsaw}" +SD_CARD_DEVICE="${SD_CARD_DEVICE:-}" + +# Remote laptop configuration - will be auto-discovered if not set +REMOTE_LAPTOP_IP="${REMOTE_LAPTOP_IP:-}" +REMOTE_LAPTOP_USER="${REMOTE_LAPTOP_USER:-kuchy}" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# All log functions output to stderr so they don't interfere with function return values +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" >&2 +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" >&2 +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" >&2 +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" >&2 +} + +die() { + log_error "$1" + exit 1 +} + +check_root() { + if [[ $EUID -ne 0 ]]; then + die "This script must be run as root. Use: sudo $0" + fi +} + +save_config() { + cat >"$CONFIG_FILE" </dev/null | tr -dc 'A-Za-z0-9!@#$%&*' | cut -c1-"$length") + echo "$chars" +} + +auto_generate_pi_password() { + if [[ -z "$PI_PASSWORD" ]]; then + PI_PASSWORD=$(generate_password 16) + log_info "Auto-generated Pi password (will be saved to config file)" + fi +} + +# ============================================================================= +# Network Discovery Functions +# ============================================================================= + +ensure_dependencies() { + local missing_packages=() + + if ! command -v nmap &>/dev/null; then + missing_packages+=("nmap") + fi + + if ! command -v sshpass &>/dev/null; then + missing_packages+=("sshpass") + fi + + if [[ ${#missing_packages[@]} -gt 0 ]]; then + log_info "Installing missing packages: ${missing_packages[*]}" + + if command -v pacman &>/dev/null; then + sudo pacman -S --noconfirm "${missing_packages[@]}" + elif command -v apt-get &>/dev/null; then + sudo apt-get update && sudo apt-get install -y "${missing_packages[@]}" + elif command -v dnf &>/dev/null; then + sudo dnf install -y "${missing_packages[@]}" + else + die "Could not detect package manager. Please install manually: ${missing_packages[*]}" + fi + + log_success "Dependencies installed" + fi +} + +discover_remote_laptop() { + log_info "Auto-discovering remote laptop on local network..." + + ensure_dependencies + + local my_ip + my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) + + local gateway + gateway=$(ip route | grep default | awk '{print $3}' | head -1) + local network="${gateway%.*}.0/24" + + log_info "Local IP: $my_ip, Gateway: $gateway, Network: $network" + log_info "Scanning network for SSH-enabled devices (using nmap)..." + + local ssh_hosts + nmap -sn -T4 "$network" &>/dev/null || true + ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) + + if [[ -z "$ssh_hosts" ]]; then + die "No SSH-enabled devices found on network" + fi + + local host_count + host_count=$(echo "$ssh_hosts" | wc -l) + log_info "Found $host_count SSH-enabled device(s): $(echo "$ssh_hosts" | tr '\n' ' ')" + + local common_users=("$REMOTE_LAPTOP_USER" "kuchy" "kuhy" "$(whoami)" "pi" "user" "admin") + local users=() + for u in "${common_users[@]}"; do + local is_dup=0 + for existing in "${users[@]}"; do + if [[ "$u" == "$existing" ]]; then + is_dup=1 + break + fi + done + if [[ $is_dup -eq 0 ]]; then + users+=("$u") + fi + done + + log_info "Will try usernames: ${users[*]}" + + local found_laptop="" + local found_user="" + local idx=0 + + for ip in $ssh_hosts; do + idx=$((idx + 1)) + + if [[ "$ip" == "$gateway" ]]; then + log_info "[$idx/$host_count] Skipping $ip (gateway)" + continue + fi + + log_info "[$idx/$host_count] $ip - Trying SSH key access with common usernames..." + + for try_user in "${users[@]}"; do + if ssh -o BatchMode=yes -o ConnectTimeout=2 -o StrictHostKeyChecking=accept-new "${try_user}@${ip}" "echo ok" 2>/dev/null | grep -q "ok"; then + log_success "[$idx/$host_count] $ip - SSH key access confirmed with user '$try_user'!" + found_user="$try_user" + + log_info "[$idx/$host_count] $ip - Checking for SD card..." + local has_sd + has_sd=$(ssh -o BatchMode=yes -o ConnectTimeout=2 "${try_user}@${ip}" "lsblk -d -o NAME,RM,TRAN 2>/dev/null | grep -E '1.*(usb|mmc)' | head -1" 2>/dev/null || true) + + if [[ -n "$has_sd" ]]; then + log_success "[$idx/$host_count] $ip - Found SD card: $has_sd" + found_laptop="$ip" + break 2 + else + log_warning "[$idx/$host_count] $ip - No SD card detected, saving as fallback..." + if [[ -z "$found_laptop" ]]; then + found_laptop="$ip" + fi + fi + break + fi + done + done + + if [[ -z "$found_laptop" ]] || [[ -z "$found_user" ]]; then + log_warning "No device with passwordless SSH found using common usernames." + + found_laptop=$(echo "$ssh_hosts" | grep -vw "$gateway" | head -1) + + if [[ -z "$found_laptop" ]]; then + die "Could not find any suitable SSH-enabled device" + fi + + log_info "Found SSH host at $found_laptop but need credentials." + read -r -p "Enter username for $found_laptop: " found_user + + if [[ -z "$found_user" ]]; then + die "No username provided" + fi + fi + + REMOTE_LAPTOP_IP="$found_laptop" + REMOTE_LAPTOP_USER="$found_user" + log_success "Selected remote laptop: ${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}" + + save_config +} + +setup_ssh_key_to_remote() { + local remote_host="$1" + local remote_user="$2" + + if ssh -o BatchMode=yes -o ConnectTimeout=5 "${remote_user}@${remote_host}" "echo 'SSH key works'" 2>/dev/null; then + log_success "SSH key authentication to ${remote_user}@${remote_host} already configured" + return 0 + fi + + log_info "Setting up SSH key authentication to ${remote_user}@${remote_host}..." + + if [[ ! -f ~/.ssh/id_rsa.pub ]] && [[ ! -f ~/.ssh/id_ed25519.pub ]]; then + log_info "Generating SSH key..." + ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" -q + fi + + log_info "Copying SSH key to remote host (you may be prompted for password)..." + + if command -v ssh-copy-id &>/dev/null; then + ssh-copy-id -o StrictHostKeyChecking=no "${remote_user}@${remote_host}" + else + local pub_key + pub_key=$(cat ~/.ssh/id_ed25519.pub 2>/dev/null || cat ~/.ssh/id_rsa.pub) + ssh -o StrictHostKeyChecking=no "${remote_user}@${remote_host}" "mkdir -p ~/.ssh && echo '$pub_key' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" + fi + + log_success "SSH key authentication configured" +} + +# ============================================================================= +# Download and Flash Functions +# ============================================================================= + +download_raspberry_pi_os() { + local download_dir="/tmp/rpi-image" + local image_url="https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-11-19/2024-11-19-raspios-bookworm-arm64-lite.img.xz" + local image_file="$download_dir/raspios.img.xz" + local extracted_image="$download_dir/raspios.img" + local expected_size=459000608 + + mkdir -p "$download_dir" + + if [[ -f "$extracted_image" ]]; then + log_info "Using existing image at $extracted_image" + echo "$extracted_image" + return + fi + + if [[ -f "$image_file" ]]; then + local actual_size + actual_size=$(stat -c%s "$image_file" 2>/dev/null || stat -f%z "$image_file" 2>/dev/null || echo 0) + if [[ "$actual_size" -lt "$expected_size" ]]; then + log_warning "Incomplete download detected ($actual_size < $expected_size bytes), re-downloading..." + rm -f "$image_file" + else + log_info "Image archive already downloaded" + fi + fi + + if [[ ! -f "$image_file" ]]; then + log_info "Downloading Raspberry Pi OS Lite (64-bit)..." + log_info "This may take a while depending on your internet connection..." + + if command -v aria2c &>/dev/null; then + aria2c -x 4 -c -d "$download_dir" --out="raspios.img.xz" "$image_url" >&2 + elif command -v wget &>/dev/null; then + wget --continue --show-progress -O "$image_file" "$image_url" >&2 + elif command -v curl &>/dev/null; then + curl -L -C - -o "$image_file" "$image_url" --progress-bar >&2 + else + die "No download tool available. Install wget, curl, or aria2c" + fi + + local actual_size + actual_size=$(stat -c%s "$image_file" 2>/dev/null || stat -f%z "$image_file" 2>/dev/null || echo 0) + if [[ "$actual_size" -lt "$expected_size" ]]; then + die "Download incomplete: got $actual_size bytes, expected $expected_size" + fi + log_success "Download complete: $actual_size bytes" + fi + + log_info "Extracting image..." + xz -dk "$image_file" + + if [[ ! -f "$extracted_image" ]]; then + die "Failed to extract image" + fi + + echo "$extracted_image" +} + +# ============================================================================= +# Local Flash +# ============================================================================= + +phase_flash_local() { + check_root + + log_info "=== Flash Raspberry Pi OS to SD Card (Local) ===" + + # Detect SD card + log_info "Detecting removable storage devices..." + local devices + devices=$(lsblk -d -o NAME,SIZE,TYPE,RM,TRAN | grep -E "disk.*1.*usb|disk.*1.*mmc" | awk '{print "/dev/"$1" ("$2")"}') + + if [[ -z "$devices" ]]; then + log_warning "No removable devices detected automatically." + lsblk -d -o NAME,SIZE,TYPE,RM,TRAN + read -r -p "Enter the SD card device path (e.g., /dev/sdb): " SD_CARD_DEVICE + else + echo "Detected removable devices:" + echo "$devices" + read -r -p "Enter the SD card device path from above (e.g., /dev/sdb): " SD_CARD_DEVICE + fi + + if [[ ! -b "$SD_CARD_DEVICE" ]]; then + die "Device $SD_CARD_DEVICE does not exist or is not a block device" + fi + + local root_device + root_device=$(findmnt -n -o SOURCE / | sed 's/[0-9]*$//' | sed 's/p[0-9]*$//') + if [[ "$SD_CARD_DEVICE" == "$root_device" ]]; then + die "Cannot flash to the system drive!" + fi + + auto_generate_pi_password + + local encrypted_password + encrypted_password=$(echo "$PI_PASSWORD" | openssl passwd -6 -stdin) + + save_config + + local image_path + image_path=$(download_raspberry_pi_os) + + log_warning "This will ERASE ALL DATA on $SD_CARD_DEVICE" + read -r -p "Are you sure you want to continue? (yes/no): " confirm + + if [[ "$confirm" != "yes" ]]; then + die "Aborted by user" + fi + + log_info "Unmounting partitions on $SD_CARD_DEVICE..." + for partition in "${SD_CARD_DEVICE}"*; do + if mountpoint -q "$partition" 2>/dev/null || mount | grep -q "$partition"; then + umount "$partition" 2>/dev/null || true + fi + done + + log_info "Flashing image to SD card..." + dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync + sync + log_success "Image flashed successfully!" + + # Configure headless boot + log_info "Configuring headless boot..." + sleep 2 + partprobe "$SD_CARD_DEVICE" 2>/dev/null || true + sleep 2 + + local boot_partition + if [[ -b "${SD_CARD_DEVICE}1" ]]; then + boot_partition="${SD_CARD_DEVICE}1" + elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then + boot_partition="${SD_CARD_DEVICE}p1" + else + die "Could not find boot partition" + fi + + local boot_mount="/tmp/rpi-boot" + mkdir -p "$boot_mount" + mount "$boot_partition" "$boot_mount" + + touch "$boot_mount/ssh" + log_success "SSH enabled" + + echo "${PI_USER}:${encrypted_password}" >"$boot_mount/userconf.txt" + log_success "User '$PI_USER' configured" + + local root_partition + if [[ -b "${SD_CARD_DEVICE}2" ]]; then + root_partition="${SD_CARD_DEVICE}2" + elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then + root_partition="${SD_CARD_DEVICE}p2" + fi + + if [[ -n "$root_partition" ]]; then + local root_mount="/tmp/rpi-root" + mkdir -p "$root_mount" + mount "$root_partition" "$root_mount" + + echo "$PI_HOSTNAME" >"$root_mount/etc/hostname" + sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts" + + log_success "Hostname set to '$PI_HOSTNAME'" + + umount "$root_mount" + fi + + umount "$boot_mount" + sync + + log_success "SD card configured for headless boot!" + log_success "Flash complete!" + echo + log_info "Pi credentials:" + log_info " User: $PI_USER" + log_info " Password: $PI_PASSWORD" + log_info " Hostname: $PI_HOSTNAME" + echo + log_info "Next steps:" + log_info "1. Remove SD card and insert into Raspberry Pi" + log_info "2. Connect the Pi to power and network" + log_info "3. Wait 2-3 minutes for first boot" +} + +# ============================================================================= +# Remote Flash +# ============================================================================= + +phase_flash_remote() { + log_info "=== Flash Raspberry Pi OS to SD Card on Remote Laptop ===" + + discover_remote_laptop + + setup_ssh_key_to_remote "$REMOTE_LAPTOP_IP" "$REMOTE_LAPTOP_USER" + + local remote="${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}" + + log_info "Checking for SD card on remote laptop..." + echo "Block devices on ${remote}:" + ssh "$remote" "lsblk -d -o NAME,SIZE,TYPE,RM,TRAN,MODEL" || true + echo + + log_info "Auto-detecting SD card on remote laptop..." + local sd_device + sd_device=$(ssh "$remote" "lsblk -d -o NAME,RM,TRAN | grep -E '1.*(usb|mmc)' | awk '{print \"/dev/\"\$1}' | head -1" 2>/dev/null || true) + + if [[ -z "$sd_device" ]]; then + die "No SD card detected on remote laptop. Please insert an SD card and try again." + fi + + local sd_info + # shellcheck disable=SC2029 # Intentional client-side expansion + sd_info=$(ssh "$remote" "lsblk -d -o NAME,SIZE,MODEL $sd_device 2>/dev/null | tail -1" || true) + + log_success "Auto-detected SD card: $sd_device ($sd_info)" + SD_CARD_DEVICE="$sd_device" + + # shellcheck disable=SC2029 # Intentional client-side expansion + if ! ssh "$remote" "[[ -b '$SD_CARD_DEVICE' ]]" 2>/dev/null; then + die "Device $SD_CARD_DEVICE does not exist on remote laptop" + fi + + auto_generate_pi_password + log_success "Pi user '$PI_USER' password: $PI_PASSWORD" + + local encrypted_password + encrypted_password=$(echo "$PI_PASSWORD" | openssl passwd -6 -stdin) + + save_config + + log_info "Copying script to remote laptop..." + scp "$0" "${remote}:/tmp/raspberry_pi_flash_sd.sh" + + log_info "Executing flash on remote laptop..." + log_warning "This will ERASE ALL DATA on ${SD_CARD_DEVICE} on the remote laptop!" + log_info "Proceeding automatically in 5 seconds... (Ctrl+C to cancel)" + sleep 5 + + ssh -tt "$remote" "sudo SD_CARD_DEVICE='$SD_CARD_DEVICE' PI_USER='$PI_USER' PI_HOSTNAME='$PI_HOSTNAME' bash /tmp/raspberry_pi_flash_sd.sh execute-remote '$encrypted_password'" + + log_success "Flash complete!" + echo + log_info "Pi credentials:" + log_info " User: $PI_USER" + log_info " Password: $PI_PASSWORD" + log_info " Hostname: $PI_HOSTNAME" + echo + log_info "Next steps:" + log_info "1. Remove SD card from the laptop and insert into Raspberry Pi" + log_info "2. Connect the Pi to power and network" + log_info "3. Wait 2-3 minutes for first boot" +} + +# Called on the remote laptop by phase_flash_remote +phase_execute_remote() { + check_root + + local encrypted_password="${1:-}" + + log_info "=== Executing Flash on Remote Laptop ===" + + if [[ -z "$SD_CARD_DEVICE" ]]; then + die "SD_CARD_DEVICE not set" + fi + + local image_path + image_path=$(download_raspberry_pi_os) + + log_info "Unmounting partitions on $SD_CARD_DEVICE..." + for partition in "${SD_CARD_DEVICE}"*; do + if mountpoint -q "$partition" 2>/dev/null || mount | grep -q "$partition"; then + umount "$partition" 2>/dev/null || true + fi + done + + log_info "Flashing image to SD card..." + dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync + sync + log_success "Image flashed successfully!" + + log_info "Configuring headless boot..." + sleep 2 + partprobe "$SD_CARD_DEVICE" 2>/dev/null || true + sleep 2 + + local boot_partition + if [[ -b "${SD_CARD_DEVICE}1" ]]; then + boot_partition="${SD_CARD_DEVICE}1" + elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then + boot_partition="${SD_CARD_DEVICE}p1" + else + die "Could not find boot partition" + fi + + local boot_mount="/tmp/rpi-boot" + mkdir -p "$boot_mount" + mount "$boot_partition" "$boot_mount" + + touch "$boot_mount/ssh" + log_success "SSH enabled" + + if [[ -n "$encrypted_password" ]]; then + echo "${PI_USER}:${encrypted_password}" >"$boot_mount/userconf.txt" + log_success "User '$PI_USER' configured" + fi + + local root_partition + if [[ -b "${SD_CARD_DEVICE}2" ]]; then + root_partition="${SD_CARD_DEVICE}2" + elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then + root_partition="${SD_CARD_DEVICE}p2" + fi + + if [[ -n "$root_partition" ]]; then + local root_mount="/tmp/rpi-root" + mkdir -p "$root_mount" + mount "$root_partition" "$root_mount" + + echo "$PI_HOSTNAME" >"$root_mount/etc/hostname" + sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts" + + log_success "Hostname set to '$PI_HOSTNAME'" + + umount "$root_mount" + fi + + umount "$boot_mount" + sync + + log_success "SD card configured for headless boot!" +} + +# ============================================================================= +# Main +# ============================================================================= + +show_help() { + cat <<'EOF' +Raspberry Pi SD Card Flash Script + +Usage: ./raspberry_pi_flash_sd.sh + +Commands: + local Flash SD card locally (requires root) + remote Flash SD card on a remote laptop via SSH + execute-remote Internal: executed on remote laptop + help Show this help message + +The script will: +1. Auto-discover a remote laptop with an SD card (for remote mode) +2. Download Raspberry Pi OS Lite (64-bit) +3. Flash the image to the SD card +4. Configure headless boot (SSH enabled, user created, hostname set) + +Credentials are auto-generated and saved to .raspberry_pi.conf + +Examples: + # Flash locally (run as root) + sudo ./raspberry_pi_flash_sd.sh local + + # Flash on remote laptop + ./raspberry_pi_flash_sd.sh remote +EOF +} + +main() { + local command="${1:-help}" + + case "$command" in + local) + phase_flash_local + ;; + remote) + phase_flash_remote + ;; + execute-remote) + phase_execute_remote "${2:-}" + ;; + help | --help | -h) + show_help + ;; + *) + log_error "Unknown command: $command" + show_help + exit 1 + ;; + esac +} + +main "$@" diff --git a/scripts/features/raspberry_pi_nextcloud.sh b/scripts/features/raspberry_pi_nextcloud.sh new file mode 100755 index 0000000..0bf53ce --- /dev/null +++ b/scripts/features/raspberry_pi_nextcloud.sh @@ -0,0 +1,1275 @@ +#!/bin/bash +# Nextcloud Installation Script for Raspberry Pi +# This script installs and configures Nextcloud on a Raspberry Pi +# +# Usage: +# ./raspberry_pi_nextcloud.sh install - Install Nextcloud (run on Pi or via SSH) +# ./raspberry_pi_nextcloud.sh fix - Fix common Nextcloud issues +# ./raspberry_pi_nextcloud.sh install-remote - Install Nextcloud via SSH from laptop + +set -euo pipefail + +# Script directory for config file +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="${SCRIPT_DIR}/.raspberry_pi.conf" + +# Load configuration from gitignored config file if it exists +if [[ -f "$CONFIG_FILE" ]]; then + # shellcheck source=/dev/null + source "$CONFIG_FILE" +fi + +# Configuration +PI_HOSTNAME="${PI_HOSTNAME:-nextcloud-pi}" +PI_USER="${PI_USER:-pi}" +PI_PASSWORD="${PI_PASSWORD:-}" +PI_TIMEZONE="${PI_TIMEZONE:-Europe/Warsaw}" +PI_LOCALE="${PI_LOCALE:-en_US.UTF-8}" +NEXTCLOUD_ADMIN_USER="${NEXTCLOUD_ADMIN_USER:-admin}" +NEXTCLOUD_ADMIN_PASSWORD="${NEXTCLOUD_ADMIN_PASSWORD:-}" +NEXTCLOUD_DATA_DIR="${NEXTCLOUD_DATA_DIR:-/var/www/nextcloud/data}" +NEXTCLOUD_VERSION="${NEXTCLOUD_VERSION:-latest}" + +# DuckDNS for free domain and Let's Encrypt SSL +# Get your free subdomain at https://www.duckdns.org/ +DUCKDNS_DOMAIN="${DUCKDNS_DOMAIN:-}" # e.g., "mycloud" for mycloud.duckdns.org +DUCKDNS_TOKEN="${DUCKDNS_TOKEN:-}" # Your DuckDNS token +LETSENCRYPT_EMAIL="${LETSENCRYPT_EMAIL:-}" # Email for Let's Encrypt notifications + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" >&2 +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" >&2 +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" >&2 +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" >&2 +} + +die() { + log_error "$1" + exit 1 +} + +check_root() { + if [[ $EUID -ne 0 ]]; then + die "This script must be run as root. Use: sudo $0" + fi +} + +save_config() { + cat >"$CONFIG_FILE" </dev/null | tr -dc 'A-Za-z0-9!@#$%&*' | cut -c1-"$length") + echo "$chars" +} + +auto_generate_nextcloud_password() { + if [[ -z "$NEXTCLOUD_ADMIN_PASSWORD" ]]; then + NEXTCLOUD_ADMIN_PASSWORD=$(generate_password 20) + log_info "Auto-generated Nextcloud admin password (will be saved to config file)" + fi +} + +wait_for_apt_lock() { + local max_wait=600 + local waited=0 + + while fuser /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/cache/apt/archives/lock >/dev/null 2>&1; do + if [[ $waited -eq 0 ]]; then + log_info "Waiting for other apt/dpkg processes to finish..." + pgrep -a 'apt|dpkg' | head -5 >&2 || true + fi + sleep 5 + waited=$((waited + 5)) + if [[ $waited -ge $max_wait ]]; then + die "Timeout waiting for apt lock after ${max_wait}s" + fi + if [[ $((waited % 30)) -eq 0 ]]; then + log_info "Still waiting... (${waited}s elapsed)" + fi + done + + if [[ $waited -gt 0 ]]; then + log_success "Apt lock acquired after ${waited}s" + fi +} + +# ============================================================================= +# Network Discovery (for remote installation) +# ============================================================================= + +ensure_dependencies() { + local missing_packages=() + + if ! command -v nmap &>/dev/null; then + missing_packages+=("nmap") + fi + + if ! command -v sshpass &>/dev/null; then + missing_packages+=("sshpass") + fi + + if [[ ${#missing_packages[@]} -gt 0 ]]; then + log_info "Installing missing packages: ${missing_packages[*]}" + + if command -v pacman &>/dev/null; then + sudo pacman -S --noconfirm "${missing_packages[@]}" + elif command -v apt-get &>/dev/null; then + sudo apt-get update && sudo apt-get install -y "${missing_packages[@]}" + elif command -v dnf &>/dev/null; then + sudo dnf install -y "${missing_packages[@]}" + else + die "Could not detect package manager. Please install manually: ${missing_packages[*]}" + fi + fi +} + +discover_raspberry_pi() { + log_info "Auto-discovering Raspberry Pi on local network..." + + ensure_dependencies + + local my_ip + my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) + local gateway + gateway=$(ip route | grep default | awk '{print $3}' | head -1) + local network="${gateway%.*}.0/24" + + log_info "Local IP: $my_ip, Network: $network" + log_info "Scanning for Raspberry Pi (hostname: $PI_HOSTNAME)..." + + local pi_ip="" + + # Try resolving hostname directly + pi_ip=$(getent hosts "$PI_HOSTNAME" 2>/dev/null | awk '{print $1}' | head -1) || true + if [[ -z "$pi_ip" ]]; then + pi_ip=$(getent hosts "${PI_HOSTNAME}.local" 2>/dev/null | awk '{print $1}' | head -1) || true + fi + + if [[ -n "$pi_ip" ]]; then + log_success "Found Pi by hostname: $pi_ip" + echo "$pi_ip" + return + fi + + log_info "Hostname resolution failed, scanning network..." + nmap -sn -T4 "$network" &>/dev/null || true + + local ssh_hosts + ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) || true + + if [[ -z "$ssh_hosts" ]]; then + die "No SSH-enabled devices found. Is the Pi connected and booted?" + fi + + log_info "Found SSH-enabled devices: $(echo "$ssh_hosts" | tr '\n' ' ')" + + for ip in $ssh_hosts; do + log_info "Trying $ip with user '$PI_USER'..." + + if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "hostname" 2>/dev/null | grep -qi "$PI_HOSTNAME"; then + log_success "Found Raspberry Pi at $ip" + echo "$ip" + return + fi + + if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "echo ok" 2>/dev/null | grep -q "ok"; then + log_success "Found device responding to Pi credentials at $ip" + echo "$ip" + return + fi + done + + die "Could not find Raspberry Pi on network." +} + +# ============================================================================= +# System Configuration Phase +# ============================================================================= + +phase_configure_system() { + check_root + + log_info "=== Configuring Raspberry Pi System ===" + + wait_for_apt_lock + + log_info "Fixing any broken packages..." + DEBIAN_FRONTEND=noninteractive dpkg --configure -a --force-confdef --force-confold || true + + log_info "Updating system packages..." + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade -y + + log_info "Setting timezone to $PI_TIMEZONE..." + timedatectl set-timezone "$PI_TIMEZONE" + + log_info "Configuring locale..." + sed -i "s/^# *$PI_LOCALE/$PI_LOCALE/" /etc/locale.gen + locale-gen + update-locale LANG="$PI_LOCALE" + + log_info "Hardening SSH configuration..." + cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup + + cat >>/etc/ssh/sshd_config.d/hardening.conf <<'EOF' +# Security hardening +PermitRootLogin no +PasswordAuthentication yes +PubkeyAuthentication yes +X11Forwarding no +MaxAuthTries 3 +ClientAliveInterval 300 +ClientAliveCountMax 2 +EOF + + systemctl restart sshd + + log_info "Installing useful packages..." + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + vim \ + htop \ + curl \ + wget \ + git \ + ufw \ + fail2ban \ + unattended-upgrades + + log_info "Configuring firewall..." + ufw default deny incoming + ufw default allow outgoing + ufw allow ssh + ufw allow 80/tcp + ufw allow 443/tcp + ufw --force enable + + log_info "Configuring fail2ban..." + cat >/etc/fail2ban/jail.local <<'EOF' +[DEFAULT] +bantime = 1h +findtime = 10m +maxretry = 5 + +[sshd] +enabled = true +port = ssh +filter = sshd +logpath = /var/log/auth.log +maxretry = 3 +EOF + + systemctl enable fail2ban + systemctl restart fail2ban + + log_info "Enabling automatic security updates..." + cat >/etc/apt/apt.conf.d/50unattended-upgrades <<'EOF' +Unattended-Upgrade::Origins-Pattern { + "origin=Debian,codename=${distro_codename},label=Debian-Security"; + "origin=Raspbian,codename=${distro_codename},label=Raspbian"; +}; +Unattended-Upgrade::AutoFixInterruptedDpkg "true"; +Unattended-Upgrade::Remove-Unused-Dependencies "true"; +EOF + + cat >/etc/apt/apt.conf.d/20auto-upgrades <<'EOF' +APT::Periodic::Update-Package-Lists "1"; +APT::Periodic::Unattended-Upgrade "1"; +APT::Periodic::AutocleanInterval "7"; +EOF + + log_success "System configuration complete!" +} + +# ============================================================================= +# Nextcloud Installation Phase +# ============================================================================= + +phase_install_nextcloud() { + check_root + + log_info "=== Installing Nextcloud ===" + + wait_for_apt_lock + + log_info "Installing Apache, PHP, MariaDB, and dependencies..." + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + apache2 \ + mariadb-server \ + php \ + php-gd \ + php-json \ + php-mysql \ + php-curl \ + php-mbstring \ + php-intl \ + php-imagick \ + php-xml \ + php-zip \ + php-bz2 \ + php-bcmath \ + php-gmp \ + php-apcu \ + php-redis \ + php-ldap \ + libapache2-mod-php \ + redis-server \ + certbot \ + python3-certbot-apache \ + imagemagick \ + libmagickcore-6.q16-6-extra + + log_success "Packages installed" + + # Configure MariaDB + log_info "Configuring MariaDB..." + + local db_password + db_password=$(generate_password 32) + + mysql -u root </root/.nextcloud_db_password + chmod 600 /root/.nextcloud_db_password + log_success "MariaDB configured" + + # Download Nextcloud + log_info "Downloading Nextcloud..." + + cd /tmp + if [[ ! -f nextcloud.zip ]]; then + wget -q --show-progress "https://download.nextcloud.com/server/releases/latest.zip" -O nextcloud.zip >&2 + fi + + rm -rf /var/www/nextcloud + unzip -q nextcloud.zip -d /var/www/ + chown -R www-data:www-data /var/www/nextcloud + + log_success "Nextcloud downloaded and extracted" + + # Configure Apache + log_info "Configuring Apache..." + + cat >/etc/apache2/sites-available/nextcloud.conf <<'EOF' + + ServerAdmin admin@localhost + DocumentRoot /var/www/nextcloud + + + Require all granted + AllowOverride All + Options FollowSymLinks MultiViews + + + Dav off + + + + ErrorLog ${APACHE_LOG_DIR}/nextcloud_error.log + CustomLog ${APACHE_LOG_DIR}/nextcloud_access.log combined + +EOF + + a2enmod rewrite + a2enmod headers + a2enmod env + a2enmod dir + a2enmod mime + a2enmod ssl + a2dissite 000-default + a2ensite nextcloud + + systemctl restart apache2 + + log_success "Apache configured" + + # Configure PHP + log_info "Configuring PHP..." + + local php_version + php_version=$(php -v | head -1 | grep -oP '\d+\.\d+') + + local php_ini="/etc/php/${php_version}/apache2/php.ini" + + sed -i 's/memory_limit = .*/memory_limit = 512M/' "$php_ini" + sed -i 's/upload_max_filesize = .*/upload_max_filesize = 16G/' "$php_ini" + sed -i 's/post_max_size = .*/post_max_size = 16G/' "$php_ini" + sed -i 's/max_execution_time = .*/max_execution_time = 3600/' "$php_ini" + sed -i 's/max_input_time = .*/max_input_time = 3600/' "$php_ini" + sed -i 's/;date.timezone =.*/date.timezone = Europe\/Warsaw/' "$php_ini" + + if ! grep -q "opcache.interned_strings_buffer" "$php_ini"; then + cat >>"$php_ini" <<'EOF' + +; Nextcloud optimizations +opcache.enable=1 +opcache.interned_strings_buffer=16 +opcache.max_accelerated_files=10000 +opcache.memory_consumption=128 +opcache.save_comments=1 +opcache.revalidate_freq=1 + +; APCu configuration +apc.enable_cli=1 +EOF + fi + + systemctl restart apache2 + + log_success "PHP configured" + + # Configure Redis + log_info "Configuring Redis..." + + systemctl enable redis-server + systemctl start redis-server + + log_success "Redis configured" + + # Install Nextcloud + log_info "Installing Nextcloud..." + + auto_generate_nextcloud_password + + local pi_ip + pi_ip=$(hostname -I | awk '{print $1}') + + cd /var/www/nextcloud + sudo -u www-data php occ maintenance:install \ + --database "mysql" \ + --database-name "nextcloud" \ + --database-user "nextcloud" \ + --database-pass "$db_password" \ + --admin-user "$NEXTCLOUD_ADMIN_USER" \ + --admin-pass "$NEXTCLOUD_ADMIN_PASSWORD" \ + --data-dir "$NEXTCLOUD_DATA_DIR" + + # Configure trusted domains + sudo -u www-data php occ config:system:set trusted_domains 1 --value="$pi_ip" + sudo -u www-data php occ config:system:set trusted_domains 2 --value="$PI_HOSTNAME" + sudo -u www-data php occ config:system:set trusted_domains 3 --value="${PI_HOSTNAME}.local" + + # Configure caching + sudo -u www-data php occ config:system:set memcache.local --value='\OC\Memcache\APCu' + sudo -u www-data php occ config:system:set memcache.distributed --value='\OC\Memcache\Redis' + sudo -u www-data php occ config:system:set memcache.locking --value='\OC\Memcache\Redis' + sudo -u www-data php occ config:system:set redis host --value='localhost' + sudo -u www-data php occ config:system:set redis port --value=6379 --type=integer + + # Set default phone region + sudo -u www-data php occ config:system:set default_phone_region --value='PL' + + # Set maintenance window + sudo -u www-data php occ config:system:set maintenance_window_start --value=1 --type=integer + + log_success "Nextcloud installed" + + # Setup background jobs + log_info "Setting up Nextcloud background jobs..." + + sudo -u www-data php occ background:cron + + # Add cron job + ( + crontab -u www-data -l 2>/dev/null || true + echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" + ) | sort -u | crontab -u www-data - + + log_success "Cron jobs configured" + + # Verify installation + log_info "Verifying Nextcloud installation..." + + if sudo -u www-data php occ status | grep -q "installed: true"; then + log_success "Nextcloud is responding!" + sudo -u www-data php occ status + else + log_warning "Nextcloud may not be fully configured" + fi + + save_config + + log_success "========================================" + log_success "Nextcloud installation complete!" + log_success "========================================" + log_info "Access Nextcloud at: http://$pi_ip" + log_info "Admin user: $NEXTCLOUD_ADMIN_USER" + log_info "Admin password: $NEXTCLOUD_ADMIN_PASSWORD" + log_info "Database password saved at: /root/.nextcloud_db_password" +} + +# ============================================================================= +# Fix Nextcloud Issues +# ============================================================================= + +phase_fix_issues() { + check_root + + log_info "=== Fixing Nextcloud Issues ===" + + cd /var/www/nextcloud + + # 1. Fix background jobs (cron not running properly) + log_info "Fixing background jobs..." + + # Ensure cron is set as background job method + sudo -u www-data php occ background:cron + + # Ensure cron job exists and is correct + ( + crontab -u www-data -l 2>/dev/null | grep -v "cron.php" + echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" + ) | crontab -u www-data - + + # Run cron manually now to reset the timer + log_info "Running cron job manually..." + sudo -u www-data php /var/www/nextcloud/cron.php + + log_success "Background jobs configured" + + # 2. Setup HTTPS with proper CA-signed certificate + log_info "Setting up HTTPS with trusted CA..." + + local pi_ip + pi_ip=$(hostname -I | awk '{print $1}') + + local ssl_dir="/etc/ssl/nextcloud" + mkdir -p "$ssl_dir" + chmod 700 "$ssl_dir" + + # Generate CA if it doesn't exist + if [[ ! -f "$ssl_dir/ca.crt" ]]; then + log_info "Creating Certificate Authority (CA)..." + + # Generate CA private key + openssl genrsa -out "$ssl_dir/ca.key" 4096 + chmod 600 "$ssl_dir/ca.key" + + # Generate CA certificate (valid for 10 years) + openssl req -x509 -new -nodes -key "$ssl_dir/ca.key" \ + -sha256 -days 3650 \ + -out "$ssl_dir/ca.crt" \ + -subj "/C=PL/ST=Home/L=Local/O=Nextcloud Home CA/OU=Certificate Authority/CN=Nextcloud Home CA" + + log_success "CA created: $ssl_dir/ca.crt" + fi + + # Generate server certificate signed by our CA + local regenerate="${1:-}" + if [[ ! -f "$ssl_dir/server.crt" ]] || [[ "$regenerate" == "--regenerate" ]]; then + log_info "Generating server certificate signed by CA..." + + # Generate server private key + openssl genrsa -out "$ssl_dir/server.key" 2048 + chmod 600 "$ssl_dir/server.key" + + # Create certificate signing request (CSR) + openssl req -new -key "$ssl_dir/server.key" \ + -out "$ssl_dir/server.csr" \ + -subj "/C=PL/ST=Home/L=Local/O=Nextcloud/OU=Server/CN=$PI_HOSTNAME" + + # Create extension file for SAN (Subject Alternative Names) + # This allows the certificate to be valid for hostname, IP, and .local + cat >"$ssl_dir/server.ext" </etc/apache2/sites-available/nextcloud-ssl.conf < + ServerAdmin admin@localhost + DocumentRoot /var/www/nextcloud + ServerName $PI_HOSTNAME + ServerAlias ${PI_HOSTNAME}.local $pi_ip + + SSLEngine on + SSLCertificateFile $ssl_dir/server.crt + SSLCertificateKeyFile $ssl_dir/server.key + + + Require all granted + AllowOverride All + Options FollowSymLinks MultiViews + + + Dav off + + + + # Security headers + Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains" + Header always set X-Content-Type-Options "nosniff" + Header always set X-Frame-Options "SAMEORIGIN" + Header always set X-XSS-Protection "1; mode=block" + Header always set Referrer-Policy "no-referrer" + + ErrorLog \${APACHE_LOG_DIR}/nextcloud_ssl_error.log + CustomLog \${APACHE_LOG_DIR}/nextcloud_ssl_access.log combined + + +# Redirect HTTP to HTTPS + + ServerName $PI_HOSTNAME + ServerAlias $pi_ip + Redirect permanent / https://$PI_HOSTNAME/ + +EOF + + a2enmod ssl + a2enmod headers + a2ensite nextcloud-ssl + + # Update Nextcloud config for HTTPS + sudo -u www-data php occ config:system:set overwrite.cli.url --value="https://$PI_HOSTNAME" + sudo -u www-data php occ config:system:set overwriteprotocol --value="https" + + systemctl restart apache2 + + log_success "HTTPS configured with CA-signed certificate" + + # 3. Run mimetype migrations + log_info "Running mimetype migrations..." + sudo -u www-data php occ maintenance:repair --include-expensive + log_success "Mimetype migrations complete" + + # 4. Add missing database indices + log_info "Adding missing database indices..." + sudo -u www-data php occ db:add-missing-indices + log_success "Database indices added" + + # 5. Install ImageMagick SVG support + log_info "Installing ImageMagick SVG support..." + DEBIAN_FRONTEND=noninteractive apt-get install -y libmagickcore-6.q16-6-extra + + # Enable SVG in ImageMagick policy + local policy_file="/etc/ImageMagick-6/policy.xml" + if [[ -f "$policy_file" ]]; then + # Remove SVG restrictions if present + sed -i 's///' "$policy_file" + # If no SVG policy exists, add one allowing it + if ! grep -q 'pattern="SVG"' "$policy_file"; then + sed -i '//a\ ' "$policy_file" + fi + fi + + systemctl restart apache2 + log_success "ImageMagick SVG support configured" + + # 6. Set up basic SMTP (placeholder - user needs to configure actual mail server) + log_info "Note: Email server not configured - please configure in Nextcloud admin settings" + + # 7. Clear any remaining warnings + log_info "Clearing Nextcloud caches..." + sudo -u www-data php occ maintenance:repair + sudo -u www-data php occ files:scan --all + + # 8. Verify all fixes + log_info "Verifying fixes..." + + # Run cron again to update last run time + sudo -u www-data php /var/www/nextcloud/cron.php + + log_success "========================================" + log_success "Nextcloud issues fixed!" + log_success "========================================" + echo + log_info "Summary of changes:" + log_info " ✓ Background jobs (cron) configured and running" + log_info " ✓ HTTPS enabled with CA-signed certificate" + log_info " ✓ Strict-Transport-Security header added" + log_info " ✓ Mimetype migrations completed" + log_info " ✓ Missing database indices added" + log_info " ✓ ImageMagick SVG support installed" + echo + log_info "Current certificate: self-signed CA (requires manual install on devices)" + log_info " - Run: $0 install-ca (on your laptop)" + log_info " - Or download: https://$PI_HOSTNAME/ca/nextcloud-ca.crt" + echo + log_info "For auto-trusted HTTPS on ALL devices (recommended):" + log_info " 1. Get free domain at https://www.duckdns.org/" + log_info " 2. Run: $0 setup-ssl" + echo + log_info "Access Nextcloud at: https://$PI_HOSTNAME" +} + +# ============================================================================= +# Setup Let's Encrypt SSL with DuckDNS +# ============================================================================= + +phase_setup_ssl() { + check_root + + log_info "=== Setting up Let's Encrypt SSL with DuckDNS ===" + + # Check if DuckDNS is configured + if [[ -z "$DUCKDNS_DOMAIN" ]] || [[ -z "$DUCKDNS_TOKEN" ]]; then + echo + log_info "To get auto-trusted HTTPS, you need a free DuckDNS domain." + log_info "1. Go to https://www.duckdns.org/ and sign in with Google/GitHub/etc." + log_info "2. Create a subdomain (e.g., 'myhomecloud' for myhomecloud.duckdns.org)" + log_info "3. Copy your token from the DuckDNS page" + echo + + read -r -p "Enter your DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN + read -r -p "Enter your DuckDNS token: " DUCKDNS_TOKEN + read -r -p "Enter your email (for Let's Encrypt notifications): " LETSENCRYPT_EMAIL + + if [[ -z "$DUCKDNS_DOMAIN" ]] || [[ -z "$DUCKDNS_TOKEN" ]] || [[ -z "$LETSENCRYPT_EMAIL" ]]; then + die "All fields are required" + fi + fi + + local full_domain="${DUCKDNS_DOMAIN}.duckdns.org" + local pi_local_ip + pi_local_ip=$(hostname -I | awk '{print $1}') + + # Get public IP for DuckDNS (Let's Encrypt needs external access) + local public_ip + public_ip=$(curl -s https://api.ipify.org) || public_ip=$(curl -s https://ifconfig.me) || true + + log_info "Domain: $full_domain" + log_info "Pi local IP: $pi_local_ip" + log_info "Public IP: $public_ip" + + echo + log_warning "=== IMPORTANT: Port Forwarding Required ===" + log_warning "For Let's Encrypt to work, you MUST forward ports on your router:" + log_warning " - Forward port 80 (HTTP) to $pi_local_ip" + log_warning " - Forward port 443 (HTTPS) to $pi_local_ip" + log_warning "" + log_warning "Go to your router admin page (usually http://192.168.1.1)" + log_warning "and set up port forwarding before continuing." + echo + read -r -p "Have you set up port forwarding? (yes/no): " port_forward_done + + if [[ "$port_forward_done" != "yes" ]]; then + log_info "Please set up port forwarding and run this command again." + log_info "Without port forwarding, Let's Encrypt cannot verify your domain." + exit 0 + fi + + # Update DuckDNS to point to PUBLIC IP (not local IP) + log_info "Updating DuckDNS to point to public IP $public_ip..." + local duckdns_response + # When ip= is empty, DuckDNS auto-detects the public IP + duckdns_response=$(curl -s "https://www.duckdns.org/update?domains=${DUCKDNS_DOMAIN}&token=${DUCKDNS_TOKEN}&ip=") + + if [[ "$duckdns_response" != "OK" ]]; then + die "Failed to update DuckDNS: $duckdns_response" + fi + log_success "DuckDNS updated to public IP" + + # Set up automatic DuckDNS updates (cron) - auto-detect public IP + log_info "Setting up automatic DuckDNS IP updates..." + mkdir -p /opt/duckdns + cat >/opt/duckdns/duck.sh </dev/null || true) | grep -v "duckdns" | { + cat + echo "*/5 * * * * /opt/duckdns/duck.sh >/dev/null 2>&1" + } | crontab - + + log_success "DuckDNS auto-update configured" + + # Wait for DNS propagation + log_info "Waiting for DNS propagation (this may take a minute)..." + local dns_ip="" + local attempts=0 + while [[ "$dns_ip" != "$public_ip" ]] && [[ $attempts -lt 12 ]]; do + sleep 5 + dns_ip=$(dig +short "$full_domain" 2>/dev/null | tail -1) || true + attempts=$((attempts + 1)) + log_info " DNS lookup: $dns_ip (expecting $public_ip, attempt $attempts/12)" + done + + if [[ "$dns_ip" != "$public_ip" ]]; then + log_warning "DNS may not have propagated yet. Continuing anyway..." + else + log_success "DNS verified: $full_domain -> $public_ip" + fi + + # Install certbot if not present + if ! command -v certbot &>/dev/null; then + log_info "Installing certbot..." + DEBIAN_FRONTEND=noninteractive apt-get install -y certbot python3-certbot-apache + fi + + # Get Let's Encrypt certificate + log_info "Obtaining Let's Encrypt certificate..." + + # First update Apache config with the new domain + cat >/etc/apache2/sites-available/nextcloud-ssl.conf < + ServerAdmin ${LETSENCRYPT_EMAIL} + DocumentRoot /var/www/nextcloud + ServerName ${full_domain} + + SSLEngine on + # Certbot will update these paths + SSLCertificateFile /etc/ssl/nextcloud/server.crt + SSLCertificateKeyFile /etc/ssl/nextcloud/server.key + + + Require all granted + AllowOverride All + Options FollowSymLinks MultiViews + + + Dav off + + + + # Security headers + Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains" + Header always set X-Content-Type-Options "nosniff" + Header always set X-Frame-Options "SAMEORIGIN" + Header always set X-XSS-Protection "1; mode=block" + Header always set Referrer-Policy "no-referrer" + + ErrorLog \${APACHE_LOG_DIR}/nextcloud_ssl_error.log + CustomLog \${APACHE_LOG_DIR}/nextcloud_ssl_access.log combined + + + + ServerName ${full_domain} + Redirect permanent / https://${full_domain}/ + +EOF + + systemctl reload apache2 + + # Run certbot + certbot --apache -d "$full_domain" --non-interactive --agree-tos --email "$LETSENCRYPT_EMAIL" --redirect + + log_success "Let's Encrypt certificate obtained!" + + # Update Nextcloud trusted domains + log_info "Updating Nextcloud configuration..." + cd /var/www/nextcloud + sudo -u www-data php occ config:system:set trusted_domains 0 --value="$full_domain" + sudo -u www-data php occ config:system:set overwrite.cli.url --value="https://$full_domain" + sudo -u www-data php occ config:system:set overwriteprotocol --value="https" + sudo -u www-data php occ config:system:set overwritehost --value="$full_domain" + + # Keep local access working + sudo -u www-data php occ config:system:set trusted_domains 1 --value="$pi_local_ip" + sudo -u www-data php occ config:system:set trusted_domains 2 --value="$PI_HOSTNAME" + sudo -u www-data php occ config:system:set trusted_domains 3 --value="${PI_HOSTNAME}.local" + + log_success "Nextcloud configured for $full_domain" + + # Set up auto-renewal + log_info "Setting up automatic certificate renewal..." + systemctl enable certbot.timer + systemctl start certbot.timer + + log_success "========================================" + log_success "Let's Encrypt SSL configured!" + log_success "========================================" + echo + log_info "Your Nextcloud is now accessible at:" + log_info " https://$full_domain (from anywhere on the internet)" + log_info " https://$pi_local_ip (from your local network)" + echo + log_info "This certificate is trusted by ALL browsers and devices automatically!" + log_info "No manual certificate installation required." + echo + log_info "Certificate auto-renewal is enabled." + log_info "DuckDNS IP auto-update is enabled." +} + +phase_setup_ssl_remote() { + log_info "=== Setting up Let's Encrypt SSL via SSH ===" + + if [[ -z "$PI_PASSWORD" ]]; then + die "PI_PASSWORD not set. Run install-remote first." + fi + + local pi_ip + pi_ip=$(discover_raspberry_pi) + + if [[ -z "$pi_ip" ]]; then + die "Failed to discover Raspberry Pi" + fi + + # Get DuckDNS credentials if not set + if [[ -z "$DUCKDNS_DOMAIN" ]] || [[ -z "$DUCKDNS_TOKEN" ]]; then + echo + log_info "To get auto-trusted HTTPS, you need a free DuckDNS domain." + log_info "1. Go to https://www.duckdns.org/ and sign in" + log_info "2. Create a subdomain (e.g., 'myhomecloud')" + log_info "3. Copy your token" + echo + + read -r -p "Enter your DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN + read -r -p "Enter your DuckDNS token: " DUCKDNS_TOKEN + read -r -p "Enter your email (for Let's Encrypt): " LETSENCRYPT_EMAIL + fi + + save_config + + log_info "Copying script to Pi..." + sshpass -p "$PI_PASSWORD" scp -o StrictHostKeyChecking=no "$0" "${PI_USER}@${pi_ip}:/tmp/raspberry_pi_nextcloud.sh" + + log_info "Running SSL setup on Pi..." + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ + "echo '$PI_PASSWORD' | sudo -S DUCKDNS_DOMAIN='$DUCKDNS_DOMAIN' DUCKDNS_TOKEN='$DUCKDNS_TOKEN' LETSENCRYPT_EMAIL='$LETSENCRYPT_EMAIL' bash /tmp/raspberry_pi_nextcloud.sh setup-ssl" + + local full_domain="${DUCKDNS_DOMAIN}.duckdns.org" + + log_success "========================================" + log_success "SSL setup complete!" + log_success "========================================" + echo + log_info "Access your Nextcloud at: https://$full_domain" + log_info "This works on ALL devices without certificate warnings!" +} + +# ============================================================================= +# Remote Installation +# ============================================================================= + +phase_install_remote() { + log_info "=== Installing Nextcloud via SSH ===" + + if [[ -z "$PI_PASSWORD" ]]; then + die "PI_PASSWORD not set. Did you run flash script first?" + fi + + local pi_ip + pi_ip=$(discover_raspberry_pi) + + if [[ -z "$pi_ip" ]]; then + die "Failed to discover Raspberry Pi" + fi + + log_info "Using Raspberry Pi at: $pi_ip" + + # Remove old host key if present + ssh-keygen -R "$pi_ip" 2>/dev/null || true + + log_info "Copying script to Pi..." + sshpass -p "$PI_PASSWORD" scp -o StrictHostKeyChecking=no "$0" "${PI_USER}@${pi_ip}:/tmp/raspberry_pi_nextcloud.sh" + + log_info "Running system configuration on Pi..." + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ + "echo '$PI_PASSWORD' | sudo -S bash /tmp/raspberry_pi_nextcloud.sh configure" + + log_info "Installing Nextcloud on Pi..." + auto_generate_nextcloud_password + save_config + + log_success "Nextcloud admin user: $NEXTCLOUD_ADMIN_USER" + log_success "Nextcloud admin password: $NEXTCLOUD_ADMIN_PASSWORD" + + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ + "echo '$PI_PASSWORD' | sudo -S NEXTCLOUD_ADMIN_PASSWORD='$NEXTCLOUD_ADMIN_PASSWORD' NEXTCLOUD_ADMIN_USER='$NEXTCLOUD_ADMIN_USER' bash /tmp/raspberry_pi_nextcloud.sh install-local" + + log_info "Fixing Nextcloud issues..." + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ + "echo '$PI_PASSWORD' | sudo -S bash /tmp/raspberry_pi_nextcloud.sh fix" + + log_success "========================================" + log_success "Remote Nextcloud installation complete!" + log_success "========================================" + echo + log_info "=== Access Information ===" + log_info "Nextcloud URL: https://$pi_ip" + log_info "Admin user: $NEXTCLOUD_ADMIN_USER" + log_info "Admin password: $NEXTCLOUD_ADMIN_PASSWORD" + log_info "All credentials saved in: $CONFIG_FILE" + echo + log_info "=== Trust the certificate ===" + log_info "Run: $0 install-ca" +} + +# ============================================================================= +# Install CA Certificate on Client +# ============================================================================= + +phase_install_ca() { + log_info "=== Installing Nextcloud CA Certificate ===" + + if [[ -z "$PI_PASSWORD" ]]; then + die "PI_PASSWORD not set. Run this after running install-remote or flash." + fi + + local pi_ip + pi_ip=$(discover_raspberry_pi) + + if [[ -z "$pi_ip" ]]; then + die "Failed to discover Raspberry Pi" + fi + + log_info "Downloading CA certificate from Pi..." + + local ca_file="/tmp/nextcloud-ca.crt" + + # Use SSH with sudo to cat the file (since it's in a protected directory) + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no \ + "${PI_USER}@${pi_ip}" "echo '$PI_PASSWORD' | sudo -S cat /etc/ssl/nextcloud/ca.crt" >"$ca_file" 2>/dev/null + + if [[ ! -f "$ca_file" ]] || [[ ! -s "$ca_file" ]]; then + die "Failed to download CA certificate" + fi + + log_success "CA certificate downloaded to: $ca_file" + + # Detect OS and install appropriately + if [[ -f /etc/arch-release ]]; then + log_info "Detected Arch Linux - installing CA..." + sudo cp "$ca_file" /etc/ca-certificates/trust-source/anchors/nextcloud-ca.crt + sudo trust extract-compat + log_success "CA installed in system trust store" + + elif [[ -f /etc/debian_version ]]; then + log_info "Detected Debian/Ubuntu - installing CA..." + sudo cp "$ca_file" /usr/local/share/ca-certificates/nextcloud-ca.crt + sudo update-ca-certificates + log_success "CA installed in system trust store" + + elif [[ -f /etc/redhat-release ]]; then + log_info "Detected RHEL/Fedora - installing CA..." + sudo cp "$ca_file" /etc/pki/ca-trust/source/anchors/nextcloud-ca.crt + sudo update-ca-trust + log_success "CA installed in system trust store" + + elif [[ "$(uname)" == "Darwin" ]]; then + log_info "Detected macOS - installing CA..." + sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "$ca_file" + log_success "CA installed in system keychain" + + else + log_warning "Unknown OS - please install CA manually from: $ca_file" + fi + + # Install in browser certificate stores + log_info "Installing CA in browser certificate stores..." + + # Chrome/Chromium (uses NSS) + if [[ -d ~/.pki/nssdb ]] || command -v certutil &>/dev/null; then + mkdir -p ~/.pki/nssdb + if ! certutil -d sql:~/.pki/nssdb -L 2>/dev/null | grep -q "Nextcloud"; then + # Initialize NSS db if needed + certutil -d sql:~/.pki/nssdb -N --empty-password 2>/dev/null || true + if certutil -d sql:~/.pki/nssdb -A -n "Nextcloud Home CA" -t "CT,C,C" -i "$ca_file" 2>/dev/null; then + log_success "CA installed in Chrome/Chromium" + else + log_warning "Could not install in Chrome/Chromium NSS db" + fi + else + log_info "CA already installed in Chrome/Chromium" + fi + fi + + # Firefox (has its own profile NSS databases) + if [[ -d ~/.mozilla/firefox ]]; then + local installed=0 + for profile_dir in ~/.mozilla/firefox/*.default* ~/.mozilla/firefox/*.esr*; do + if [[ -d "$profile_dir" ]]; then + if ! certutil -d sql:"$profile_dir" -L 2>/dev/null | grep -q "Nextcloud"; then + certutil -d sql:"$profile_dir" -A -n "Nextcloud Home CA" -t "CT,C,C" -i "$ca_file" 2>/dev/null && + installed=1 + else + installed=1 + fi + fi + done + if [[ $installed -eq 1 ]]; then + log_success "CA installed in Firefox" + else + log_warning "Could not install in Firefox - you may need to import manually" + fi + fi + + # Add hostname to /etc/hosts if not present + if ! grep -q "$PI_HOSTNAME" /etc/hosts 2>/dev/null; then + log_info "Adding $PI_HOSTNAME to /etc/hosts..." + echo "$pi_ip $PI_HOSTNAME ${PI_HOSTNAME}.local" | sudo tee -a /etc/hosts >/dev/null + log_success "Added $PI_HOSTNAME to /etc/hosts" + else + log_info "$PI_HOSTNAME already in /etc/hosts" + fi + + # Verify + log_info "Verifying HTTPS connection..." + if curl -s --max-time 5 "https://$PI_HOSTNAME/status.php" 2>/dev/null | grep -q "installed"; then + log_success "HTTPS connection verified - no certificate warnings!" + else + log_warning "Could not verify HTTPS - you may need to restart your browser" + fi + + log_success "========================================" + log_success "CA Certificate installed!" + log_success "========================================" + echo + log_info "Access Nextcloud at: https://$PI_HOSTNAME" + log_info "Your browser should now trust the certificate without warnings." + echo + log_info "For other devices (phones, tablets, other computers):" + log_info " Download: https://$PI_HOSTNAME/ca/nextcloud-ca.crt" + log_info " Then install the certificate in your device's trust store." +} + +# ============================================================================= +# Main +# ============================================================================= + +show_help() { + cat <<'EOF' +Nextcloud Installation Script for Raspberry Pi + +Usage: ./raspberry_pi_nextcloud.sh + +Commands: + install-remote Install Nextcloud via SSH from your laptop (recommended) + setup-ssl-remote Setup Let's Encrypt SSL with DuckDNS (auto-trusted on all devices) + install-ca Install self-signed CA on this machine (alternative to setup-ssl) + configure Configure Pi system (run on Pi) + install-local Install Nextcloud (run on Pi) + fix Fix common Nextcloud issues (run on Pi) + setup-ssl Setup Let's Encrypt SSL (run on Pi) + help Show this help message + +The script will: +1. Configure the Raspberry Pi system (SSH hardening, firewall, etc.) +2. Install Apache, PHP, MariaDB, Redis +3. Download and install Nextcloud +4. Configure caching, background jobs, and security + +For HTTPS trusted on ALL devices automatically: + ./raspberry_pi_nextcloud.sh install-remote + ./raspberry_pi_nextcloud.sh setup-ssl-remote + + This uses DuckDNS (free) + Let's Encrypt for real trusted certificates. + Go to https://www.duckdns.org/ to get your free domain first. + +For self-signed certificates (requires manual CA install on each device): + ./raspberry_pi_nextcloud.sh install-remote + ./raspberry_pi_nextcloud.sh install-ca + +For local installation (on Pi): + sudo ./raspberry_pi_nextcloud.sh configure + sudo ./raspberry_pi_nextcloud.sh install-local + sudo ./raspberry_pi_nextcloud.sh fix + sudo ./raspberry_pi_nextcloud.sh setup-ssl +EOF +} + +main() { + local command="${1:-help}" + + case "$command" in + install-remote) + phase_install_remote + ;; + setup-ssl-remote) + phase_setup_ssl_remote + ;; + setup-ssl) + phase_setup_ssl + ;; + install-ca) + phase_install_ca + ;; + configure) + phase_configure_system + ;; + install-local | install) + phase_install_nextcloud + ;; + fix) + phase_fix_issues + ;; + help | --help | -h) + show_help + ;; + *) + log_error "Unknown command: $command" + show_help + exit 1 + ;; + esac +} + +main "$@"