diff --git a/SECURITY-IMPROVEMENTS.md b/SECURITY-IMPROVEMENTS.md new file mode 100644 index 0000000..7340b81 --- /dev/null +++ b/SECURITY-IMPROVEMENTS.md @@ -0,0 +1,409 @@ +# Security and Infrastructure Improvements + +## Summary of Changes + +This document describes the comprehensive security and infrastructure improvements made to the Ubuntu post-installation script. + +## Issues Fixed + +### 1. Docker Directory Ownership +**Problem:** Docker directories were being created as root, causing permission issues when running Docker without sudo. + +**Solution:** +- Added `ensure_docker_dir_ownership()` helper function +- Applied to ALL 25+ services (Immich, Keycloak, ActualBudget, Jellyfin, etc.) +- Fixed disaster recovery path (line 309) +- All Docker directories now properly owned by sudo user + +**Impact:** Docker containers can now be managed without requiring root/sudo for every command. + +--- + +### 2. Keycloak Security Overhaul +**Problem:** Weak default passwords, special characters causing issues, development mode in production. + +**Solutions Implemented:** + +#### Password Requirements +- **Minimum length:** 12 characters (16+ recommended) +- **Character set:** Letters and numbers ONLY (no special characters) +- **Auto-generation:** Press ENTER to generate secure passwords automatically +- **Validation:** Real-time password validation with retry loop + +#### Production vs Development Mode +- **Production mode:** Uses `start` command, requires hostname configuration +- **Development mode:** Uses `start-dev` command, relaxed security for testing +- **Hostname support:** Proper `KC_HOSTNAME` configuration for public deployment + +#### Environment Variables +- All credentials moved to `.env` file +- Admin password and database password securely stored +- No more hardcoded passwords in docker-compose.yml + +**Example Keycloak .env file structure:** +```env +# Keycloak Environment Variables +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD= +POSTGRES_DB=keycloak +POSTGRES_USER=keycloak +POSTGRES_PASSWORD= +KC_PROXY=edge +KC_HTTP_ENABLED=true +KC_HOSTNAME=auth.yourdomain.com # (if production mode) +``` + +--- + +### 3. Environment Variable Management (.env Files) + +**Services Now Using .env Files:** +- ✅ Keycloak (admin + database passwords) +- ✅ ActualBudget (timezone and config) +- ✅ Immich (already had .env) +- ✅ FindMyDevice (already had .env) +- ✅ wg-easy (already had .env) +- ✅ Kopia (already had .env) + +**Benefits:** +- Passwords not visible in docker-compose.yml files +- Easy to backup separately from compose files +- Can be excluded from version control +- Easier credential rotation + +--- + +### 4. Caddy2 Reverse Proxy Integration + +**Existing Integration:** +All services already include Caddy2 reverse proxy configuration via the `configure_caddy_for_service()` function. + +**Features:** +- Automatic HTTPS via Let's Encrypt +- HTTP/2 support +- Security headers (HSTS, X-Frame-Options, etc.) +- JSON logging for fail2ban +- Automatic certificate renewal + +**Example Caddy Configuration:** +``` +photos.yourdomain.com { + reverse_proxy localhost:2283 + + 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" + } + + log { + output file /var/log/caddy/photos-access.log + format json + } +} +``` + +--- + +### 5. Fail2ban Integration + +**Existing fail2ban Labels:** +All services already include fail2ban monitoring labels: + +```yaml +labels: + - "io.podman.annotations.label/fail2ban.enable=true" + - "io.podman.annotations.label/fail2ban.filter=caddy-auth" +``` + +**Services with fail2ban monitoring:** +- ActualBudget +- Keycloak +- All other internet-facing services + +**fail2ban Configuration:** +- Filter: `/etc/fail2ban/filter.d/caddy-auth.conf` +- Jail: `/etc/fail2ban/jail.d/caddy.conf` +- Ban after: 5 failed attempts +- Ban duration: 3600 seconds (1 hour) +- Detection window: 600 seconds + +**Detailed Setup:** See `CADDY-FAIL2BAN-SETUP.md` for complete configuration. + +--- + +## Helper Functions Added + +### `ensure_docker_dir_ownership(dir1 [dir2 ...])` +Ensures Docker directories are owned by the actual user (not root). + +**Usage:** +```bash +mkdir -p "$SERVICE_DIR" +ensure_docker_dir_ownership "$SERVICE_DIR" +``` + +### `generate_password([length])` +Generates secure alphanumeric passwords (no special characters). + +**Usage:** +```bash +PASSWORD=$(generate_password 20) # 20-character password +``` + +### `validate_password(password [min_length])` +Validates passwords for Keycloak compatibility. + +**Validation Rules:** +- Minimum length (default: 12 characters) +- Alphanumeric only (a-zA-Z0-9) +- Returns 0 if valid, 1 if invalid + +**Usage:** +```bash +if validate_password "$USER_PASSWORD" 12; then + echo "Password accepted" +fi +``` + +--- + +## Keycloak Setup Guide + +### For ActualBudget on Pikapods + +1. **Install Keycloak with production mode:** + ```bash + sudo bash ubuntu-post-install.sh + # Select Keycloak from menu + # Choose production mode (y) + # Enter hostname: auth.yourdomain.com + # Press ENTER to auto-generate secure passwords + ``` + +2. **Configure Caddy2:** + - Script automatically prompts for Caddy configuration + - Enter your domain (e.g., auth.yourdomain.com) + - Ensure DNS A record points to your server + +3. **Configure DNS:** + ``` + auth.yourdomain.com → Your Server IP + ``` + +4. **Access Keycloak:** + ``` + https://auth.yourdomain.com + ``` + +5. **Set up ActualBudget OAuth:** + - The script automatically creates an OAuth client for ActualBudget + - Client details saved to: `~/docker/keycloak/actualbudget-oauth.txt` + - Use these credentials in your Pikapod ActualBudget instance + +6. **Configure ActualBudget on Pikapods:** + - Go to your ActualBudget settings + - Enable OpenID Connect + - Enter your Keycloak details: + - Issuer: `https://auth.yourdomain.com/realms/homelab` + - Client ID: (from actualbudget-oauth.txt) + - Client Secret: (from actualbudget-oauth.txt) + +### For Other Self-Hosted Services + +The script can create generic OAuth clients for other services. After Keycloak installation, you can: + +1. Access Keycloak admin console +2. Create new OAuth2/OIDC clients +3. Configure redirect URIs for your services +4. Use the client credentials in your service configuration + +**Generic Client Template:** +- Client ID: your-service-name +- Client Type: Confidential +- Standard Flow Enabled: Yes +- Valid Redirect URIs: https://your-service.com/* + +--- + +## Password Requirements Reference + +### Keycloak Passwords +- **Minimum:** 12 characters +- **Recommended:** 16+ characters +- **Format:** Alphanumeric only (a-zA-Z0-9) +- **No special characters:** `!@#$%^&*()` etc. are NOT allowed +- **Generation:** Press ENTER for auto-generated secure passwords + +### Why No Special Characters? +Keycloak has issues with special characters in certain authentication flows and database connection strings. Restricting to alphanumeric ensures compatibility. + +### Password Strength with Alphanumeric Only +- 12 characters: ~62^12 = 3.2 × 10^21 combinations +- 16 characters: ~62^16 = 4.7 × 10^28 combinations +- 20 characters: ~62^20 = 7.0 × 10^35 combinations + +This is cryptographically secure for all practical purposes. + +--- + +## Verification Checklist + +After running the updated script: + +### Docker Ownership +```bash +# Check docker directory ownership +ls -la ~/docker/ +# All directories should be owned by your user, not root + +# Test docker without sudo +docker ps +# Should work without permission errors +``` + +### Keycloak +```bash +# Check .env file exists +cat ~/docker/keycloak/.env +# Should contain KEYCLOAK_ADMIN_PASSWORD and POSTGRES_PASSWORD + +# Check production mode +cat ~/docker/keycloak/docker-compose.yml | grep command +# Should show "start" for production or "start-dev" for development + +# Test access +curl http://localhost:8180/health +# Should return health status +``` + +### Caddy2 +```bash +# Check Caddy is running +docker ps | grep caddy + +# Check logs +docker logs caddy + +# Test HTTPS redirect +curl -I http://yourdomain.com +# Should redirect to HTTPS +``` + +### fail2ban +```bash +# Check fail2ban status +sudo fail2ban-client status caddy-auth + +# Test ban +# (Make 5 failed login attempts) +sudo fail2ban-client status caddy-auth +# Should show banned IP +``` + +--- + +## Migration Guide + +If you have existing services: + +### Existing ActualBudget +1. Backup existing data: `cp -r ~/docker/actualbudget ~/docker/actualbudget.backup` +2. Run updated script and select "Reconfigure" when prompted +3. New .env file will be created +4. Verify ownership: `ls -la ~/docker/actualbudget` +5. Restart container: `cd ~/docker/actualbudget && docker compose restart` + +### Existing Keycloak +1. **IMPORTANT:** Backup your data first! + ```bash + cp -r ~/docker/keycloak ~/docker/keycloak.backup + ``` +2. Stop existing container: + ```bash + cd ~/docker/keycloak && docker compose down + ``` +3. Run updated script and select Keycloak +4. Choose whether to keep existing data or start fresh +5. If keeping data, manually update .env with your existing passwords +6. Restart: `docker compose up -d` + +--- + +## Troubleshooting + +### Permission Denied Errors +```bash +# Fix ownership of all docker directories +sudo chown -R $USER:$USER ~/docker +``` + +### Keycloak Won't Start +```bash +# Check logs +docker logs keycloak + +# Common issues: +# 1. Missing KC_HOSTNAME in production mode +# 2. Database connection failed (check postgres container) +# 3. Port 8180 already in use + +# Fix: Edit .env and docker-compose.yml as needed +``` + +### Caddy Certificate Errors +```bash +# Check Caddy logs +docker logs caddy + +# Common issues: +# 1. DNS not pointing to server +# 2. Ports 80/443 not open +# 3. Firewall blocking Let's Encrypt + +# Test DNS: +dig auth.yourdomain.com + +# Test port accessibility: +sudo ufw status +``` + +--- + +## Security Best Practices + +1. **Change default passwords:** Even with auto-generation, review and update if needed +2. **Use production mode for Keycloak:** Never use development mode for internet-facing deployments +3. **Enable fail2ban:** Monitor and ban malicious IPs +4. **Regular updates:** Keep containers updated (use Watchtower in notify mode) +5. **Backup .env files:** Store securely, separate from compose files +6. **Use HTTPS everywhere:** Configure Caddy2 for all public services +7. **Limit exposed ports:** Only expose necessary ports to the internet +8. **Monitor logs:** Regular review of Caddy and fail2ban logs + +--- + +## Additional Resources + +- **Keycloak Setup Guide:** `KEYCLOAK-SETUP-GUIDE.md` +- **Caddy + fail2ban Setup:** `CADDY-FAIL2BAN-SETUP.md` +- **Main Script:** `ubuntu-post-install.sh` +- **Caddy Helper:** `caddy-setup-helper.sh` + +--- + +## Support + +If you encounter issues: + +1. Check logs: `docker logs ` +2. Verify ownership: `ls -la ~/docker` +3. Review this document for troubleshooting steps +4. Check existing documentation in repository + +--- + +**Last Updated:** 2026-01-13 +**Script Version:** Latest (with security improvements) diff --git a/ubuntu-post-install.sh b/ubuntu-post-install.sh index 93ada3c..10215ca 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -306,6 +306,7 @@ run_disaster_recovery() { echo "" mkdir -p "$DOCKER_DIR" + ensure_docker_dir_ownership "$DOCKER_DIR" # Find all docker-compose.yml files in restored backup declare -A SERVICE_DIRS @@ -1306,6 +1307,51 @@ run_cmd() { fi } +# Ensure Docker directories are owned by the actual user (not root) +# Usage: ensure_docker_dir_ownership /path/to/dir [additional_paths...] +ensure_docker_dir_ownership() { + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would set ownership of $* to $ACTUAL_USER:$ACTUAL_USER" + return 0 + fi + + for dir in "$@"; do + if [ -d "$dir" ]; then + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$dir" 2>/dev/null || true + fi + done +} + +# Generate a secure random password with alphanumeric characters only (no special chars) +# Usage: generate_password [length] +# Default length: 32 +generate_password() { + local length="${1:-32}" + openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c "$length" +} + +# Validate password for Keycloak (alphanumeric only, minimum length) +# Usage: validate_password "password" [min_length] +# Returns 0 if valid, 1 if invalid +validate_password() { + local password="$1" + local min_length="${2:-12}" + + # Check length + if [ ${#password} -lt "$min_length" ]; then + echo " ⚠ Password must be at least $min_length characters long" + return 1 + fi + + # Check for special characters (not allowed for Keycloak) + if echo "$password" | grep -q '[^a-zA-Z0-9]'; then + echo " ⚠ Password must contain only letters and numbers (no special characters)" + return 1 + fi + + return 0 +} + # Prompt for yes/no, with unattended default # Usage: prompt_yn "Question?" "default" VARNAME # default can be "y" or "n" @@ -2989,6 +3035,7 @@ IMMICH_ENV echo "[DRY-RUN] Would create $ABS_DIR" else mkdir -p "$ABS_DIR" + ensure_docker_dir_ownership "$ABS_DIR" cd "$ABS_DIR" prompt_text "Path to audiobooks folder [default: $PRIMARY_DRIVE_PATH/audiobooks]:" "$PRIMARY_DRIVE_PATH/audiobooks" AUDIOBOOKS_PATH @@ -3062,6 +3109,7 @@ ABS_ENV echo "[DRY-RUN] Would create $EMBY_DIR" else mkdir -p "$EMBY_DIR" + ensure_docker_dir_ownership "$EMBY_DIR" cd "$EMBY_DIR" prompt_text "Path to media folder [default: $PRIMARY_DRIVE_PATH/media]:" "$PRIMARY_DRIVE_PATH/media" MEDIA_PATH @@ -3136,6 +3184,7 @@ EMBY_ENV echo "[DRY-RUN] Would create $ARM_DIR" else mkdir -p "$ARM_DIR" + ensure_docker_dir_ownership "$ARM_DIR" cd "$ARM_DIR" prompt_text "Path for ripped media output [default: $PRIMARY_DRIVE_PATH/ripped]:" "$PRIMARY_DRIVE_PATH/ripped" ARM_OUTPUT @@ -3224,6 +3273,7 @@ ARM_ENV echo "[DRY-RUN] Would create $FB_DIR" else mkdir -p "$FB_DIR" + ensure_docker_dir_ownership "$FB_DIR" cd "$FB_DIR" prompt_text "Path to browse [default: $PRIMARY_DRIVE_PATH]:" "$PRIMARY_DRIVE_PATH" FB_PATH @@ -3311,6 +3361,7 @@ FB_SETTINGS echo "[DRY-RUN] Would create $MM_DIR (port $MM_PORT)" else mkdir -p "$MM_DIR" + ensure_docker_dir_ownership "$MM_DIR" cd "$MM_DIR" cat > docker-compose.yml << MM_COMPOSE @@ -3537,8 +3588,15 @@ MM_CONFIG else echo "Installing ActualBudget..." mkdir -p "$AB_DIR/data" + ensure_docker_dir_ownership "$AB_DIR" cd "$AB_DIR" + # Create .env file for environment variables + cat > .env << 'AB_ENV' +# ActualBudget Environment Variables +TZ=UTC +AB_ENV + cat > docker-compose.yml << 'AB_COMPOSE' name: actualbudget @@ -3551,8 +3609,8 @@ services: - "5006:5006" volumes: - ./data:/data - environment: - - TZ=UTC + env_file: + - .env labels: - "io.podman.annotations.label/fail2ban.enable=true" - "io.podman.annotations.label/fail2ban.filter=caddy-auth" @@ -3596,19 +3654,124 @@ AB_COMPOSE else echo "Installing Keycloak..." echo "" - echo "⚠ SECURITY WARNING:" - echo " You MUST change the default admin password!" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "PASSWORD REQUIREMENTS:" + echo " • Minimum 12 characters (16+ recommended)" + echo " • Letters and numbers ONLY (no special characters)" + echo " • Press ENTER for secure auto-generated password" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" - read -p "Enter Keycloak admin password [admin123]: " KC_ADMIN_PASS - KC_ADMIN_PASS=${KC_ADMIN_PASS:-admin123} + # Prompt for admin password with validation + KC_ADMIN_PASS="" + while true; do + read -s -p "Enter Keycloak admin password [auto-generate]: " KC_ADMIN_PASS + echo "" - read -p "Enter database password [keycloak_db_pass]: " KC_DB_PASS - KC_DB_PASS=${KC_DB_PASS:-keycloak_db_pass} + # Generate secure password if user pressed Enter + if [ -z "$KC_ADMIN_PASS" ]; then + KC_ADMIN_PASS=$(generate_password 20) + echo " ✓ Generated secure admin password (saved in .env)" + break + fi + + # Validate password + if validate_password "$KC_ADMIN_PASS" 12; then + echo " ✓ Admin password accepted" + break + fi + echo " Please try again." + done + + # Prompt for database password with validation + KC_DB_PASS="" + while true; do + read -s -p "Enter database password [auto-generate]: " KC_DB_PASS + echo "" + + # Generate secure password if user pressed Enter + if [ -z "$KC_DB_PASS" ]; then + KC_DB_PASS=$(generate_password 32) + echo " ✓ Generated secure database password (saved in .env)" + break + fi + + # Validate password + if validate_password "$KC_DB_PASS" 12; then + echo " ✓ Database password accepted" + break + fi + echo " Please try again." + done + echo "" + + # Ask about production vs development mode + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "DEPLOYMENT MODE:" + echo " • Production: Requires HTTPS via Caddy2 (recommended)" + echo " • Development: HTTP only, relaxed security (testing only)" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + prompt_yn "Use production mode? (requires Caddy2 reverse proxy) (y/n):" "y" KC_PRODUCTION + + KC_HOSTNAME="" + KC_START_CMD="start-dev" + KC_HOSTNAME_STRICT="false" + + if [ "$KC_PRODUCTION" = "y" ] || [ "$KC_PRODUCTION" = "Y" ]; then + KC_START_CMD="start" + KC_HOSTNAME_STRICT="false" + + echo "" + echo "Enter your Keycloak hostname (e.g., auth.yourdomain.com)" + echo "This should match your Caddy2 configuration." + read -p "Hostname: " KC_HOSTNAME + + if [ -n "$KC_HOSTNAME" ]; then + echo " ✓ Production mode enabled with hostname: $KC_HOSTNAME" + echo " ⚠ Make sure Caddy2 is configured for this domain!" + else + echo " ⚠ No hostname provided - using relaxed mode" + KC_HOSTNAME="" + fi + fi mkdir -p "$KC_DIR/data" "$KC_DIR/postgres-data" + ensure_docker_dir_ownership "$KC_DIR" cd "$KC_DIR" + # Create .env file for sensitive credentials + cat > .env << KC_ENV +# Keycloak Environment Variables +# ⚠ KEEP THIS FILE SECURE - Contains sensitive passwords + +# Admin Credentials +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=$KC_ADMIN_PASS + +# Database Credentials +POSTGRES_DB=keycloak +POSTGRES_USER=keycloak +POSTGRES_PASSWORD=$KC_DB_PASS +KC_DB=postgres +KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak +KC_DB_USERNAME=keycloak +KC_DB_PASSWORD=$KC_DB_PASS + +# Keycloak Configuration +KC_PROXY=edge +KC_HTTP_ENABLED=true +KC_HOSTNAME_STRICT=$KC_HOSTNAME_STRICT +KC_LOG_LEVEL=INFO +KC_HEALTH_ENABLED=true +KC_METRICS_ENABLED=true +KC_ENV + + # Add hostname to .env if provided + if [ -n "$KC_HOSTNAME" ]; then + echo "KC_HOSTNAME=$KC_HOSTNAME" >> .env + fi + + # Create docker-compose.yml cat > docker-compose.yml << KC_COMPOSE name: keycloak @@ -3617,10 +3780,8 @@ services: image: postgres:16-alpine container_name: keycloak-db restart: unless-stopped - environment: - POSTGRES_DB: keycloak - POSTGRES_USER: keycloak - POSTGRES_PASSWORD: $KC_DB_PASS + env_file: + - .env volumes: - ./postgres-data:/var/lib/postgresql/data healthcheck: @@ -3634,20 +3795,9 @@ services: container_name: keycloak restart: unless-stopped command: - - start-dev - environment: - - KEYCLOAK_ADMIN=admin - - KEYCLOAK_ADMIN_PASSWORD=$KC_ADMIN_PASS - - KC_DB=postgres - - KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak - - KC_DB_USERNAME=keycloak - - KC_DB_PASSWORD=$KC_DB_PASS - - KC_HOSTNAME_STRICT=false - - KC_PROXY=edge - - KC_HTTP_ENABLED=true - - KC_LOG_LEVEL=INFO - - KC_HEALTH_ENABLED=true - - KC_METRICS_ENABLED=true + - $KC_START_CMD + env_file: + - .env ports: - "8180:8080" volumes: @@ -3660,7 +3810,15 @@ services: - "io.podman.annotations.label/fail2ban.filter=caddy-auth" KC_COMPOSE + echo "" echo " ✓ Keycloak configured at $KC_DIR" + echo " ✓ Credentials saved in .env file" + if [ "$KC_PRODUCTION" = "y" ] || [ "$KC_PRODUCTION" = "Y" ]; then + echo " ✓ Production mode enabled" + else + echo " ℹ Development mode (use production mode for internet-facing deployments)" + fi + echo "" # If Caddy is installed/being installed, offer to configure it for Keycloak if [ "$INSTALL_CADDY" = "y" ] || [ "$INSTALL_CADDY" = "Y" ] || [ -d "$DOCKER_DIR/caddy" ]; then @@ -4126,6 +4284,7 @@ EOF else echo "Installing Caddy..." mkdir -p "$CADDY_DIR/data" "$CADDY_DIR/config" + ensure_docker_dir_ownership "$CADDY_DIR" # Backup existing Caddyfile if it exists if [ -f "$CADDY_DIR/Caddyfile" ]; then @@ -4386,6 +4545,7 @@ backend = auto" echo "[DRY-RUN] Would create $LMS_DIR" else mkdir -p "$LMS_DIR" + ensure_docker_dir_ownership "$LMS_DIR" cd "$LMS_DIR" prompt_text "Path to music folder [default: $PRIMARY_DRIVE_PATH/music]:" "$PRIMARY_DRIVE_PATH/music" MUSIC_PATH @@ -4452,6 +4612,7 @@ LMS_ENV echo "[DRY-RUN] Would create $MEALIE_DIR" else mkdir -p "$MEALIE_DIR" + ensure_docker_dir_ownership "$MEALIE_DIR" cd "$MEALIE_DIR" cat > docker-compose.yml << MEALIE_COMPOSE @@ -4516,6 +4677,7 @@ MEALIE_COMPOSE echo "[DRY-RUN] Would create $MC_DIR" else mkdir -p "$MC_DIR" + ensure_docker_dir_ownership "$MC_DIR" cd "$MC_DIR" echo "" @@ -4674,6 +4836,7 @@ MC_ENV echo "[DRY-RUN] Would create $JELLYFIN_DIR" else mkdir -p "$JELLYFIN_DIR" + ensure_docker_dir_ownership "$JELLYFIN_DIR" cd "$JELLYFIN_DIR" prompt_text "Path to media folder [default: $PRIMARY_DRIVE_PATH/media]:" "$PRIMARY_DRIVE_PATH/media" MEDIA_PATH @@ -4750,6 +4913,7 @@ JELLYFIN_ENV else # STEP 1: Create directory and install docker-compose mkdir -p "$FRIGATE_DIR" 2>/dev/null || true + ensure_docker_dir_ownership "$FRIGATE_DIR" cd "$FRIGATE_DIR" 2>/dev/null || cd "$DOCKER_DIR" # Default path @@ -5161,6 +5325,7 @@ DDCLIENT_CONF echo "[DRY-RUN] Would create $NTFY_DIR" else mkdir -p "$NTFY_DIR" + ensure_docker_dir_ownership "$NTFY_DIR" cd "$NTFY_DIR" cat > docker-compose.yml << 'NTFY_COMPOSE' @@ -5224,6 +5389,7 @@ NTFY_ENV echo "[DRY-RUN] Would create $UPTIME_DIR" else mkdir -p "$UPTIME_DIR" + ensure_docker_dir_ownership "$UPTIME_DIR" cd "$UPTIME_DIR" cat > docker-compose.yml << 'UPTIME_COMPOSE' @@ -5280,6 +5446,7 @@ UPTIME_COMPOSE echo "[DRY-RUN] Would create $WGEASY_DIR" else mkdir -p "$WGEASY_DIR" + ensure_docker_dir_ownership "$WGEASY_DIR" cd "$WGEASY_DIR" # Get public IP or hostname @@ -5356,6 +5523,7 @@ WGEASY_ENV echo "[DRY-RUN] Would create $TRACCAR_DIR" else mkdir -p "$TRACCAR_DIR" + ensure_docker_dir_ownership "$TRACCAR_DIR" cd "$TRACCAR_DIR" cat > docker-compose.yml << 'TRACCAR_COMPOSE' @@ -5429,6 +5597,7 @@ TRACCAR_XML echo "[DRY-RUN] Would create $PORTAINER_DIR" else mkdir -p "$PORTAINER_DIR" + ensure_docker_dir_ownership "$PORTAINER_DIR" cd "$PORTAINER_DIR" cat > docker-compose.yml << 'PORTAINER_COMPOSE' @@ -5484,6 +5653,7 @@ PORTAINER_COMPOSE echo "[DRY-RUN] Would create $MC_DIR" else mkdir -p "$MC_DIR" 2>/dev/null || true + ensure_docker_dir_ownership "$MC_DIR" cd "$MC_DIR" 2>/dev/null || cd "$DOCKER_DIR" cat > docker-compose.yml << 'MC_COMPOSE' @@ -5564,6 +5734,7 @@ MC_ENV echo "[DRY-RUN] Would create $FMD_DIR" else mkdir -p "$FMD_DIR" + ensure_docker_dir_ownership "$FMD_DIR" cd "$FMD_DIR" # Generate random admin password @@ -5630,6 +5801,7 @@ FMD_ENV else # STEP 1: Create directory and docker-compose (always succeeds) mkdir -p "$FN_DIR" 2>/dev/null || true + ensure_docker_dir_ownership "$FN_DIR" cd "$FN_DIR" 2>/dev/null || cd "$DOCKER_DIR" cat > docker-compose.yml << 'FN_COMPOSE' @@ -5734,6 +5906,7 @@ FN_CONFIG echo "[DRY-RUN] Would create $WT_DIR" else mkdir -p "$WT_DIR" 2>/dev/null || true + ensure_docker_dir_ownership "$WT_DIR" cd "$WT_DIR" 2>/dev/null || cd "$DOCKER_DIR" # Ask about mode @@ -5871,6 +6044,7 @@ WT_ENV echo "[DRY-RUN] Would create $KOPIA_DIR" else mkdir -p "$KOPIA_DIR" + ensure_docker_dir_ownership "$KOPIA_DIR" cd "$KOPIA_DIR" KOPIA_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)