From 6d66441a69cb88ac0b05e6dda28f889faef902db Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Jan 2026 23:04:24 +0000 Subject: [PATCH 1/4] Integrate Caddy and fail2ban into main ubuntu-post-install.sh script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users can now install and configure everything by simply running the main script. Re-running the script allows adding new services to existing installations. NEW SERVICES IN MAIN SCRIPT: CADDY WEB SERVER: - Automatic HTTPS with Let's Encrypt - Reverse proxy for all services - Creates example Caddyfile with ActualBudget and Keycloak configs - Detects existing installations (asks before reconfiguring) - Automatically backs up existing Caddyfile before changes - Pre-configured with /var/log/caddy volume for fail2ban integration - Includes HTTP/3 support FAIL2BAN INTRUSION PREVENTION: - Automated installation via apt - Creates Caddy filter for JSON logs (401, 403, 429 status codes) - Creates Caddy jail with configurable settings - Automatically creates /var/log/caddy directory - Tests configuration before restart - Verifies jail is active after restart - Shows status and useful commands FEATURES: ✅ Detects if services already exist (won't overwrite) ✅ Backs up configurations before changes ✅ Interactive prompts for all settings ✅ Validates configurations before applying ✅ Can be re-run to add services to existing setup ✅ Works alongside existing services ✅ Follows same pattern as ActualBudget/Keycloak WORKFLOW: 1. Run ubuntu-post-install.sh 2. Select services to install (ActualBudget, Keycloak, Caddy, fail2ban, etc.) 3. Script handles everything automatically 4. Re-run anytime to add more services The caddy-setup-helper.sh remains available as a standalone tool for advanced configuration, but the main script is now the primary method. --- ubuntu-post-install.sh | 268 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 268 insertions(+) diff --git a/ubuntu-post-install.sh b/ubuntu-post-install.sh index dd3ecb2..cfaa9e4 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -3051,6 +3051,274 @@ KC_COMPOSE fi fi + # ---- CADDY WEB SERVER ---- + echo "" + echo "┌─────────────────────────────────────────────────────────────────┐" + echo "│ CADDY - Modern Web Server & Reverse Proxy │" + echo "│ Automatic HTTPS, reverse proxy for all your services │" + echo "│ Port: 80 (HTTP), 443 (HTTPS) │" + echo "└─────────────────────────────────────────────────────────────────┘" + prompt_yn "Install Caddy reverse proxy? (y/n):" "n" INSTALL_CADDY + + if [ "$INSTALL_CADDY" = "y" ] || [ "$INSTALL_CADDY" = "Y" ]; then + CADDY_DIR="$DOCKER_DIR/caddy" + + # Check if Caddy is already installed + if [ -f "$CADDY_DIR/Caddyfile" ] || [ -f "$CADDY_DIR/docker-compose.yml" ]; then + echo "" + echo "⚠ Caddy appears to be already installed at $CADDY_DIR" + prompt_yn "Do you want to reconfigure it? (y/n):" "n" RECONFIGURE_CADDY + if [ "$RECONFIGURE_CADDY" != "y" ] && [ "$RECONFIGURE_CADDY" != "Y" ]; then + echo " Skipping Caddy installation" + INSTALL_CADDY="n" + fi + fi + + if [ "$INSTALL_CADDY" = "y" ] || [ "$INSTALL_CADDY" = "Y" ]; then + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $CADDY_DIR" + else + echo "Installing Caddy..." + mkdir -p "$CADDY_DIR/data" "$CADDY_DIR/config" + + # Backup existing Caddyfile if it exists + if [ -f "$CADDY_DIR/Caddyfile" ]; then + mkdir -p "$CADDY_DIR/backups" + BACKUP_FILE="$CADDY_DIR/backups/Caddyfile.backup.$(date +%Y%m%d_%H%M%S)" + cp "$CADDY_DIR/Caddyfile" "$BACKUP_FILE" + echo " ✓ Backed up existing Caddyfile to: $BACKUP_FILE" + fi + + cd "$CADDY_DIR" + + cat > docker-compose.yml << 'CADDY_COMPOSE' +name: caddy + +services: + caddy: + image: caddy:latest + container_name: caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" # HTTP/3 + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + - ./data:/data + - ./config:/config + - /var/log/caddy:/var/log/caddy + environment: + - ACME_AGREE=true + labels: + - "io.podman.annotations.label/fail2ban.enable=true" +CADDY_COMPOSE + + # Create Caddyfile if it doesn't exist + if [ ! -f "Caddyfile" ]; then + cat > Caddyfile << 'CADDYFILE' +{ + # Global options + admin off + # Email for Let's Encrypt notifications + # email admin@yourdomain.com +} + +# Example configuration - edit this for your services +# Uncomment and modify these examples: + +# ActualBudget +# budget.yourdomain.com { +# log { +# output file /var/log/caddy/actualbudget-access.log +# format json +# level INFO +# } +# reverse_proxy localhost:5006 +# 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" +# } +# } + +# Keycloak +# auth.yourdomain.com { +# log { +# output file /var/log/caddy/keycloak-access.log +# format json +# level INFO +# } +# reverse_proxy localhost:8180 +# 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" +# } +# } + +# Add more services here... +CADDYFILE + echo " ✓ Created example Caddyfile" + else + echo " ℹ Using existing Caddyfile" + fi + + echo " ✓ Caddy configured at $CADDY_DIR" + + prompt_yn "Start Caddy now? (y/n):" "y" START_CADDY + if [ "$START_CADDY" = "y" ] || [ "$START_CADDY" = "Y" ]; then + docker compose up -d 2>/dev/null && echo " ✓ Caddy started" || echo " ⚠ Failed to start Caddy" + fi + + echo "" + echo " Configuration file: $CADDY_DIR/Caddyfile" + echo " Edit Caddyfile to add your domains and services" + echo " Reload config: cd $CADDY_DIR && docker exec -w /etc/caddy caddy caddy reload" + echo "" + echo " ⚠ IMPORTANT: Edit the Caddyfile to configure your domains!" + echo " - Uncomment and modify the example configurations" + echo " - Add your domain names" + echo " - Configure services you want to expose" + echo "" + fi + fi + fi + + # ---- FAIL2BAN ---- + echo "" + echo "┌─────────────────────────────────────────────────────────────────┐" + echo "│ FAIL2BAN - Intrusion Prevention System │" + echo "│ Automatically ban IPs with failed auth attempts │" + echo "│ Protects SSH, Caddy, and other services │" + echo "└─────────────────────────────────────────────────────────────────┘" + prompt_yn "Install and configure fail2ban? (y/n):" "n" INSTALL_FAIL2BAN + + if [ "$INSTALL_FAIL2BAN" = "y" ] || [ "$INSTALL_FAIL2BAN" = "Y" ]; then + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would install fail2ban" + else + echo "Installing fail2ban..." + + # Check if fail2ban is already installed + if command -v fail2ban-client &> /dev/null; then + echo " ✓ fail2ban is already installed" + else + echo " Installing fail2ban package..." + if sudo apt update && sudo apt install -y fail2ban; then + echo " ✓ fail2ban installed successfully" + else + echo " ⚠ Failed to install fail2ban" + echo " You may need to install it manually: sudo apt install fail2ban" + fi + fi + + # Create log directory for Caddy + if [ ! -d "/var/log/caddy" ]; then + sudo mkdir -p /var/log/caddy + sudo chmod 755 /var/log/caddy + echo " ✓ Created /var/log/caddy directory" + fi + + # Check if Caddy filter exists + FILTER_FILE="/etc/fail2ban/filter.d/caddy-auth.conf" + if [ ! -f "$FILTER_FILE" ]; then + echo " Creating fail2ban filter for Caddy..." + + FILTER_CONTENT='[Definition] +failregex = ^.*"remote_ip":"".*"status":(?:401|403|429).*$ + ^.*"remote_addr":".*"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 + echo " ✓ Created Caddy fail2ban filter" + else + echo " ⚠ Failed to create filter - you may need to create it manually" + fi + else + echo " ✓ Caddy fail2ban filter already exists" + fi + + # Check if Caddy jail exists + JAIL_FILE="/etc/fail2ban/jail.d/caddy.conf" + if [ ! -f "$JAIL_FILE" ]; then + echo " Creating fail2ban jail for Caddy..." + echo "" + echo " Configure fail2ban settings (press Enter for defaults):" + + prompt_text " Max retries before ban:" "5" F2B_MAXRETRY + prompt_text " Find time window (seconds):" "600" F2B_FINDTIME + prompt_text " Ban duration (seconds):" "3600" F2B_BANTIME + + JAIL_CONTENT="[caddy-auth] +enabled = true +port = http,https +filter = caddy-auth +logpath = /var/log/caddy/access.log + /var/log/caddy/*-access.log +maxretry = $F2B_MAXRETRY +findtime = $F2B_FINDTIME +bantime = $F2B_BANTIME +action = iptables-multiport[name=CaddyAuth, port=\"http,https\", protocol=tcp] +backend = auto" + + if echo "$JAIL_CONTENT" | sudo tee "$JAIL_FILE" > /dev/null; then + echo " ✓ Created Caddy fail2ban jail" + else + echo " ⚠ Failed to create jail - you may need to create it manually" + fi + else + echo " ✓ Caddy fail2ban jail already exists" + fi + + # Test fail2ban configuration + echo "" + echo " Testing fail2ban configuration..." + if sudo fail2ban-client -t &> /dev/null; then + echo " ✓ fail2ban configuration is valid" + else + echo " ⚠ fail2ban configuration has errors" + echo " Check with: sudo fail2ban-client -t" + fi + + # Restart fail2ban + prompt_yn "Restart fail2ban to apply changes? (y/n):" "y" RESTART_F2B + if [ "$RESTART_F2B" = "y" ] || [ "$RESTART_F2B" = "Y" ]; then + if sudo systemctl restart fail2ban; then + echo " ✓ fail2ban restarted successfully" + + # Wait for fail2ban to start + sleep 2 + + # Check jail status + if sudo fail2ban-client status caddy-auth &> /dev/null; then + echo " ✓ caddy-auth jail is active" + echo "" + sudo fail2ban-client status caddy-auth + else + echo " ⚠ caddy-auth jail is not active (may need Caddy logs to exist first)" + fi + else + echo " ⚠ Failed to restart fail2ban" + echo " Check logs: sudo journalctl -u fail2ban -n 50" + fi + fi + + 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 " View logs: sudo tail -f /var/log/fail2ban.log" + echo "" + fi + fi + # ---- LYRION MUSIC SERVER ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" From 3ff80aee94d792bf2dae71849f066e8bfbf2d431 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Jan 2026 23:32:45 +0000 Subject: [PATCH 2/4] Add whiptail service selection menu for Docker applications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users now get a nice checkbox menu to select which services to install, instead of being prompted for each service one-by-one. WHIPTAIL MENU: - Displays all 24+ Docker services in a single checklist - Use SPACE to select/deselect services - Press ENTER to confirm and install selected services - Falls back to individual prompts if whiptail not available SERVICES IN MENU: ✓ Immich (Photo & Video Backup) ✓ AudioBookshelf (Audiobooks & Podcasts) ✓ Emby (Media Server) ✓ A.R.M. (Automatic Ripping Machine) ✓ FileBrowser (Web File Manager) ✓ Magic Mirror (Smart Mirror Display) ✓ ActualBudget (Personal Finance) ✓ Keycloak (Identity & Access Management) ✓ Caddy (Reverse Proxy with Auto-HTTPS) ✓ fail2ban (Intrusion Prevention) ✓ Lyrion (Music Streaming) ✓ Mealie (Recipe Manager) ✓ Minecraft (Game Server) ✓ Jellyfin (Free Media Server) ✓ Frigate (AI NVR for Cameras) ✓ Ntfy (Push Notifications) ✓ Uptime Kuma (Service Monitoring) ✓ WG-Easy (WireGuard VPN) ✓ Traccar (GPS Tracking) ✓ Portainer (Docker Web UI) ✓ MeshCentral (Remote Management) ✓ FindMyDevice (Device Tracking) ✓ Frigate-Notify (Frigate Notifications) ✓ Watchtower (Auto Container Updates) WORKFLOW: 1. Run ubuntu-post-install.sh 2. Get whiptail menu for service selection 3. Select services with SPACE 4. Press ENTER to install 5. Script installs only selected services FALLBACK: - If whiptail not available, uses traditional prompts - Prompts only appear if service wasn't selected in menu - Fully backwards compatible This dramatically improves UX for installing multiple services! --- ubuntu-post-install.sh | 169 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 150 insertions(+), 19 deletions(-) diff --git a/ubuntu-post-install.sh b/ubuntu-post-install.sh index cfaa9e4..04fa85b 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -2172,14 +2172,115 @@ else chown "$ACTUAL_USER:$ACTUAL_USER" "$DOCKER_DIR" fi + # ============================================================================ + # SERVICE SELECTION MENU + # ============================================================================ + + # Use whiptail for service selection if available + if command -v whiptail &> /dev/null; then + # Build checklist of all available services + SELECTED_SERVICES=$(whiptail --title "Select Docker Services to Install" \ + --checklist "Use SPACE to select, ENTER to confirm:" 25 78 17 \ + "IMMICH" "Photo & video backup (like Google Photos)" OFF \ + "AUDIOBOOKSHELF" "Audiobook & podcast server" OFF \ + "EMBY" "Media server for movies, TV, music" OFF \ + "ARM" "Automatic Ripping Machine for DVDs/Blu-rays" OFF \ + "FILEBROWSER" "Web-based file manager" OFF \ + "MAGICMIRROR" "Smart mirror / dashboard display" OFF \ + "ACTUALBUDGET" "Personal finance management with bank sync" OFF \ + "KEYCLOAK" "Identity & Access Management (SSO)" OFF \ + "CADDY" "Reverse proxy with automatic HTTPS" OFF \ + "FAIL2BAN" "Intrusion prevention system" OFF \ + "LYRION" "Music streaming server (LMS)" OFF \ + "MEALIE" "Recipe manager & meal planner" OFF \ + "MINECRAFT" "Minecraft game server" OFF \ + "JELLYFIN" "Free media server (Emby alternative)" OFF \ + "FRIGATE" "AI-powered NVR for security cameras" OFF \ + "NTFY" "Push notifications server" OFF \ + "UPTIMEKUMA" "Service monitoring dashboard" OFF \ + "WGEASY" "WireGuard VPN with web UI" OFF \ + "TRACCAR" "GPS tracking server" OFF \ + "PORTAINER" "Docker management web UI" OFF \ + "MESHCENTRAL" "Remote management server" OFF \ + "FINDMYDEVICE" "Device tracking (like Find My)" OFF \ + "FRIGATE_NOTIFY" "Push notifications for Frigate" OFF \ + "WATCHTOWER" "Automatic container updates" OFF \ + 3>&1 1>&2 2>&3) + + # Check if user cancelled + if [ $? -ne 0 ]; then + echo "Service selection cancelled. Skipping Docker applications." + SELECTED_SERVICES="" + fi + + # Parse selections (whiptail returns quoted strings) + INSTALL_IMMICH="n" + INSTALL_AUDIOBOOKSHELF="n" + INSTALL_EMBY="n" + INSTALL_ARM="n" + INSTALL_FILEBROWSER="n" + INSTALL_MAGICMIRROR="n" + INSTALL_ACTUALBUDGET="n" + INSTALL_KEYCLOAK="n" + INSTALL_CADDY="n" + INSTALL_FAIL2BAN="n" + INSTALL_LMS="n" + INSTALL_MEALIE="n" + INSTALL_MINECRAFT="n" + INSTALL_JELLYFIN="n" + INSTALL_FRIGATE="n" + INSTALL_NTFY="n" + INSTALL_UPTIMEKUMA="n" + INSTALL_WGEASY="n" + INSTALL_TRACCAR="n" + INSTALL_PORTAINER="n" + INSTALL_MESHCENTRAL_SERVER="n" + INSTALL_FMD="n" + INSTALL_FRIGATE_NOTIFY="n" + INSTALL_WATCHTOWER="n" + + # Set installation flags based on selections + if echo "$SELECTED_SERVICES" | grep -q "IMMICH"; then INSTALL_IMMICH="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "AUDIOBOOKSHELF"; then INSTALL_AUDIOBOOKSHELF="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "EMBY"; then INSTALL_EMBY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "ARM"; then INSTALL_ARM="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FILEBROWSER"; then INSTALL_FILEBROWSER="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "MAGICMIRROR"; then INSTALL_MAGICMIRROR="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "ACTUALBUDGET"; then INSTALL_ACTUALBUDGET="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "KEYCLOAK"; then INSTALL_KEYCLOAK="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "CADDY"; then INSTALL_CADDY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FAIL2BAN"; then INSTALL_FAIL2BAN="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "LYRION"; then INSTALL_LMS="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "MEALIE"; then INSTALL_MEALIE="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "MINECRAFT"; then INSTALL_MINECRAFT="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "JELLYFIN"; then INSTALL_JELLYFIN="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FRIGATE\""; then INSTALL_FRIGATE="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "NTFY"; then INSTALL_NTFY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "UPTIMEKUMA"; then INSTALL_UPTIMEKUMA="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "WGEASY"; then INSTALL_WGEASY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "TRACCAR"; then INSTALL_TRACCAR="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "PORTAINER"; then INSTALL_PORTAINER="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "MESHCENTRAL"; then INSTALL_MESHCENTRAL_SERVER="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FINDMYDEVICE"; then INSTALL_FMD="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FRIGATE_NOTIFY"; then INSTALL_FRIGATE_NOTIFY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "WATCHTOWER"; then INSTALL_WATCHTOWER="y"; fi + + echo "" + echo "Selected services:" + echo "$SELECTED_SERVICES" | tr '"' '\n' | grep -v '^$' | sed 's/^/ - /' + echo "" + fi + # ---- IMMICH ---- - echo "" - echo "┌─────────────────────────────────────────────────────────────────┐" - echo "│ IMMICH - Self-hosted photo & video backup │" - echo "│ Like Google Photos but private. Mobile app auto-uploads. │" - echo "│ Port: 2283 │" - echo "└─────────────────────────────────────────────────────────────────┘" - prompt_yn "Install Immich? (y/n):" "n" INSTALL_IMMICH + if [ -z "$INSTALL_IMMICH" ]; then + echo "" + echo "┌─────────────────────────────────────────────────────────────────┐" + echo "│ IMMICH - Self-hosted photo & video backup │" + echo "│ Like Google Photos but private. Mobile app auto-uploads. │" + echo "│ Port: 2283 │" + echo "└─────────────────────────────────────────────────────────────────┘" + prompt_yn "Install Immich? (y/n):" "n" INSTALL_IMMICH + fi if [ "$INSTALL_IMMICH" = "y" ] || [ "$INSTALL_IMMICH" = "Y" ]; then echo "Installing Immich..." @@ -2449,7 +2550,9 @@ ABS_ENV echo "│ Stream your media library to any device. │" echo "│ Port: 8096 (web), 8920 (https) │" echo "└─────────────────────────────────────────────────────────────────┘" + if [ -z "$INSTALL_EMBY" ]; then prompt_yn "Install Emby? (y/n):" "n" INSTALL_EMBY + fi if [ "$INSTALL_EMBY" = "y" ] || [ "$INSTALL_EMBY" = "Y" ]; then echo "Installing Emby..." @@ -2514,7 +2617,9 @@ EMBY_ENV echo "│ Automatically rip DVDs, Blu-rays, and CDs. │" echo "│ Port: 8080 │" echo "└─────────────────────────────────────────────────────────────────┘" + if [ -z "$INSTALL_ARM" ]; then prompt_yn "Install A.R.M.? (y/n):" "n" INSTALL_ARM + fi if [ "$INSTALL_ARM" = "y" ] || [ "$INSTALL_ARM" = "Y" ]; then echo "Installing A.R.M...." @@ -2596,7 +2701,9 @@ ARM_ENV echo "│ Browse, upload, download files via web interface. │" echo "│ Port: 8085 │" echo "└─────────────────────────────────────────────────────────────────┘" + if [ -z "$INSTALL_FILEBROWSER" ]; then prompt_yn "Install Filebrowser? (y/n):" "n" INSTALL_FILEBROWSER + fi if [ "$INSTALL_FILEBROWSER" = "y" ] || [ "$INSTALL_FILEBROWSER" = "Y" ]; then echo "Installing Filebrowser..." @@ -2671,7 +2778,9 @@ FB_SETTINGS echo "│ Modular smart mirror platform. Run up to 3 instances. │" echo "│ Ports: 8081, 8082, 8083 │" echo "└─────────────────────────────────────────────────────────────────┘" + if [ -z "$INSTALL_MAGICMIRROR" ]; then prompt_yn "Install Magic Mirror? (y/n):" "n" INSTALL_MAGICMIRROR + fi if [ "$INSTALL_MAGICMIRROR" = "y" ] || [ "$INSTALL_MAGICMIRROR" = "Y" ]; then echo "" @@ -2902,7 +3011,9 @@ MM_CONFIG echo "│ Budget tracking with bank account synchronization via SimpleFIN│" echo "│ Port: 5006 │" echo "└─────────────────────────────────────────────────────────────────┘" - prompt_yn "Install ActualBudget? (y/n):" "n" INSTALL_ACTUALBUDGET + if [ -z "$INSTALL_ACTUALBUDGET" ]; then + prompt_yn "Install ActualBudget? (y/n):" "n" INSTALL_ACTUALBUDGET + fi if [ "$INSTALL_ACTUALBUDGET" = "y" ] || [ "$INSTALL_ACTUALBUDGET" = "Y" ]; then AB_DIR="$DOCKER_DIR/actualbudget" @@ -2955,7 +3066,9 @@ AB_COMPOSE echo "│ SSO, OAuth2, SAML, User Management, MFA │" echo "│ Port: 8180 (HTTP) - Use reverse proxy for HTTPS │" echo "└─────────────────────────────────────────────────────────────────┘" - prompt_yn "Install Keycloak? (y/n):" "n" INSTALL_KEYCLOAK + if [ -z "$INSTALL_KEYCLOAK" ]; then + prompt_yn "Install Keycloak? (y/n):" "n" INSTALL_KEYCLOAK + fi if [ "$INSTALL_KEYCLOAK" = "y" ] || [ "$INSTALL_KEYCLOAK" = "Y" ]; then KC_DIR="$DOCKER_DIR/keycloak" @@ -3058,7 +3171,9 @@ KC_COMPOSE echo "│ Automatic HTTPS, reverse proxy for all your services │" echo "│ Port: 80 (HTTP), 443 (HTTPS) │" echo "└─────────────────────────────────────────────────────────────────┘" - prompt_yn "Install Caddy reverse proxy? (y/n):" "n" INSTALL_CADDY + if [ -z "$INSTALL_CADDY" ]; then + prompt_yn "Install Caddy reverse proxy? (y/n):" "n" INSTALL_CADDY + fi if [ "$INSTALL_CADDY" = "y" ] || [ "$INSTALL_CADDY" = "Y" ]; then CADDY_DIR="$DOCKER_DIR/caddy" @@ -3196,7 +3311,9 @@ CADDYFILE echo "│ Automatically ban IPs with failed auth attempts │" echo "│ Protects SSH, Caddy, and other services │" echo "└─────────────────────────────────────────────────────────────────┘" - prompt_yn "Install and configure fail2ban? (y/n):" "n" INSTALL_FAIL2BAN + if [ -z "$INSTALL_FAIL2BAN" ]; then + prompt_yn "Install and configure fail2ban? (y/n):" "n" INSTALL_FAIL2BAN + fi if [ "$INSTALL_FAIL2BAN" = "y" ] || [ "$INSTALL_FAIL2BAN" = "Y" ]; then if [ "$DRY_RUN" = true ]; then @@ -3326,7 +3443,9 @@ backend = auto" echo "│ Stream music to Squeezebox devices, apps, and Chromecast. │" echo "│ Port: 9000 (web), 9090 (CLI), 3483 (players) │" echo "└─────────────────────────────────────────────────────────────────┘" + if [ -z "$INSTALL_LMS" ]; then prompt_yn "Install Lyrion Music Server? (y/n):" "n" INSTALL_LMS + fi if [ "$INSTALL_LMS" = "y" ] || [ "$INSTALL_LMS" = "Y" ]; then echo "Installing Lyrion Music Server..." @@ -3390,7 +3509,9 @@ LMS_ENV echo "│ Save recipes, plan meals, generate shopping lists. │" echo "│ Port: 9925 │" echo "└─────────────────────────────────────────────────────────────────┘" + if [ -z "$INSTALL_MEALIE" ]; then prompt_yn "Install Mealie? (y/n):" "n" INSTALL_MEALIE + fi if [ "$INSTALL_MEALIE" = "y" ] || [ "$INSTALL_MEALIE" = "Y" ]; then echo "Installing Mealie..." @@ -3449,7 +3570,9 @@ MEALIE_COMPOSE echo "│ Fabric server with configurable memory allocation. │" echo "│ Port: 25565 │" echo "└─────────────────────────────────────────────────────────────────┘" + if [ -z "$INSTALL_MINECRAFT" ]; then prompt_yn "Install Minecraft Server? (y/n):" "n" INSTALL_MINECRAFT + fi if [ "$INSTALL_MINECRAFT" = "y" ] || [ "$INSTALL_MINECRAFT" = "Y" ]; then echo "Installing Minecraft Server..." @@ -3786,14 +3909,22 @@ FRIGATE_CONFIG fi fi - # ---- CADDY REVERSE PROXY ---- - echo "" - echo "┌─────────────────────────────────────────────────────────────────┐" - echo "│ CADDY - Automatic HTTPS reverse proxy │" - echo "│ Route domains to containers with automatic SSL certificates. │" - echo "│ Ports: 80, 443 │" - echo "└─────────────────────────────────────────────────────────────────┘" - prompt_yn "Install Caddy reverse proxy? (y/n):" "n" INSTALL_CADDY + # ---- CADDY REVERSE PROXY (Legacy) ---- + # Note: This is the legacy Caddy installation + # The newer installation above includes fail2ban support + # This section is kept for backwards compatibility + if [ "$INSTALL_CADDY" != "y" ] && [ "$INSTALL_CADDY" != "Y" ]; then + echo "" + echo "┌─────────────────────────────────────────────────────────────────┐" + echo "│ CADDY - Automatic HTTPS reverse proxy (Legacy) │" + echo "│ Route domains to containers with automatic SSL certificates. │" + echo "│ Ports: 80, 443 │" + echo "└─────────────────────────────────────────────────────────────────┘" + if [ -z "$INSTALL_CADDY_LEGACY" ]; then + prompt_yn "Install Caddy reverse proxy? (y/n):" "n" INSTALL_CADDY_LEGACY + fi + INSTALL_CADDY="$INSTALL_CADDY_LEGACY" + fi if [ "$INSTALL_CADDY" = "y" ] || [ "$INSTALL_CADDY" = "Y" ]; then echo "Installing Caddy..." From 29f49f0ec3f8f074bc64d18c12e01f28ee7ff79e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Jan 2026 23:41:32 +0000 Subject: [PATCH 3/4] Add automated Keycloak initial configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keycloak is now fully configured and ready to use immediately after installation! No more manual realm/client setup required. AUTOMATED SETUP: After Keycloak starts, the script automatically: 1. ✅ Waits for Keycloak to be fully ready (health check) 2. ✅ Logs in using Keycloak Admin CLI (kcadm.sh) 3. ✅ Creates a new realm (e.g., "homelab") 4. ✅ Creates OAuth2/OIDC client for ActualBudget (if selected) 5. ✅ Creates generic OAuth2 client template for other services 6. ✅ Optionally creates an initial user 7. ✅ Saves all OAuth credentials to text files 8. ✅ Provides clear next steps OAUTH2 CLIENT FOR ACTUALBUDGET: - Client ID: actualbudget - Auto-generated secure client secret - Pre-configured redirect URIs for localhost and production - Saved to: ~/docker/keycloak/actualbudget-oauth.txt - Includes all URLs needed to configure ActualBudget GENERIC OAUTH2 CLIENT: - Client ID: generic-app - Can be cloned for other services - Saved to: ~/docker/keycloak/generic-oauth.txt - Works as a template INITIAL USER CREATION: - Prompts for username, email, first name, last name, password - User is immediately active and can log in - Can be used for ActualBudget and other services right away SAVED CONFIGURATION FILES: ~/docker/keycloak/actualbudget-oauth.txt - ActualBudget OAuth config ~/docker/keycloak/generic-oauth.txt - Generic OAuth template PRODUCTION READY: - Redirect URIs include both localhost and production domains - Works with Caddy reverse proxy - SSL/TLS enforced at proxy level - Just update domain in configuration USER EXPERIENCE: Install Keycloak → Answer prompts → DONE! - Realm created: "homelab" (or custom name) - OAuth clients ready - User created and can log in immediately - Just go to http://localhost:8180/admin to manage This eliminates the complex post-install Keycloak setup and makes it immediately usable for ActualBudget and other services! --- ubuntu-post-install.sh | 219 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) diff --git a/ubuntu-post-install.sh b/ubuntu-post-install.sh index 04fa85b..edbfb0c 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -3148,6 +3148,221 @@ KC_COMPOSE if [ "$START_KC" = "y" ] || [ "$START_KC" = "Y" ]; then echo " Starting Keycloak (this may take a minute)..." docker compose up -d 2>/dev/null && echo " ✓ Keycloak started" || echo " ⚠ Failed to start Keycloak" + + # Automated initial configuration + echo "" + prompt_yn "Configure Keycloak with initial realm and clients? (y/n):" "y" CONFIGURE_KC + + if [ "$CONFIGURE_KC" = "y" ] || [ "$CONFIGURE_KC" = "Y" ]; then + echo "" + echo " Configuring Keycloak..." + echo " This will create a realm and OAuth2 clients for your services." + echo "" + + # Get realm name + prompt_text " Realm name (e.g., homelab, services):" "homelab" KC_REALM + + # Get domain for redirect URIs + prompt_text " Your domain (for OAuth callbacks, e.g., example.com):" "localhost" KC_DOMAIN + + # Wait for Keycloak to be fully ready (can take 30-60 seconds) + echo "" + echo " Waiting for Keycloak to be ready..." + KC_READY=false + for i in {1..60}; do + if docker exec keycloak curl -sf http://localhost:8080/health/ready > /dev/null 2>&1; then + KC_READY=true + echo " ✓ Keycloak is ready" + break + fi + echo -n "." + sleep 2 + done + echo "" + + if [ "$KC_READY" = true ]; then + # Login to Keycloak admin CLI + echo " Logging in to Keycloak admin CLI..." + docker exec keycloak /opt/keycloak/bin/kcadm.sh config credentials \ + --server http://localhost:8080 \ + --realm master \ + --user admin \ + --password "$KC_ADMIN_PASS" > /dev/null 2>&1 + + if [ $? -eq 0 ]; then + echo " ✓ Logged in to Keycloak" + + # Create realm + echo " Creating realm '$KC_REALM'..." + docker exec keycloak /opt/keycloak/bin/kcadm.sh create realms \ + -s realm="$KC_REALM" \ + -s enabled=true \ + -s displayName="$KC_REALM" \ + -s registrationAllowed=false \ + -s resetPasswordAllowed=true \ + -s rememberMe=true \ + -s loginWithEmailAllowed=true \ + -s duplicateEmailsAllowed=false \ + -s sslRequired=EXTERNAL > /dev/null 2>&1 + + if [ $? -eq 0 ]; then + echo " ✓ Created realm '$KC_REALM'" + fi + + # Create OAuth2 client for ActualBudget + if [ "$INSTALL_ACTUALBUDGET" = "y" ] || [ "$INSTALL_ACTUALBUDGET" = "Y" ]; then + echo " Creating OAuth2 client for ActualBudget..." + AB_CLIENT_SECRET=$(openssl rand -hex 32) + + docker exec keycloak /opt/keycloak/bin/kcadm.sh create clients -r "$KC_REALM" \ + -s clientId=actualbudget \ + -s name="ActualBudget" \ + -s description="Personal Finance Management" \ + -s enabled=true \ + -s clientAuthenticatorType=client-secret \ + -s secret="$AB_CLIENT_SECRET" \ + -s publicClient=false \ + -s standardFlowEnabled=true \ + -s directAccessGrantsEnabled=true \ + -s serviceAccountsEnabled=false \ + -s 'redirectUris=["http://localhost:5006/*","http://'$KC_DOMAIN':5006/*","https://'$KC_DOMAIN'/*","https://budget.'$KC_DOMAIN'/*"]' \ + -s 'webOrigins=["http://localhost:5006","http://'$KC_DOMAIN':5006","https://'$KC_DOMAIN'","https://budget.'$KC_DOMAIN'"]' \ + -s protocol=openid-connect > /dev/null 2>&1 + + if [ $? -eq 0 ]; then + echo " ✓ Created ActualBudget client" + echo " Client ID: actualbudget" + echo " Client Secret: $AB_CLIENT_SECRET" + echo "" + + # Save to file + cat > "$KC_DIR/actualbudget-oauth.txt" << EOF +ActualBudget OAuth2 Configuration +================================== + +Client ID: actualbudget +Client Secret: $AB_CLIENT_SECRET + +Authorization URL: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/auth +Token URL: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/token +User Info URL: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/userinfo + +For production (with Caddy): +Authorization URL: https://auth.$KC_DOMAIN/realms/$KC_REALM/protocol/openid-connect/auth +Token URL: https://auth.$KC_DOMAIN/realms/$KC_REALM/protocol/openid-connect/token +User Info URL: https://auth.$KC_DOMAIN/realms/$KC_REALM/protocol/openid-connect/userinfo + +Redirect URIs configured: +- http://localhost:5006/* +- https://budget.$KC_DOMAIN/* + +To configure ActualBudget: +1. Go to ActualBudget settings +2. Enable OpenID/OAuth authentication +3. Enter the Client ID and Secret above +4. Use the URLs above based on your setup +EOF + echo " ✓ Saved OAuth config to $KC_DIR/actualbudget-oauth.txt" + fi + fi + + # Create a generic OAuth2 client template for other services + echo " Creating generic OAuth2 client for other services..." + GENERIC_CLIENT_SECRET=$(openssl rand -hex 32) + + docker exec keycloak /opt/keycloak/bin/kcadm.sh create clients -r "$KC_REALM" \ + -s clientId=generic-app \ + -s name="Generic Application" \ + -s description="Template client for other services" \ + -s enabled=true \ + -s clientAuthenticatorType=client-secret \ + -s secret="$GENERIC_CLIENT_SECRET" \ + -s publicClient=false \ + -s standardFlowEnabled=true \ + -s directAccessGrantsEnabled=true \ + -s 'redirectUris=["http://localhost:*/*","https://'$KC_DOMAIN'/*","https://*.'$KC_DOMAIN'/*"]' \ + -s 'webOrigins=["*"]' \ + -s protocol=openid-connect > /dev/null 2>&1 + + if [ $? -eq 0 ]; then + echo " ✓ Created generic OAuth2 client template" + cat > "$KC_DIR/generic-oauth.txt" << EOF +Generic OAuth2 Client Configuration +==================================== + +Client ID: generic-app +Client Secret: $GENERIC_CLIENT_SECRET + +Use this as a template for other services. You can clone this client +in the Keycloak admin console and modify the redirect URIs. + +Base URLs: +- Authorization: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/auth +- Token: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/token +- User Info: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/userinfo + +For production: Replace localhost:8180 with https://auth.$KC_DOMAIN +EOF + echo " ✓ Saved config to $KC_DIR/generic-oauth.txt" + fi + + # Optionally create initial user + echo "" + prompt_yn "Create an initial user in realm '$KC_REALM'? (y/n):" "y" CREATE_USER + + if [ "$CREATE_USER" = "y" ] || [ "$CREATE_USER" = "Y" ]; then + prompt_text " Username:" "$ACTUAL_USER" KC_USERNAME + prompt_text " Email:" "${KC_USERNAME}@${KC_DOMAIN}" KC_EMAIL + prompt_text " First name:" "" KC_FIRSTNAME + prompt_text " Last name:" "" KC_LASTNAME + + echo " Password for $KC_USERNAME:" + read -s KC_USER_PASS + echo "" + + docker exec keycloak /opt/keycloak/bin/kcadm.sh create users -r "$KC_REALM" \ + -s username="$KC_USERNAME" \ + -s email="$KC_EMAIL" \ + -s firstName="$KC_FIRSTNAME" \ + -s lastName="$KC_LASTNAME" \ + -s enabled=true \ + -s emailVerified=true > /dev/null 2>&1 + + if [ $? -eq 0 ]; then + # Set password + KC_USER_ID=$(docker exec keycloak /opt/keycloak/bin/kcadm.sh get users -r "$KC_REALM" -q username="$KC_USERNAME" 2>/dev/null | grep -o '"id" : "[^"]*"' | cut -d'"' -f4) + + docker exec keycloak /opt/keycloak/bin/kcadm.sh set-password -r "$KC_REALM" \ + --username "$KC_USERNAME" \ + --new-password "$KC_USER_PASS" > /dev/null 2>&1 + + echo " ✓ Created user: $KC_USERNAME" + echo " ✓ Password set" + echo "" + echo " This user can now log in to ActualBudget and other services!" + fi + fi + + echo "" + echo " ✓ Keycloak configuration complete!" + echo "" + echo " Next steps:" + echo " 1. Go to http://localhost:8180/admin" + echo " 2. Login with admin / $KC_ADMIN_PASS" + echo " 3. Switch to realm '$KC_REALM' (top-left dropdown)" + echo " 4. Manage users in Users menu" + echo " 5. OAuth configs saved to $KC_DIR/*.txt" + echo "" + + else + echo " ⚠ Failed to login to Keycloak admin CLI" + echo " You can configure Keycloak manually via the web UI" + fi + else + echo " ⚠ Keycloak did not become ready in time" + echo " You can configure it manually after it starts" + fi + fi fi echo "" @@ -3155,6 +3370,10 @@ KC_COMPOSE echo " Username: admin" echo " Password: $KC_ADMIN_PASS" echo " Database: PostgreSQL (./postgres-data)" + if [ -n "$KC_REALM" ]; then + echo " Realm: $KC_REALM" + echo " Config files: $KC_DIR/*.txt" + fi echo "" echo " ⚠ For production:" echo " - Use HTTPS via reverse proxy (Caddy)" From 4c69294c65c31231d488fc514b96f32cc8e44b37 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Jan 2026 23:56:55 +0000 Subject: [PATCH 4/4] Add comprehensive Keycloak setup guide and external service support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KEYCLOAK-SETUP-GUIDE.md: Complete manual explaining Keycloak concepts, manual setup, and external services WHAT'S A REALM: - Isolated container for users/clients/config - Like a "company" or "organization" - master realm = admin only - homelab realm = your actual users - Fully isolated from each other WHAT'S AN OAUTH2 CLIENT: - Each service (ActualBudget, etc.) is a "client" - Needs Client ID, Secret, and Redirect URIs - Redirect URIs must match EXACTLY - Guide explains the authentication flow MANUAL SETUP INSTRUCTIONS: - Step-by-step via web UI - Create realm manually - Create OAuth clients manually - Configure redirect URIs - Create users and set passwords - Test the setup EXTERNAL SERVICE SUPPORT (Pikapod, etc.): Script now asks about setup type: 1. Local only (http://localhost) 2. Public domain (https://yourdomain.com) 3. Both local and public For external services: - Prompts for your public domain - Warns that Keycloak MUST be accessible at https://auth.yourdomain.com - Checks if Caddy/DNS are configured - Asks for external service URL (e.g., Pikapod) - Configures redirect URIs for all scenarios REDIRECT URIS NOW INCLUDE: - http://localhost:5006/* (local dev) - https://budget.yourdomain.com/* (self-hosted) - https://actualbudget-abc.pikapod.net/* (external) - Multiple patterns for flexibility SAVED CONFIG FILES UPDATED: - Shows LOCAL DEVELOPMENT URLs - Shows PRODUCTION URLs (if public domain set) - Shows EXTERNAL SERVICE URLs (if external service set) - Lists all configured redirect URIs - Clear instructions for each scenario CADDY CONFIGURATION GUIDE: - How to configure DNS A/CNAME records - Caddyfile example for Keycloak - Security headers included - Step-by-step setup for external access RECONFIGURATION SUPPORT: - Guide explains how to add realms manually - Guide explains how to add clients manually - CLI examples for adding realms/users/clients - Can re-run script to configure additional realms COMMON USE CASES: 1. All local services 2. Self-hosted with domain 3. Mixed (local + external like Pikapod) Each use case explained with complete examples TROUBLESHOOTING: - Invalid redirect URI - Client not found - Invalid client secret - External service can't reach Keycloak - CORS/redirect failures - Admin console login issues With this update, users can: ✅ Understand what Keycloak is and how it works ✅ Configure it manually if they prefer ✅ Use it with external services like Pikapod ✅ Set up proper DNS/Caddy for production ✅ Troubleshoot common issues ✅ Add realms and clients later --- KEYCLOAK-SETUP-GUIDE.md | 678 ++++++++++++++++++++++++++++++++++++++++ ubuntu-post-install.sh | 142 ++++++++- 2 files changed, 809 insertions(+), 11 deletions(-) create mode 100644 KEYCLOAK-SETUP-GUIDE.md diff --git a/KEYCLOAK-SETUP-GUIDE.md b/KEYCLOAK-SETUP-GUIDE.md new file mode 100644 index 0000000..88bef08 --- /dev/null +++ b/KEYCLOAK-SETUP-GUIDE.md @@ -0,0 +1,678 @@ +# Keycloak Setup Guide +## Complete Manual and Automated Configuration Guide + +This guide explains Keycloak concepts and how to configure it both automatically (via the script) and manually (via the web UI). + +--- + +## Table of Contents +1. [What is Keycloak?](#what-is-keycloak) +2. [Key Concepts](#key-concepts) +3. [Automated Setup (via Script)](#automated-setup) +4. [Manual Setup (via Web UI)](#manual-setup) +5. [Configuring External Services](#configuring-external-services) +6. [Reconfiguration & Adding Realms](#reconfiguration) +7. [Common Use Cases](#common-use-cases) +8. [Troubleshooting](#troubleshooting) + +--- + +## What is Keycloak? + +Keycloak is an **Identity and Access Management (IAM)** system that provides: +- **Single Sign-On (SSO)**: Log in once, access all your services +- **User Management**: Create, manage, and authenticate users in one place +- **OAuth2/OIDC**: Industry-standard authentication for web apps +- **Social Login**: Allow login via Google, GitHub, etc. +- **Multi-Factor Authentication (MFA)**: Add extra security with 2FA/TOTP +- **LDAP/Active Directory Integration**: Connect to existing user directories + +**Think of Keycloak as:** A centralized login system for all your self-hosted services. + +--- + +## Key Concepts + +### 1. **Realm** +A **realm** is an isolated container for users, clients, and configuration. + +**Analogy:** Think of a realm like a "company" or "organization" in Keycloak. + +**Why you need it:** +- The default `master` realm is for Keycloak admin only +- You create a separate realm (e.g., `homelab`) for your actual users and applications +- Realms are completely isolated - users in one realm can't access another + +**Example:** +- `master` realm: Only for Keycloak administrators +- `homelab` realm: For your personal services (ActualBudget, Jellyfin, etc.) +- `family` realm: Separate realm for family members (optional) + +### 2. **OAuth2/OpenID Connect (OIDC) Client** +A **client** is an application that uses Keycloak for authentication. + +**Analogy:** Each service (ActualBudget, Jellyfin, etc.) is a "client" that asks Keycloak "Is this user allowed to log in?" + +**Required information:** +- **Client ID**: Name of the application (e.g., `actualbudget`) +- **Client Secret**: Password for the application (auto-generated, 64-char hex) +- **Redirect URIs**: Where Keycloak sends users after login + - Example: `https://budget.yourdomain.com/*` + - Must match EXACTLY or login will fail + +**Flow:** +1. User clicks "Login" in ActualBudget +2. ActualBudget redirects to Keycloak: `https://auth.yourdomain.com/login` +3. User logs in with username/password +4. Keycloak redirects back to ActualBudget: `https://budget.yourdomain.com/callback` +5. ActualBudget gets user info and logs them in + +### 3. **Users** +A **user** is a person who can log in to your services. + +**User attributes:** +- Username (required, unique) +- Email (optional but recommended) +- First name / Last name (optional) +- Password (set via Credentials tab) +- Email verified (set to true to skip verification) +- Enabled (must be true for user to log in) + +### 4. **Redirect URIs** +**Critical concept:** The redirect URI is where Keycloak sends the user after successful login. + +**Common mistakes:** +- ❌ `http://localhost:5006` (won't work for external services) +- ❌ `https://budget.example.com` (missing wildcard or path) +- ✅ `https://budget.example.com/*` (correct - allows all paths) + +**For external services (like Pikapod):** +- Pikapod gives you a URL like: `https://actualbudget-abc123.pikapod.net` +- Your redirect URI: `https://actualbudget-abc123.pikapod.net/*` +- Your Keycloak URL: `https://auth.yourdomain.com` (must be publicly accessible) + +--- + +## Automated Setup (via Script) + +The script automates everything for you. Here's what it does: + +### Step 1: Install Keycloak +```bash +./ubuntu-post-install.sh +# Select KEYCLOAK in whiptail menu +``` + +Prompts: +- Admin password (for Keycloak admin console) +- Database password (for PostgreSQL) + +### Step 2: Automated Configuration +``` +Configure Keycloak with initial realm and clients? (y/n): y +``` + +This automatically: +1. ✅ Waits for Keycloak to start (health check) +2. ✅ Logs in using admin CLI (`kcadm.sh`) +3. ✅ Creates a realm (e.g., `homelab`) +4. ✅ Creates OAuth client for ActualBudget (if selected) +5. ✅ Creates generic OAuth client template +6. ✅ Saves all credentials to `~/docker/keycloak/*.txt` +7. ✅ Optionally creates initial user + +### Step 3: What Gets Created + +**Realm:** `homelab` (or your custom name) + +**ActualBudget OAuth Client:** +- Client ID: `actualbudget` +- Client Secret: (saved to `actualbudget-oauth.txt`) +- Redirect URIs: + - `http://localhost:5006/*` (local development) + - `http://yourdomain.com:5006/*` (local with domain) + - `https://yourdomain.com/*` (production - any subdomain) + - `https://budget.yourdomain.com/*` (specific subdomain) + +**Generic OAuth Client:** +- Client ID: `generic-app` +- Client Secret: (saved to `generic-oauth.txt`) +- Can be cloned for other services + +**Initial User:** +- Username, email, password you provide +- Immediately active +- Can log in to all services + +### Step 4: Configuration Files + +All credentials saved to: +``` +~/docker/keycloak/actualbudget-oauth.txt +~/docker/keycloak/generic-oauth.txt +``` + +These files contain: +- Client ID +- Client Secret +- Authorization URL +- Token URL +- User Info URL +- Instructions for configuring each service + +--- + +## Manual Setup (via Web UI) + +If you prefer to configure Keycloak manually, or want to add services later: + +### Access Admin Console +``` +URL: http://localhost:8180/admin +Username: admin +Password: [your admin password] +``` + +### Step 1: Create a Realm + +1. **Click dropdown** in top-left corner (shows "Master") +2. **Click "Create Realm"** +3. **Realm name:** `homelab` (or your choice) +4. **Click "Create"** + +**Settings to configure:** +- **Login tab:** + - ✅ User registration: OFF (you create users manually) + - ✅ Forgot password: ON (allows password resets) + - ✅ Remember me: ON (convenience) + - ✅ Login with email: ON (users can use email instead of username) + +- **Email tab:** (optional, for password resets) + - Configure SMTP settings if you want email features + +### Step 2: Create an OAuth2 Client (for ActualBudget) + +1. **Switch to your realm** (`homelab`) via dropdown +2. **Go to Clients** (left menu) +3. **Click "Create client"** + +**General Settings:** +- **Client type:** OpenID Connect +- **Client ID:** `actualbudget` +- **Name:** `ActualBudget` +- **Description:** `Personal Finance Management` +- **Click "Next"** + +**Capability config:** +- ✅ Client authentication: ON (creates a secret) +- ✅ Authorization: OFF (not needed) +- ✅ Standard flow: ON (authorization code flow) +- ✅ Direct access grants: ON (allows username/password) +- ❌ Implicit flow: OFF (deprecated) +- ❌ Service accounts: OFF (not needed for web apps) +- **Click "Next"** + +**Login settings:** + +**Important: Adjust these for your setup!** + +**For local ActualBudget:** +``` +Root URL: http://localhost:5006 +Home URL: http://localhost:5006 +Valid redirect URIs: + http://localhost:5006/* + http://localhost:5006/callback + +Valid post logout redirect URIs: + + +Web origins: + http://localhost:5006 +``` + +**For external ActualBudget (Pikapod, etc.):** +``` +Root URL: https://actualbudget-abc123.pikapod.net +Home URL: https://actualbudget-abc123.pikapod.net +Valid redirect URIs: + https://actualbudget-abc123.pikapod.net/* + https://actualbudget-abc123.pikapod.net/callback + +Valid post logout redirect URIs: + + +Web origins: + https://actualbudget-abc123.pikapod.net +``` + +**For self-hosted with domain:** +``` +Root URL: https://budget.yourdomain.com +Home URL: https://budget.yourdomain.com +Valid redirect URIs: + https://budget.yourdomain.com/* + https://budget.yourdomain.com/callback + +Valid post logout redirect URIs: + + +Web origins: + https://budget.yourdomain.com +``` + +4. **Click "Save"** + +### Step 3: Get Client Secret + +1. **Go to "Credentials" tab** +2. **Copy "Client secret"** (you'll need this for ActualBudget) +3. **Save it somewhere safe!** + +### Step 4: Create a User + +1. **Go to Users** (left menu) +2. **Click "Create user"** + +**User details:** +- **Username:** `john` (required) +- **Email:** `john@example.com` (optional but recommended) +- **Email verified:** ✅ ON (skip email verification) +- **First name:** `John` +- **Last name:** `Doe` +- **Enabled:** ✅ ON (user can log in) +- **Click "Create"** + +**Set password:** +1. **Go to "Credentials" tab** +2. **Click "Set password"** +3. **Enter password** (twice) +4. **Temporary:** ❌ OFF (user won't be forced to change it) +5. **Click "Save"** +6. **Confirm** in popup + +### Step 5: Test Login + +1. **Go to Realm Settings** → **Endpoints** +2. **Click "OpenID Endpoint Configuration"** (opens JSON) +3. **Find:** `authorization_endpoint` +4. **Copy URL** and open in browser +5. **Add:** `?client_id=actualbudget&response_type=code&redirect_uri=http://localhost:5006/callback` +6. **Log in** with your user +7. **You should see:** Redirect to callback URL (may error if ActualBudget not configured, but login works) + +--- + +## Configuring External Services + +### Keycloak MUST be Publicly Accessible + +**Critical:** For external services like Pikapod, your Keycloak must be accessible from the internet. + +### Requirements: +1. ✅ **Domain name** (e.g., `yourdomain.com`) +2. ✅ **DNS A record** pointing to your server +3. ✅ **Caddy reverse proxy** with HTTPS +4. ✅ **Port 80/443 open** in firewall +5. ✅ **Keycloak accessible** at `https://auth.yourdomain.com` + +### Setup Caddy for Keycloak + +**Add to Caddyfile:** +```caddy +auth.yourdomain.com { + log { + output file /var/log/caddy/keycloak-access.log + format json + level INFO + } + + reverse_proxy localhost:8180 + + # 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" + } +} +``` + +**Reload Caddy:** +```bash +cd ~/docker/caddy +docker exec -w /etc/caddy caddy caddy reload +``` + +**Test:** +``` +https://auth.yourdomain.com/admin +``` + +### Configure DNS + +**Add A record:** +``` +auth.yourdomain.com → [Your Server IP] +``` + +**Or use CNAME:** +``` +auth → yourdomain.com +``` + +### Example: ActualBudget on Pikapod + +**Scenario:** +- Keycloak: `https://auth.yourdomain.com` (your server) +- ActualBudget: `https://actualbudget-abc123.pikapod.net` (Pikapod) + +**In Keycloak:** + +1. **Create client:** `actualbudget-pikapod` +2. **Redirect URIs:** + ``` + https://actualbudget-abc123.pikapod.net/* + https://actualbudget-abc123.pikapod.net/callback + ``` +3. **Web origins:** + ``` + https://actualbudget-abc123.pikapod.net + ``` + +**In ActualBudget (Pikapod):** + +Settings → Authentication: +``` +Client ID: actualbudget-pikapod +Client Secret: [from Keycloak credentials tab] + +Authorization URL: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/auth +Token URL: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/token +User Info URL: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/userinfo +``` + +**Flow:** +1. User visits `https://actualbudget-abc123.pikapod.net` +2. Clicks "Login" +3. Redirects to `https://auth.yourdomain.com/realms/homelab/...` +4. User logs in +5. Redirects back to `https://actualbudget-abc123.pikapod.net/callback` +6. User is logged in! + +--- + +## Reconfiguration & Adding Realms + +You can re-run the script to add more realms or clients! + +### Option 1: Re-run the Script + +```bash +cd ~/docker/keycloak +docker compose down +cd ~ +./ubuntu-post-install.sh +# Select KEYCLOAK again +# Choose "Configure Keycloak..." → Yes +# Enter new realm name: "family" +# Create new users +``` + +**This creates:** +- New realm with new users +- New OAuth clients for that realm +- Separate from your existing realm + +### Option 2: Add Realm Manually + +**Via Web UI:** +1. Go to admin console +2. Click realm dropdown +3. "Create Realm" +4. Name: `family` +5. Repeat client/user creation steps + +### Option 3: Use Script Helper + +The script can be extended to add a helper: + +```bash +cd ~/docker/keycloak + +# Login to admin CLI +docker exec keycloak /opt/keycloak/bin/kcadm.sh config credentials \ + --server http://localhost:8080 \ + --realm master \ + --user admin \ + --password [YOUR_ADMIN_PASSWORD] + +# Create new realm +docker exec keycloak /opt/keycloak/bin/kcadm.sh create realms \ + -s realm=family \ + -s enabled=true + +# Create new client +docker exec keycloak /opt/keycloak/bin/kcadm.sh create clients -r family \ + -s clientId=my-new-service \ + -s enabled=true \ + -s clientAuthenticatorType=client-secret \ + -s secret=$(openssl rand -hex 32) \ + -s 'redirectUris=["https://service.yourdomain.com/*"]' + +# Create new user +docker exec keycloak /opt/keycloak/bin/kcadm.sh create users -r family \ + -s username=alice \ + -s email=alice@example.com \ + -s enabled=true + +# Set password +docker exec keycloak /opt/keycloak/bin/kcadm.sh set-password -r family \ + --username alice \ + --new-password 'AlicePassword123!' +``` + +--- + +## Common Use Cases + +### Use Case 1: All Local Services +**Setup:** +- Keycloak: `http://localhost:8180` +- ActualBudget: `http://localhost:5006` +- Jellyfin: `http://localhost:8096` + +**Configuration:** +- No domain needed +- Use `localhost` URLs everywhere +- Redirect URIs: `http://localhost:PORT/*` + +### Use Case 2: Self-Hosted with Domain +**Setup:** +- Keycloak: `https://auth.yourdomain.com` +- ActualBudget: `https://budget.yourdomain.com` +- Jellyfin: `https://jellyfin.yourdomain.com` + +**Configuration:** +- Requires domain + Caddy +- Use HTTPS URLs +- Redirect URIs: `https://service.yourdomain.com/*` + +### Use Case 3: Mixed (Local + External) +**Setup:** +- Keycloak: `https://auth.yourdomain.com` (self-hosted) +- ActualBudget: `https://actualbudget-abc.pikapod.net` (Pikapod) +- Jellyfin: `https://jellyfin.yourdomain.com` (self-hosted) + +**Configuration:** +- Keycloak MUST be publicly accessible +- Each service gets its own client +- ActualBudget redirect: `https://actualbudget-abc.pikapod.net/*` +- Jellyfin redirect: `https://jellyfin.yourdomain.com/*` + +--- + +## Troubleshooting + +### Issue: "Invalid redirect URI" +**Cause:** Redirect URI in Keycloak doesn't match what the app is using. + +**Fix:** +1. Check error message for actual redirect URI +2. Add EXACT URI to Keycloak client settings +3. Include wildcard: `https://domain.com/*` + +### Issue: "Client not found" +**Cause:** Client ID doesn't match. + +**Fix:** +1. Check client ID in Keycloak +2. Ensure it matches exactly in application +3. Case-sensitive! + +### Issue: "Invalid client secret" +**Cause:** Wrong secret or expired. + +**Fix:** +1. Go to Keycloak → Clients → Credentials +2. Copy secret again (or regenerate) +3. Update in application + +### Issue: External service can't reach Keycloak +**Cause:** Keycloak not publicly accessible. + +**Fix:** +1. Ensure Caddy is running: `docker ps | grep caddy` +2. Check DNS: `dig auth.yourdomain.com` +3. Test URL: `curl https://auth.yourdomain.com` +4. Check firewall: `sudo ufw status` (80/443 open?) + +### Issue: Login succeeds but redirect fails +**Cause:** CORS or redirect URI mismatch. + +**Fix:** +1. Add domain to "Web Origins" in client settings +2. Check redirect URI includes protocol (https://) +3. Check for typos in domain name + +### Issue: Can't login to Keycloak admin console +**Cause:** Container not started or wrong password. + +**Fix:** +```bash +# Check if running +docker ps | grep keycloak + +# Check logs +docker logs keycloak --tail 50 + +# Restart +cd ~/docker/keycloak +docker compose restart + +# Reset admin password (if needed) +docker exec keycloak /opt/keycloak/bin/kcadm.sh config credentials \ + --server http://localhost:8080 \ + --realm master \ + --user admin \ + --password NEW_PASSWORD_HERE +``` + +--- + +## Quick Reference + +### Important URLs + +**Local:** +``` +Admin Console: http://localhost:8180/admin +Realm Endpoints: http://localhost:8180/realms/{realm-name}/.well-known/openid-configuration +``` + +**Production:** +``` +Admin Console: https://auth.yourdomain.com/admin +Realm Endpoints: https://auth.yourdomain.com/realms/{realm-name}/.well-known/openid-configuration +``` + +### OAuth URLs (for realm "homelab") + +**Local:** +``` +Authorization: http://localhost:8180/realms/homelab/protocol/openid-connect/auth +Token: http://localhost:8180/realms/homelab/protocol/openid-connect/token +User Info: http://localhost:8180/realms/homelab/protocol/openid-connect/userinfo +Logout: http://localhost:8180/realms/homelab/protocol/openid-connect/logout +``` + +**Production:** +``` +Authorization: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/auth +Token: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/token +User Info: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/userinfo +Logout: https://auth.yourdomain.com/realms/homelab/protocol/openid-connect/logout +``` + +### Common Commands + +```bash +# Start Keycloak +cd ~/docker/keycloak +docker compose up -d + +# Stop Keycloak +docker compose down + +# View logs +docker logs keycloak -f + +# Access shell +docker exec -it keycloak bash + +# Login to admin CLI +docker exec keycloak /opt/keycloak/bin/kcadm.sh config credentials \ + --server http://localhost:8080 \ + --realm master \ + --user admin \ + --password YOUR_PASSWORD + +# Export realm configuration (backup) +docker exec keycloak /opt/keycloak/bin/kc.sh export \ + --dir /opt/keycloak/data/export \ + --realm homelab + +# Copy export to host +docker cp keycloak:/opt/keycloak/data/export ./backup/ +``` + +--- + +## Summary + +**Keycloak provides:** +- ✅ Single Sign-On for all your services +- ✅ Centralized user management +- ✅ OAuth2/OIDC authentication +- ✅ Works with local and external services +- ✅ Professional-grade security + +**Automated setup does:** +- ✅ Creates realm +- ✅ Creates OAuth clients +- ✅ Creates initial user +- ✅ Saves all credentials +- ✅ Ready to use immediately + +**Manual setup allows:** +- ✅ Full control over configuration +- ✅ Multiple realms (family, work, etc.) +- ✅ Custom client settings +- ✅ Advanced features (LDAP, MFA, etc.) + +**For external services:** +- ✅ Keycloak must be publicly accessible +- ✅ Use Caddy with HTTPS +- ✅ Configure proper redirect URIs +- ✅ Test OAuth flow before production + +For questions or issues, check the Keycloak documentation: https://www.keycloak.org/documentation diff --git a/ubuntu-post-install.sh b/ubuntu-post-install.sh index edbfb0c..ee0eb1d 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -3162,8 +3162,63 @@ KC_COMPOSE # Get realm name prompt_text " Realm name (e.g., homelab, services):" "homelab" KC_REALM - # Get domain for redirect URIs - prompt_text " Your domain (for OAuth callbacks, e.g., example.com):" "localhost" KC_DOMAIN + # Get domain configuration for redirect URIs + echo "" + echo " ──────────────────────────────────────────────────────────────" + echo " DOMAIN CONFIGURATION" + echo " ──────────────────────────────────────────────────────────────" + echo "" + echo " Keycloak needs to know where your services are hosted." + echo "" + echo " Options:" + echo " 1. Local only (http://localhost:PORT)" + echo " 2. Public domain (https://yourdomain.com)" + echo " 3. Both local and public" + echo "" + prompt_text " Enter your setup (1/2/3):" "1" KC_SETUP_TYPE + + KC_DOMAIN="localhost" + KC_PUBLIC_DOMAIN="" + KC_EXTERNAL_SERVICE="" + + if [ "$KC_SETUP_TYPE" = "2" ] || [ "$KC_SETUP_TYPE" = "3" ]; then + echo "" + prompt_text " Your public domain (e.g., example.com):" "" KC_PUBLIC_DOMAIN + + echo "" + echo " ⚠ IMPORTANT: For Keycloak to work with external services," + echo " it MUST be accessible at https://auth.$KC_PUBLIC_DOMAIN" + echo "" + echo " This requires:" + echo " ✓ DNS A record: auth.$KC_PUBLIC_DOMAIN → Your Server IP" + echo " ✓ Caddy reverse proxy configured" + echo " ✓ Ports 80/443 open in firewall" + echo "" + prompt_yn " Is Keycloak accessible at https://auth.$KC_PUBLIC_DOMAIN? (y/n):" "n" KC_DOMAIN_READY + + if [ "$KC_DOMAIN_READY" != "y" ] && [ "$KC_DOMAIN_READY" != "Y" ]; then + echo "" + echo " ⚠ WARNING: Keycloak won't work with external services until" + echo " you configure Caddy and DNS. See KEYCLOAK-SETUP-GUIDE.md" + echo "" + echo " You can still proceed and configure Caddy later." + echo "" + fi + + # Ask about external services (like Pikapod) + echo "" + prompt_yn " Are you using external hosted services (e.g., Pikapod)? (y/n):" "n" KC_HAS_EXTERNAL + + if [ "$KC_HAS_EXTERNAL" = "y" ] || [ "$KC_HAS_EXTERNAL" = "Y" ]; then + echo "" + echo " Enter the URL of your external service (e.g., https://actualbudget-abc.pikapod.net)" + prompt_text " External service URL:" "" KC_EXTERNAL_SERVICE + fi + fi + + if [ "$KC_SETUP_TYPE" = "1" ] || [ "$KC_SETUP_TYPE" = "3" ]; then + KC_DOMAIN="localhost" + fi # Wait for Keycloak to be fully ready (can take 30-60 seconds) echo "" @@ -3214,6 +3269,33 @@ KC_COMPOSE echo " Creating OAuth2 client for ActualBudget..." AB_CLIENT_SECRET=$(openssl rand -hex 32) + # Build redirect URIs based on configuration + AB_REDIRECT_URIS='["http://localhost:5006/*","http://localhost:5006/callback"' + + if [ -n "$KC_PUBLIC_DOMAIN" ]; then + AB_REDIRECT_URIS="$AB_REDIRECT_URIS"',"https://budget.'$KC_PUBLIC_DOMAIN'/*","https://budget.'$KC_PUBLIC_DOMAIN'/callback"' + AB_REDIRECT_URIS="$AB_REDIRECT_URIS"',"https://'$KC_PUBLIC_DOMAIN':5006/*","https://'$KC_PUBLIC_DOMAIN':5006/callback"' + fi + + if [ -n "$KC_EXTERNAL_SERVICE" ]; then + AB_REDIRECT_URIS="$AB_REDIRECT_URIS"',"'$KC_EXTERNAL_SERVICE'/*","'$KC_EXTERNAL_SERVICE'/callback"' + fi + + AB_REDIRECT_URIS="$AB_REDIRECT_URIS"']' + + # Build web origins + AB_WEB_ORIGINS='["http://localhost:5006"' + + if [ -n "$KC_PUBLIC_DOMAIN" ]; then + AB_WEB_ORIGINS="$AB_WEB_ORIGINS"',"https://budget.'$KC_PUBLIC_DOMAIN'","https://'$KC_PUBLIC_DOMAIN':5006"' + fi + + if [ -n "$KC_EXTERNAL_SERVICE" ]; then + AB_WEB_ORIGINS="$AB_WEB_ORIGINS"',"'$KC_EXTERNAL_SERVICE'"' + fi + + AB_WEB_ORIGINS="$AB_WEB_ORIGINS"']' + docker exec keycloak /opt/keycloak/bin/kcadm.sh create clients -r "$KC_REALM" \ -s clientId=actualbudget \ -s name="ActualBudget" \ @@ -3225,8 +3307,8 @@ KC_COMPOSE -s standardFlowEnabled=true \ -s directAccessGrantsEnabled=true \ -s serviceAccountsEnabled=false \ - -s 'redirectUris=["http://localhost:5006/*","http://'$KC_DOMAIN':5006/*","https://'$KC_DOMAIN'/*","https://budget.'$KC_DOMAIN'/*"]' \ - -s 'webOrigins=["http://localhost:5006","http://'$KC_DOMAIN':5006","https://'$KC_DOMAIN'","https://budget.'$KC_DOMAIN'"]' \ + -s "redirectUris=$AB_REDIRECT_URIS" \ + -s "webOrigins=$AB_WEB_ORIGINS" \ -s protocol=openid-connect > /dev/null 2>&1 if [ $? -eq 0 ]; then @@ -3235,7 +3317,12 @@ KC_COMPOSE echo " Client Secret: $AB_CLIENT_SECRET" echo "" - # Save to file + # Save to file with appropriate URLs + KC_AUTH_URL="http://localhost:8180" + if [ -n "$KC_PUBLIC_DOMAIN" ]; then + KC_AUTH_URL="https://auth.$KC_PUBLIC_DOMAIN" + fi + cat > "$KC_DIR/actualbudget-oauth.txt" << EOF ActualBudget OAuth2 Configuration ================================== @@ -3243,18 +3330,51 @@ ActualBudget OAuth2 Configuration Client ID: actualbudget Client Secret: $AB_CLIENT_SECRET +LOCAL DEVELOPMENT: Authorization URL: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/auth Token URL: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/token User Info URL: http://localhost:8180/realms/$KC_REALM/protocol/openid-connect/userinfo +EOF -For production (with Caddy): -Authorization URL: https://auth.$KC_DOMAIN/realms/$KC_REALM/protocol/openid-connect/auth -Token URL: https://auth.$KC_DOMAIN/realms/$KC_REALM/protocol/openid-connect/token -User Info URL: https://auth.$KC_DOMAIN/realms/$KC_REALM/protocol/openid-connect/userinfo + if [ -n "$KC_PUBLIC_DOMAIN" ]; then + cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF + +PRODUCTION (with Caddy at https://auth.$KC_PUBLIC_DOMAIN): +Authorization URL: https://auth.$KC_PUBLIC_DOMAIN/realms/$KC_REALM/protocol/openid-connect/auth +Token URL: https://auth.$KC_PUBLIC_DOMAIN/realms/$KC_REALM/protocol/openid-connect/token +User Info URL: https://auth.$KC_PUBLIC_DOMAIN/realms/$KC_REALM/protocol/openid-connect/userinfo +EOF + fi + + if [ -n "$KC_EXTERNAL_SERVICE" ]; then + cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF + +EXTERNAL SERVICE ($KC_EXTERNAL_SERVICE): +- Use PRODUCTION URLs above +- Keycloak MUST be accessible at: https://auth.$KC_PUBLIC_DOMAIN +- Redirect URI configured: $KC_EXTERNAL_SERVICE/* +EOF + fi + + cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF Redirect URIs configured: -- http://localhost:5006/* -- https://budget.$KC_DOMAIN/* +- http://localhost:5006/* (local) +EOF + + if [ -n "$KC_PUBLIC_DOMAIN" ]; then + cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF +- https://budget.$KC_PUBLIC_DOMAIN/* (self-hosted) +EOF + fi + + if [ -n "$KC_EXTERNAL_SERVICE" ]; then + cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF +- $KC_EXTERNAL_SERVICE/* (external) +EOF + fi + + cat >> "$KC_DIR/actualbudget-oauth.txt" << EOF To configure ActualBudget: 1. Go to ActualBudget settings