Files
ubuntu-post-install/caddy-setup-helper.sh
T
Claude f564b4b6d8 Remove Keycloak; standardize on Authelia for SSO
Keycloak never reliably ran (fiddly reverse-proxy/hostname config) and the
repo has standardized on Authelia for SSO + 2FA. This rips Keycloak out
entirely:

- Delete the install block, whiptail menu entry, uninstall plumbing, and
  EXISTING_SERVICES detection from both 24.04 and 26.04 scripts
- Delete docker-compose-keycloak.yml, fix-keycloak-proxy.sh, and
  KEYCLOAK-SETUP-GUIDE.md
- Remove the Keycloak block from caddy-setup-helper.sh
- Update docs (CADDY-FAIL2BAN-SETUP.md, SECURITY-IMPROVEMENTS.md,
  NEW-SCRIPT-STRUCTURE.md, SCRIPT-FLOW-INTRO.txt) to reference Authelia

Also documents the fail2ban/Authelia overlap: Authelia handles failed-login
regulation (per-account lockout); the Caddy fail2ban jail is complementary
firewall-level IP banning. Neither does geo-blocking — noted CrowdSec / Caddy
GeoIP as the path for that.

https://claude.ai/code/session_017eA2qqq9jfF2tNtpUYL8vK
2026-06-03 03:44:26 +00:00

512 lines
18 KiB
Bash
Executable File

#!/bin/bash
# Caddy Setup Helper Script
# This script helps manage Caddy configuration, backups, and fail2ban integration
# for dockerized Caddy setups - FULLY AUTOMATED with error handling
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo "==============================================="
echo " Caddy Configuration & Fail2ban Setup Helper"
echo "==============================================="
echo ""
# Function to print colored output
print_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Function to ask yes/no questions
ask_yn() {
local prompt="$1"
local default="${2:-n}"
local response
if [ "$default" = "y" ]; then
read -p "$prompt [Y/n]: " response
response=${response:-y}
else
read -p "$prompt [y/N]: " response
response=${response:-n}
fi
[[ "$response" =~ ^[Yy]$ ]]
}
# Track if we need to show manual instructions
SHOW_MANUAL=false
ERROR_MESSAGES=()
# ==============================
# 1. CHECK IF CADDY IS INSTALLED
# ==============================
print_info "Checking for Caddy installation..."
CADDY_CONTAINER=""
CADDYFILE_PATH=""
# Check if Docker is available
if ! command -v docker &> /dev/null; then
print_error "Docker is not installed or not in PATH"
exit 1
fi
# Try to find Caddy container
if docker ps --format '{{.Names}}' | grep -iq "caddy"; then
CADDY_CONTAINER=$(docker ps --format '{{.Names}}' | grep -i "caddy" | head -1)
print_success "Found running Caddy container: $CADDY_CONTAINER"
else
print_warning "No running Caddy container found"
if ! ask_yn "Is Caddy installed?" "n"; then
print_info "Caddy is not installed. Please install Caddy first."
echo ""
echo "To install Caddy with Docker, see CADDY-FAIL2BAN-SETUP.md"
exit 0
fi
fi
# ==============================
# 2. LOCATE CADDYFILE
# ==============================
print_info "Locating Caddyfile..."
# Common Caddyfile locations
POSSIBLE_PATHS=(
"$HOME/docker/caddy/Caddyfile"
"$HOME/docker/caddy/caddyfile"
"$HOME/docker/caddy/config/Caddyfile"
"/etc/caddy/Caddyfile"
)
for path in "${POSSIBLE_PATHS[@]}"; do
if [ -f "$path" ]; then
CADDYFILE_PATH="$path"
print_success "Found Caddyfile at: $CADDYFILE_PATH"
break
fi
done
if [ -z "$CADDYFILE_PATH" ]; then
print_warning "Caddyfile not found in common locations"
read -p "Enter path to Caddyfile: " CUSTOM_PATH
if [ -n "$CUSTOM_PATH" ] && [ -f "$CUSTOM_PATH" ]; then
CADDYFILE_PATH="$CUSTOM_PATH"
print_success "Using Caddyfile at: $CADDYFILE_PATH"
else
print_error "Cannot proceed without Caddyfile location"
exit 1
fi
fi
CADDY_DIR=$(dirname "$CADDYFILE_PATH")
# ==============================
# 3. BACKUP CADDYFILE (ALWAYS FIRST!)
# ==============================
print_info "Creating backup of Caddyfile..."
BACKUP_DIR="$CADDY_DIR/backups"
mkdir -p "$BACKUP_DIR"
BACKUP_FILE="$BACKUP_DIR/Caddyfile.backup.$(date +%Y%m%d_%H%M%S)"
cp "$CADDYFILE_PATH" "$BACKUP_FILE"
print_success "Backup created: $BACKUP_FILE"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " BACKUP RESTORE INSTRUCTIONS"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "To restore this backup:"
echo " cp $BACKUP_FILE $CADDYFILE_PATH"
if [ -n "$CADDY_CONTAINER" ]; then
echo " docker exec -w /etc/caddy $CADDY_CONTAINER caddy reload"
echo " docker exec -w /etc/caddy $CADDY_CONTAINER caddy fmt --overwrite"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# ==============================
# 4. CHECK FOR FAIL2BAN INSTALLATION
# ==============================
print_info "Checking for fail2ban installation..."
FAIL2BAN_INSTALLED=false
if command -v fail2ban-client &> /dev/null; then
print_success "fail2ban is installed"
FAIL2BAN_INSTALLED=true
else
print_warning "fail2ban is not installed"
if ask_yn "Would you like to install fail2ban now?" "y"; then
print_info "Installing fail2ban..."
if sudo apt update && sudo apt install -y fail2ban; then
print_success "fail2ban installed successfully"
FAIL2BAN_INSTALLED=true
else
print_error "Failed to install fail2ban"
ERROR_MESSAGES+=("Failed to install fail2ban - you may need to install it manually")
SHOW_MANUAL=true
fi
else
print_warning "Skipping fail2ban installation"
SHOW_MANUAL=true
fi
fi
# ==============================
# 5. CREATE LOG DIRECTORY
# ==============================
if [ "$FAIL2BAN_INSTALLED" = true ]; then
print_info "Checking Caddy log directory..."
LOG_DIR="/var/log/caddy"
if [ ! -d "$LOG_DIR" ]; then
if ask_yn "Create $LOG_DIR for Caddy logs?" "y"; then
if sudo mkdir -p "$LOG_DIR" && sudo chmod 755 "$LOG_DIR"; then
print_success "Log directory created: $LOG_DIR"
else
print_error "Failed to create log directory"
ERROR_MESSAGES+=("Failed to create $LOG_DIR - create it manually with: sudo mkdir -p $LOG_DIR && sudo chmod 755 $LOG_DIR")
SHOW_MANUAL=true
fi
fi
else
print_success "Log directory exists: $LOG_DIR"
fi
fi
# ==============================
# 6. CHECK CADDY DOCKER COMPOSE FOR LOG VOLUME
# ==============================
if [ "$FAIL2BAN_INSTALLED" = true ] && [ -n "$CADDY_CONTAINER" ]; then
print_info "Checking if Caddy container has log volume mounted..."
# Check if the container has /var/log/caddy mounted
if docker inspect "$CADDY_CONTAINER" 2>/dev/null | grep -q "/var/log/caddy"; then
print_success "Caddy container has log volume mounted"
else
print_warning "Caddy container does not have /var/log/caddy volume mounted"
# Check if there's a docker-compose.yml
COMPOSE_FILE=""
for file in "$CADDY_DIR/docker-compose.yml" "$CADDY_DIR/docker-compose.yaml"; do
if [ -f "$file" ]; then
COMPOSE_FILE="$file"
break
fi
done
if [ -n "$COMPOSE_FILE" ]; then
if ask_yn "Add /var/log/caddy volume to docker-compose.yml?" "y"; then
# Backup docker-compose.yml
cp "$COMPOSE_FILE" "$COMPOSE_FILE.backup.$(date +%Y%m%d_%H%M%S)"
# Check if volumes section exists
if grep -q "volumes:" "$COMPOSE_FILE"; then
# Add to existing volumes
if ! grep -q "/var/log/caddy" "$COMPOSE_FILE"; then
# Find the volumes section and add our volume
sed -i '/volumes:/a\ - /var/log/caddy:/var/log/caddy' "$COMPOSE_FILE"
print_success "Added log volume to docker-compose.yml"
print_warning "You'll need to restart the Caddy container for this to take effect"
if ask_yn "Restart Caddy container now?" "n"; then
cd "$CADDY_DIR"
if docker compose down && docker compose up -d; then
print_success "Caddy container restarted"
# Update CADDY_CONTAINER name in case it changed
CADDY_CONTAINER=$(docker ps --format '{{.Names}}' | grep -i "caddy" | head -1)
else
print_error "Failed to restart Caddy container"
ERROR_MESSAGES+=("Failed to restart Caddy - restart manually with: cd $CADDY_DIR && docker compose restart")
fi
fi
fi
else
print_warning "Could not automatically add volume - docker-compose.yml format is unexpected"
ERROR_MESSAGES+=("Add this volume manually to your Caddy service: /var/log/caddy:/var/log/caddy")
SHOW_MANUAL=true
fi
fi
else
print_warning "No docker-compose.yml found - you may need to add the volume manually"
ERROR_MESSAGES+=("Add log volume to Caddy container: /var/log/caddy:/var/log/caddy")
SHOW_MANUAL=true
fi
fi
fi
# ==============================
# 7. CREATE FAIL2BAN FILTER
# ==============================
if [ "$FAIL2BAN_INSTALLED" = true ]; then
print_info "Checking fail2ban filter configuration..."
FILTER_FILE="/etc/fail2ban/filter.d/caddy-auth.conf"
if [ -f "$FILTER_FILE" ]; then
print_success "fail2ban filter already exists: $FILTER_FILE"
else
if ask_yn "Create fail2ban filter for Caddy?" "y"; then
print_info "Creating fail2ban filter..."
FILTER_CONTENT='[Definition]
failregex = ^.*"remote_ip":"<HOST>".*"status":(?:401|403|429).*$
^.*"remote_addr":"<HOST>.*"status":(?:401|403|429).*$
ignoreregex = ^.*"remote_ip":"(?:127\.0\.0\.1|::1)".*$
datepattern = "ts":%%s'
if echo "$FILTER_CONTENT" | sudo tee "$FILTER_FILE" > /dev/null; then
print_success "Created fail2ban filter: $FILTER_FILE"
else
print_error "Failed to create fail2ban filter"
ERROR_MESSAGES+=("Failed to create $FILTER_FILE - create it manually (see CADDY-FAIL2BAN-SETUP.md)")
SHOW_MANUAL=true
fi
fi
fi
fi
# ==============================
# 8. CREATE FAIL2BAN JAIL
# ==============================
if [ "$FAIL2BAN_INSTALLED" = true ]; then
print_info "Checking fail2ban jail configuration..."
JAIL_FILE="/etc/fail2ban/jail.d/caddy.conf"
if [ -f "$JAIL_FILE" ]; then
print_success "fail2ban jail already exists: $JAIL_FILE"
else
if ask_yn "Create fail2ban jail for Caddy?" "y"; then
print_info "Creating fail2ban jail..."
# Ask for custom settings
echo ""
print_info "Fail2ban jail settings (press Enter for defaults):"
read -p " Max retries before ban [5]: " MAXRETRY
MAXRETRY=${MAXRETRY:-5}
read -p " Find time window in seconds [600]: " FINDTIME
FINDTIME=${FINDTIME:-600}
read -p " Ban duration in seconds [3600]: " BANTIME
BANTIME=${BANTIME:-3600}
JAIL_CONTENT="[caddy-auth]
enabled = true
port = http,https
filter = caddy-auth
logpath = /var/log/caddy/access.log
/var/log/caddy/*-access.log
maxretry = $MAXRETRY
findtime = $FINDTIME
bantime = $BANTIME
action = iptables-multiport[name=CaddyAuth, port=\"http,https\", protocol=tcp]
backend = auto"
if echo "$JAIL_CONTENT" | sudo tee "$JAIL_FILE" > /dev/null; then
print_success "Created fail2ban jail: $JAIL_FILE"
else
print_error "Failed to create fail2ban jail"
ERROR_MESSAGES+=("Failed to create $JAIL_FILE - create it manually (see CADDY-FAIL2BAN-SETUP.md)")
SHOW_MANUAL=true
fi
fi
fi
fi
# ==============================
# 9. TEST FAIL2BAN CONFIGURATION
# ==============================
if [ "$FAIL2BAN_INSTALLED" = true ]; then
print_info "Testing fail2ban configuration..."
if sudo fail2ban-client -t &> /dev/null; then
print_success "fail2ban configuration is valid"
else
print_error "fail2ban configuration has errors"
ERROR_MESSAGES+=("fail2ban configuration is invalid - check with: sudo fail2ban-client -t")
SHOW_MANUAL=true
fi
fi
# ==============================
# 10. RESTART FAIL2BAN
# ==============================
if [ "$FAIL2BAN_INSTALLED" = true ]; then
if ask_yn "Restart fail2ban to apply changes?" "y"; then
print_info "Restarting fail2ban..."
if sudo systemctl restart fail2ban; then
print_success "fail2ban restarted successfully"
# Wait a moment for fail2ban to start
sleep 2
# Check if caddy-auth jail is running
if sudo fail2ban-client status caddy-auth &> /dev/null; then
print_success "caddy-auth jail is active"
echo ""
print_info "Jail status:"
sudo fail2ban-client status caddy-auth
else
print_warning "caddy-auth jail is not active"
ERROR_MESSAGES+=("caddy-auth jail failed to start - check with: sudo fail2ban-client status")
SHOW_MANUAL=true
fi
else
print_error "Failed to restart fail2ban"
ERROR_MESSAGES+=("Failed to restart fail2ban - check logs with: sudo journalctl -u fail2ban -n 50")
SHOW_MANUAL=true
fi
fi
fi
# ==============================
# 11. ADD SERVICE CONFIGURATIONS TO CADDYFILE
# ==============================
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " ADDING NEW SERVICES TO CADDYFILE"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
print_info "You can now add your services to the Caddyfile"
echo ""
echo "Available services to add:"
echo " - ActualBudget (Personal Finance) - Port 5006"
echo ""
if ask_yn "Would you like to add ActualBudget to Caddyfile?" "n"; then
read -p "Enter domain for ActualBudget (e.g., budget.yourdomain.com): " AB_DOMAIN
if [ -n "$AB_DOMAIN" ]; then
AB_CONFIG="
# ActualBudget - Personal Finance
$AB_DOMAIN {
log {
output file /var/log/caddy/actualbudget-access.log
format json
level INFO
}
reverse_proxy localhost:5006
# Security headers
header {
Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\"
X-Frame-Options \"SAMEORIGIN\"
X-Content-Type-Options \"nosniff\"
X-XSS-Protection \"1; mode=block\"
Referrer-Policy \"strict-origin-when-cross-origin\"
}
}
"
if echo "$AB_CONFIG" >> "$CADDYFILE_PATH"; then
print_success "Added ActualBudget configuration to Caddyfile"
else
print_error "Failed to add ActualBudget configuration"
ERROR_MESSAGES+=("Add ActualBudget manually - see CADDY-FAIL2BAN-SETUP.md")
fi
fi
fi
# ==============================
# 12. VALIDATE AND RELOAD CADDY
# ==============================
if [ -n "$CADDY_CONTAINER" ]; then
echo ""
if ask_yn "Validate and reload Caddy configuration?" "y"; then
print_info "Validating Caddyfile..."
# Format first
if docker exec -w /etc/caddy "$CADDY_CONTAINER" caddy fmt --overwrite 2>/dev/null; then
print_success "Caddyfile formatted"
fi
# Validate
if docker exec "$CADDY_CONTAINER" caddy validate --config /etc/caddy/Caddyfile 2>/dev/null; then
print_success "Caddyfile is valid"
# Reload
print_info "Reloading Caddy configuration..."
if docker exec -w /etc/caddy "$CADDY_CONTAINER" caddy reload 2>/dev/null; then
print_success "Caddy configuration reloaded successfully"
else
print_error "Failed to reload Caddy configuration"
ERROR_MESSAGES+=("Failed to reload Caddy - check logs with: docker logs $CADDY_CONTAINER")
SHOW_MANUAL=true
fi
else
print_error "Caddyfile validation failed"
ERROR_MESSAGES+=("Caddyfile has syntax errors - check with: docker exec $CADDY_CONTAINER caddy validate --config /etc/caddy/Caddyfile")
SHOW_MANUAL=true
fi
fi
fi
# ==============================
# 13. FINAL SUMMARY
# ==============================
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " SETUP COMPLETE"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
if [ "$SHOW_MANUAL" = true ]; then
print_warning "Some steps could not be completed automatically"
echo ""
echo "Issues encountered:"
for msg in "${ERROR_MESSAGES[@]}"; do
echo " - $msg"
done
echo ""
print_info "See CADDY-FAIL2BAN-SETUP.md for manual setup instructions"
echo ""
fi
print_success "Caddyfile backed up to: $BACKUP_FILE"
if [ "$FAIL2BAN_INSTALLED" = true ]; then
print_success "fail2ban is installed and configured"
echo ""
echo "Useful commands:"
echo " Check jail status: sudo fail2ban-client status caddy-auth"
echo " View banned IPs: sudo fail2ban-client get caddy-auth banip"
echo " Unban IP: sudo fail2ban-client set caddy-auth unbanip 1.2.3.4"
echo " Test filter: sudo fail2ban-regex /var/log/caddy/access.log /etc/fail2ban/filter.d/caddy-auth.conf"
fi
echo ""
echo "Caddyfile location: $CADDYFILE_PATH"
echo "Backup location: $BACKUP_FILE"
if [ -n "$CADDY_CONTAINER" ]; then
echo "Caddy container: $CADDY_CONTAINER"
echo "Reload Caddy: docker exec -w /etc/caddy $CADDY_CONTAINER caddy reload"
echo "View Caddy logs: docker logs $CADDY_CONTAINER --tail 50"
fi
echo ""
print_success "All done!"
echo ""