diff --git a/.gitignore b/.gitignore index 1ad1e04..ecd6958 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,9 @@ yarn-error.log* *.pfx acme.json +# Docker TLS certificates directory +docker-tls/ + # Nextcloud application files (should be mounted via volumes) docker-compose/productivity/nextcloud/html/ diff --git a/docker-compose/core/docker-compose.yml b/docker-compose/core/docker-compose.yml index fb41a57..256ae09 100644 --- a/docker-compose/core/docker-compose.yml +++ b/docker-compose/core/docker-compose.yml @@ -99,10 +99,10 @@ services: # Sablier - Lazy loading service for Docker containers # Controls startup/shutdown of lazy-loaded services, must always run # REQUIREMENTS FOR DOCKER API ACCESS: - # 1. Docker daemon must be configured to listen on TCP port 2375 + # 1. Docker daemon must be configured to listen on TCP port 2376 with TLS # 2. DOCKER_HOST environment variable must point to accessible Docker API endpoint - # 3. Firewall must allow TCP connections to Docker API port (default 2375) - # 4. For production, consider using TLS for Docker API communication + # 3. Firewall must allow TCP connections to Docker API port (2376) + # 4. TLS certificates must be mounted and environment variables set # 5. Ensure dockerproxy service is running and accessible sablier-service: image: sablierapp/sablier:latest @@ -115,7 +115,11 @@ services: - SABLIER_DOCKER_API_VERSION=1.51 - SABLIER_DOCKER_NETWORK=traefik-network - SABLIER_LOG_LEVEL=debug - - DOCKER_HOST=tcp://192.168.4.11:2375 + - DOCKER_HOST=tcp://${SERVER_IP}:2376 + - DOCKER_TLS_VERIFY=1 + - DOCKER_CERT_PATH=/certs + volumes: + - ./sablier-certs:/certs:ro ports: - 10000:10000 labels: diff --git a/docker-compose/media-management/docker-compose.yml b/docker-compose/media-management/docker-compose.yml index 5af1e7d..77a9079 100644 --- a/docker-compose/media-management/docker-compose.yml +++ b/docker-compose/media-management/docker-compose.yml @@ -28,7 +28,7 @@ services: - PGID=${PGID} - TZ=${TZ} healthcheck: - test: ["CMD", "curl", "-f", "http://${SERVER_IP}:8989/"] + test: ["CMD", "curl", "-f", "http://localhost:8989/"] interval: 30s timeout: 10s retries: 3 @@ -73,7 +73,7 @@ services: - PGID=${PGID} - TZ=${TZ} healthcheck: - test: ["CMD", "curl", "-f", "http://${SERVER_IP}:7878/"] + test: ["CMD", "curl", "-f", "http://localhost:7878/"] interval: 30s timeout: 10s retries: 3 @@ -116,7 +116,7 @@ services: - PGID=${PGID} - TZ=${TZ} healthcheck: - test: ["CMD", "curl", "-f", "http://${SERVER_IP}:9696/"] + test: ["CMD", "curl", "-f", "http://localhost:9696/"] interval: 30s timeout: 10s retries: 3 @@ -315,7 +315,7 @@ services: - LOG_LEVEL=info - TZ=${TZ} healthcheck: - test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://${SERVER_IP}:5055/"] + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:5055/"] interval: 30s timeout: 10s retries: 3 diff --git a/docs/Ondemand-Remote-Services.md b/docs/Ondemand-Remote-Services.md index 39b5b94..4c87c36 100644 --- a/docs/Ondemand-Remote-Services.md +++ b/docs/Ondemand-Remote-Services.md @@ -115,3 +115,302 @@ docker stop Access your service by the proxy url. +--- + +# Deployment Plan for Multi-Server Setup + +This section provides a complete deployment plan for scenarios where the core infrastructure (Traefik, Authelia, Sablier) runs on one server, and application services run on remote servers. This setup enables centralized control and routing while maintaining service isolation. + +## Architecture Overview + +- **Core Server**: Hosts Traefik (reverse proxy), Authelia (SSO), Sablier (lazy loading controller) +- **Remote/Media Servers**: Host application containers controlled by Sablier +- **Communication**: TLS-secured Docker API calls between servers + +## Prerequisites + +- Both servers must be on the same network and able to communicate +- SSH access configured between servers (passwordless recommended for automation) +- Domain configured with DuckDNS or similar +- The EZ-Homelab script handles most Docker TLS and certificate setup automatically +- Basic understanding of Docker concepts (optional - script guides you through setup) + +## Step 1: Configure Docker TLS on All Servers + +### On Each Server (Core and Remote) + +1. **Install Docker** (if not already installed): + ```bash + curl -fsSL https://get.docker.com | sh + usermod -aG docker $USER + systemctl enable docker + systemctl start docker + # Log out and back in for group changes + ``` + +2. **Generate TLS Certificates**: + ```bash + mkdir -p ~/EZ-Homelab/docker-tls + cd ~/EZ-Homelab/docker-tls + + # Generate CA + openssl genrsa -out ca-key.pem 4096 + openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem -subj "/C=US/ST=State/L=City/O=Organization/CN=Docker-CA" + + # Generate server key and cert (replace SERVER_IP with actual IP) + openssl genrsa -out server-key.pem 4096 + openssl req -subj "/CN=" -new -key server-key.pem -out server.csr + echo "subjectAltName = DNS:,IP:,IP:127.0.0.1" > extfile.cnf + openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile extfile.cnf + + # Generate client key and cert + openssl genrsa -out client-key.pem 4096 + openssl req -subj "/CN=client" -new -key client-key.pem -out client.csr + openssl x509 -req -days 365 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem + ``` + +3. **Configure Docker Daemon**: + Create `/etc/docker/daemon.json`: + ```json + { + "tls": true, + "tlsverify": true, + "tlscacert": "/home/$USER/EZ-Homelab/docker-tls/ca.pem", + "tlscert": "/home/$USER/EZ-Homelab/docker-tls/server-cert.pem", + "tlskey": "/home/$USER/EZ-Homelab/docker-tls/server-key.pem" + } + ``` + +4. **Update Systemd Service**: + ```bash + sudo sed -i 's|-H fd://|-H fd:// -H tcp://0.0.0.0:2376|' /lib/systemd/system/docker.service + sudo systemctl daemon-reload + sudo systemctl restart docker + ``` + +5. **Configure Firewall**: + ```bash + sudo ufw allow 2376/tcp + sudo ufw --force enable + ``` + +## Certificate and Secret Sharing + +The EZ-Homelab script automatically handles certificate and secret sharing for infrastructure-only deployments: + +### Automatic Process (Recommended) + +1. **On Remote Server**: Run `./scripts/ez-homelab.sh` and select option 3 +2. **Script Actions**: + - Prompts for core server IP + - Tests SSH connectivity + - Copies Docker TLS certificates for remote control + - Sets up certificates in the correct location + +### Manual Process (Fallback) + +If automatic sharing fails, manually share certificates: + +1. **On Core Server**: + ```bash + # Copy client certificates to remote server + scp /opt/stacks/core/docker-tls/ca.pem /opt/stacks/core/docker-tls/client-cert.pem /opt/stacks/core/docker-tls/client-key.pem user@remote-server:/opt/stacks/infrastructure/docker-tls/ + ``` + +2. **On Remote Server**: + ```bash + # Ensure certificates are in the correct location + ls -la /opt/stacks/infrastructure/docker-tls/ + # Should contain: ca.pem, client-cert.pem, client-key.pem + ``` + +## Step 3: Deploy Core Infrastructure + +### On Core Server + +1. **Run the EZ-Homelab script** with core deployment: + ```bash + cd ~/EZ-Homelab + ./scripts/ez-homelab.sh + # Select option 1 (Default Setup) or 2 (Core Only) + ``` + + The script will: + - Generate Authelia secrets automatically + - Configure TLS for Docker API + - Deploy Traefik, Authelia, and Sablier + - Set up certificates for secure communication + +2. **Verify core deployment**: + ```bash + # Check services are running + docker ps --filter "label=com.docker.compose.project=core" + + # Test Authelia access + curl -k https://auth. + ``` + +## Step 4: Deploy Remote Infrastructure + +### On Remote/Media Server + +1. **Run the EZ-Homelab script** with infrastructure-only deployment: + ```bash + cd ~/EZ-Homelab + ./scripts/ez-homelab.sh + # Select option 3 (Infrastructure Only) + ``` + + The script will automatically: + - Prompt for core server IP address + - Establish SSH connection to core server + - Copy Authelia secrets and TLS certificates + - Configure Docker TLS for remote control + - Set up required networks and directories + +2. **Manual certificate sharing** (if automatic fails): + If SSH connection fails, manually copy certificates: + ```bash + # On core server, copy certs to remote server + scp /opt/stacks/core/docker-tls/ca.pem /opt/stacks/core/docker-tls/client-cert.pem /opt/stacks/core/docker-tls/client-key.pem user@remote-server:/opt/stacks/infrastructure/docker-tls/ + + # On remote server, copy Authelia secrets + scp /home/kelin/EZ-Homelab/.env user@remote-server:/home/kelin/EZ-Homelab/.env.core + ``` + +## Step 5: Configure Sablier for Remote Control + +### On Core Server + +Update Sablier configuration to control remote servers: + +1. **Edit core docker-compose.yml**: + ```yaml + sablier-service: + environment: + - DOCKER_HOST=tcp://:2376 + - DOCKER_TLS_VERIFY=1 + - DOCKER_CERT_PATH=/certs + volumes: + - ./docker-tls/ca.pem:/certs/ca.pem:ro + - ./docker-tls/client-cert.pem:/certs/cert.pem:ro + - ./docker-tls/client-key.pem:/certs/key.pem:ro + ``` + +2. **Restart core stack**: + ```bash + cd /opt/stacks/core + docker compose down + docker compose up -d + ``` + +## Step 6: Deploy Application Services + +### On Remote Server + +1. **Deploy application stacks** with Sablier labels: + ```yaml + # Example: /opt/stacks/media-management/docker-compose.yml + services: + sonarr: + labels: + - sablier.enable=true + - sablier.group=-media + - sablier.start-on-demand=true + ``` + +2. **Deploy and stop services** for lazy loading: + ```bash + cd /opt/stacks/media-management + docker compose up -d + docker compose stop + ``` + +## Step 5: Configure Traefik Routing + +### On Core Server + +Since Traefik cannot auto-discover labels from remote Docker hosts, use the file provider method: + +1. **Create external host configuration**: + `/opt/stacks/core/traefik/dynamic/external-host-.yml` + ```yaml + http: + routers: + sonarr-remote: + rule: "Host(`sonarr.`)" + entrypoints: + - websecure + service: sonarr-remote + tls: + certResolver: letsencrypt + middlewares: + - sablier--arr@file + - authelia@docker + + services: + sonarr-remote: + loadBalancer: + servers: + - url: "http://:8989" + passHostHeader: true + ``` + +2. **Create Sablier middleware configuration**: + `/opt/stacks/core/traefik/dynamic/sablier.yml` + ```yaml + http: + middlewares: + sablier--arr: + plugin: + sablier: + sablierUrl: http://sablier-service:10000 + group: -arr + sessionDuration: 2m + ignoreUserAgent: curl + dynamic: + displayName: "Media Management Services" + theme: ghost + show-details-by-default: true + ``` + +3. **Restart Traefik**: + ```bash + docker restart traefik + ``` + +## Step 6: Verification and Testing + +1. **Check Sablier connection**: + ```bash + # On core server + docker logs sablier-service + # Should show groups from remote server + ``` + +2. **Test lazy loading**: + - Access `https://sonarr.` + - Should show Sablier loading page + - Container should start on remote server + +3. **Verify Traefik routes**: + ```bash + curl -k https://localhost:8080/api/http/routers | jq + ``` + +## Troubleshooting + +- **TLS Connection Issues**: Check certificate validity and paths +- **Sablier Not Detecting Groups**: Verify DOCKER_HOST and certificates +- **Traefik Routing Problems**: Check external host YAML syntax +- **Network Connectivity**: Ensure ports 2376, 80, 443 are open between servers + +## Security Considerations + +- TLS certificates expire after 365 days - monitor and renew +- Limit Docker API access to trusted networks +- Use strong firewall rules +- Regularly update all components + +This setup provides centralized management with distributed execution, optimal for resource management and security. + diff --git a/scripts/ez-homelab.sh b/scripts/ez-homelab.sh index 9de0a97..e681a19 100755 --- a/scripts/ez-homelab.sh +++ b/scripts/ez-homelab.sh @@ -33,6 +33,13 @@ log_error() { SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" REPO_DIR="$( cd "$SCRIPT_DIR/.." && pwd )" +# Get actual user +if [ "$EUID" -eq 0 ]; then + ACTUAL_USER=${SUDO_USER:-$USER} +else + ACTUAL_USER=$USER +fi + # Default values DOMAIN="" SERVER_IP="" @@ -56,8 +63,12 @@ load_env_file() { echo " Domain: ${DOMAIN:-Not set}" echo " Server IP: ${SERVER_IP:-Not set}" echo " Server Hostname: ${SERVER_HOSTNAME:-Not set}" - echo " Admin User: ${AUTHELIA_ADMIN_USER:-Not set}" - echo " Admin Email: ${AUTHELIA_ADMIN_EMAIL:-Not set}" + echo " Default User: ${DEFAULT_USER:-Not set}" + if [ -n "${DEFAULT_PASSWORD:-}" ]; then + echo " Default Password: [HIDDEN]" + else + echo " Default Password: Not set" + fi echo " Timezone: ${TZ:-Not set}" echo "" @@ -74,27 +85,29 @@ save_env_file() { # Create .env file if it doesn't exist if [ ! -f "$REPO_DIR/.env" ]; then - cp "$REPO_DIR/.env.example" "$REPO_DIR/.env" + sudo -u "$ACTUAL_USER" cp "$REPO_DIR/.env.example" "$REPO_DIR/.env" fi - # Update values - sed -i "s%DOMAIN=.*%DOMAIN=$DOMAIN%" "$REPO_DIR/.env" - sed -i "s%SERVER_IP=.*%SERVER_IP=$SERVER_IP%" "$REPO_DIR/.env" - sed -i "s%SERVER_HOSTNAME=.*%SERVER_HOSTNAME=$SERVER_HOSTNAME%" "$REPO_DIR/.env" - sed -i "s%TZ=.*%TZ=$TZ%" "$REPO_DIR/.env" + # Update values as the actual user + sudo -u "$ACTUAL_USER" sed -i "s%DOMAIN=.*%DOMAIN=$DOMAIN%" "$REPO_DIR/.env" + sudo -u "$ACTUAL_USER" sed -i "s%SERVER_IP=.*%SERVER_IP=$SERVER_IP%" "$REPO_DIR/.env" + sudo -u "$ACTUAL_USER" sed -i "s%SERVER_HOSTNAME=.*%SERVER_HOSTNAME=$SERVER_HOSTNAME%" "$REPO_DIR/.env" + sudo -u "$ACTUAL_USER" sed -i "s%TZ=.*%TZ=$TZ%" "$REPO_DIR/.env" - # Authelia settings (only if deploying core) + # Authelia settings (only generate secrets if deploying core) if [ "$DEPLOY_CORE" = true ]; then # Ensure we have admin credentials if [ -z "$ADMIN_USER" ]; then - ADMIN_USER="admin" + ADMIN_USER="${DEFAULT_USER:-admin}" fi if [ -z "$ADMIN_EMAIL" ]; then - ADMIN_EMAIL="${ADMIN_USER}@${DOMAIN}" + ADMIN_EMAIL="${DEFAULT_EMAIL:-${ADMIN_USER}@${DOMAIN}}" fi if [ -z "$ADMIN_PASSWORD" ]; then - log_info "Using default admin password (changeme123) - please change this after setup!" - ADMIN_PASSWORD="changeme123" + ADMIN_PASSWORD="${DEFAULT_PASSWORD:-changeme123}" + if [ "$ADMIN_PASSWORD" = "changeme123" ]; then + log_info "Using default admin password (changeme123) - please change this after setup!" + fi fi if [ -z "$AUTHELIA_JWT_SECRET" ]; then @@ -108,11 +121,11 @@ save_env_file() { fi # Save Authelia settings to .env - sed -i "s%AUTHELIA_JWT_SECRET=.*%AUTHELIA_JWT_SECRET=$AUTHELIA_JWT_SECRET%" "$REPO_DIR/.env" - sed -i "s%AUTHELIA_SESSION_SECRET=.*%AUTHELIA_SESSION_SECRET=$AUTHELIA_SESSION_SECRET%" "$REPO_DIR/.env" - sed -i "s%AUTHELIA_STORAGE_ENCRYPTION_KEY=.*%AUTHELIA_STORAGE_ENCRYPTION_KEY=$AUTHELIA_STORAGE_ENCRYPTION_KEY%" "$REPO_DIR/.env" - sed -i "s%# AUTHELIA_ADMIN_USER=.*%AUTHELIA_ADMIN_USER=$ADMIN_USER%" "$REPO_DIR/.env" - sed -i "s%# AUTHELIA_ADMIN_EMAIL=.*%AUTHELIA_ADMIN_EMAIL=$ADMIN_EMAIL%" "$REPO_DIR/.env" + sudo -u "$ACTUAL_USER" sed -i "s%AUTHELIA_JWT_SECRET=.*%AUTHELIA_JWT_SECRET=$AUTHELIA_JWT_SECRET%" "$REPO_DIR/.env" + sudo -u "$ACTUAL_USER" sed -i "s%AUTHELIA_SESSION_SECRET=.*%AUTHELIA_SESSION_SECRET=$AUTHELIA_SESSION_SECRET%" "$REPO_DIR/.env" + sudo -u "$ACTUAL_USER" sed -i "s%AUTHELIA_STORAGE_ENCRYPTION_KEY=.*%AUTHELIA_STORAGE_ENCRYPTION_KEY=$AUTHELIA_STORAGE_ENCRYPTION_KEY%" "$REPO_DIR/.env" + sudo -u "$ACTUAL_USER" sed -i "s%# AUTHELIA_ADMIN_USER=.*%AUTHELIA_ADMIN_USER=$ADMIN_USER%" "$REPO_DIR/.env" + sudo -u "$ACTUAL_USER" sed -i "s%# AUTHELIA_ADMIN_EMAIL=.*%AUTHELIA_ADMIN_EMAIL=$ADMIN_EMAIL%" "$REPO_DIR/.env" # Generate password hash if needed if [ -z "$AUTHELIA_ADMIN_PASSWORD" ]; then @@ -129,8 +142,8 @@ save_env_file() { fi # Save password hash - sed -i "s%# AUTHELIA_ADMIN_PASSWORD=.*%AUTHELIA_ADMIN_PASSWORD=$AUTHELIA_ADMIN_PASSWORD%" "$REPO_DIR/.env" - sed -i "s%AUTHELIA_ADMIN_PASSWORD=.*%AUTHELIA_ADMIN_PASSWORD=$AUTHELIA_ADMIN_PASSWORD%" "$REPO_DIR/.env" + sudo -u "$ACTUAL_USER" sed -i "s%# AUTHELIA_ADMIN_PASSWORD=.*%AUTHELIA_ADMIN_PASSWORD=$AUTHELIA_ADMIN_PASSWORD%" "$REPO_DIR/.env" + sudo -u "$ACTUAL_USER" sed -i "s%AUTHELIA_ADMIN_PASSWORD=.*%AUTHELIA_ADMIN_PASSWORD=$AUTHELIA_ADMIN_PASSWORD%" "$REPO_DIR/.env" fi log_success "Configuration saved to .env file" @@ -139,79 +152,192 @@ save_env_file() { # Prompt for required values prompt_for_values() { echo "" - log_info "Please provide the following information:" - echo " (Press Enter without typing to keep the current/default value shown in brackets)" + log_info "Configuration Setup:" echo "" - # Domain - if [ -z "$DOMAIN" ]; then - read -p "Enter your domain (e.g., example.duckdns.org): " DOMAIN - while [ -z "$DOMAIN" ]; do - log_warning "Domain is required" - read -p "Enter your domain (e.g., example.duckdns.org): " DOMAIN - done - else - read -p "Domain [$DOMAIN] (press Enter to keep current): " input - [ -n "$input" ] && DOMAIN="$input" - fi + # Set defaults from env file or hardcoded fallbacks + DEFAULT_DOMAIN="${DOMAIN:-example.duckdns.org}" + DEFAULT_SERVER_IP="${SERVER_IP:-$(hostname -I | awk '{print $1}')}" + DEFAULT_SERVER_HOSTNAME="${SERVER_HOSTNAME:-$(hostname)}" + DEFAULT_TZ="${TZ:-America/New_York}" - # Server IP - if [ -z "$SERVER_IP" ]; then - read -p "Enter your server IP address: " SERVER_IP - while [ -z "$SERVER_IP" ]; do - log_warning "Server IP is required" - read -p "Enter your server IP address: " SERVER_IP - done - else - read -p "Server IP [$SERVER_IP] (press Enter to keep current): " input - [ -n "$input" ] && SERVER_IP="$input" - fi + # Display current/default configuration + echo "Please review the following configuration:" + echo " Domain: $DEFAULT_DOMAIN" + echo " Server IP: $DEFAULT_SERVER_IP" + echo " Server Hostname: $DEFAULT_SERVER_HOSTNAME" + echo " Timezone: $DEFAULT_TZ" - # Server Hostname - if [ -z "$SERVER_HOSTNAME" ]; then - SERVER_HOSTNAME="debian" - fi - read -p "Server hostname [$SERVER_HOSTNAME] (press Enter to keep current): " input - [ -n "$input" ] && SERVER_HOSTNAME="$input" - - # Timezone - if [ -z "$TZ" ]; then - TZ="America/New_York" - fi - read -p "Timezone [$TZ] (press Enter to keep current): " input - [ -n "$input" ] && TZ="$input" - - # Admin credentials (only if deploying core) if [ "$DEPLOY_CORE" = true ]; then + DEFAULT_ADMIN_USER="${DEFAULT_USER:-admin}" + DEFAULT_ADMIN_EMAIL="${DEFAULT_EMAIL:-${DEFAULT_ADMIN_USER}@${DEFAULT_DOMAIN}}" + echo " Admin User: $DEFAULT_ADMIN_USER" + echo " Admin Email: $DEFAULT_ADMIN_EMAIL" + echo " Admin Password: [Will be prompted if needed]" + fi + + echo "" + read -p "Use these default values? (Y/n): " -n 1 -r + echo "" + if [[ $REPLY =~ ^[Nn]$ ]]; then + echo "Please enter custom values:" echo "" - log_info "Authelia Admin Credentials:" - if [ -z "$ADMIN_USER" ]; then - ADMIN_USER="admin" + # Domain + read -p "Domain [$DEFAULT_DOMAIN]: " DOMAIN + DOMAIN="${DOMAIN:-$DEFAULT_DOMAIN}" + + # Server IP + read -p "Server IP [$DEFAULT_SERVER_IP]: " SERVER_IP + SERVER_IP="${SERVER_IP:-$DEFAULT_SERVER_IP}" + + # Server Hostname + read -p "Server Hostname [$DEFAULT_SERVER_HOSTNAME]: " SERVER_HOSTNAME + SERVER_HOSTNAME="${SERVER_HOSTNAME:-$DEFAULT_SERVER_HOSTNAME}" + + # Timezone + read -p "Timezone [$DEFAULT_TZ]: " TZ + TZ="${TZ:-$DEFAULT_TZ}" + + # Admin credentials (only if deploying core) + if [ "$DEPLOY_CORE" = true ]; then + echo "" + log_info "Authelia Admin Credentials:" + + read -p "Admin username [$DEFAULT_ADMIN_USER]: " ADMIN_USER + ADMIN_USER="${ADMIN_USER:-$DEFAULT_ADMIN_USER}" + + read -p "Admin email [$DEFAULT_ADMIN_EMAIL]: " ADMIN_EMAIL + ADMIN_EMAIL="${ADMIN_EMAIL:-$DEFAULT_ADMIN_EMAIL}" + + if [ -z "$ADMIN_PASSWORD" ]; then + while [ -z "$ADMIN_PASSWORD" ]; do + read -s -p "Admin password (will be hashed): " ADMIN_PASSWORD + echo "" + if [ ${#ADMIN_PASSWORD} -lt 8 ]; then + log_warning "Password must be at least 8 characters" + ADMIN_PASSWORD="" + fi + done + else + log_info "Admin password already configured" + fi fi - read -p "Admin username [$ADMIN_USER] (press Enter to keep current): " input - [ -n "$input" ] && ADMIN_USER="$input" + else + # Use defaults + DOMAIN="$DEFAULT_DOMAIN" + SERVER_IP="$DEFAULT_SERVER_IP" + SERVER_HOSTNAME="$DEFAULT_SERVER_HOSTNAME" + TZ="$DEFAULT_TZ" - if [ -z "$ADMIN_EMAIL" ]; then - ADMIN_EMAIL="${ADMIN_USER}@${DOMAIN}" + if [ "$DEPLOY_CORE" = true ]; then + ADMIN_USER="$DEFAULT_ADMIN_USER" + ADMIN_EMAIL="$DEFAULT_ADMIN_EMAIL" fi - read -p "Admin email [$ADMIN_EMAIL] (press Enter to keep current): " input - [ -n "$input" ] && ADMIN_EMAIL="$input" + fi - if [ -z "$ADMIN_PASSWORD" ]; then - while [ -z "$ADMIN_PASSWORD" ]; do - read -s -p "Admin password (will be hashed): " ADMIN_PASSWORD - echo "" - if [ ${#ADMIN_PASSWORD} -lt 8 ]; then - log_warning "Password must be at least 8 characters" - ADMIN_PASSWORD="" - fi - done + echo "" +} + +# Certificate sharing function for infrastructure-only deployments +share_certs_with_core() { + log_info "Infrastructure-only deployment detected. Setting up certificate sharing for remote Docker control..." + + # Prompt for core server IP + read -p "Enter the IP address of your core server: " CORE_SERVER_IP + while [ -z "$CORE_SERVER_IP" ]; do + log_warning "Core server IP is required for certificate sharing" + read -p "Enter the IP address of your core server: " CORE_SERVER_IP + done + + # Prompt for SSH username + DEFAULT_SSH_USER="${DEFAULT_USER:-$USER}" + read -p "SSH username for core server [$DEFAULT_SSH_USER]: " SSH_USER + SSH_USER="${SSH_USER:-$DEFAULT_SSH_USER}" + + # Test SSH connection - try key authentication first + log_info "Testing SSH connection to core server ($SSH_USER@$CORE_SERVER_IP)..." + if ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o BatchMode=yes "$SSH_USER@$CORE_SERVER_IP" "echo 'SSH connection successful'" 2>/dev/null; then + log_success "SSH connection established using key authentication" + USE_SSHPASS=false + else + # Key authentication failed, try password authentication + log_info "Key authentication failed, trying password authentication..." + read -s -p "Enter SSH password for $SSH_USER@$CORE_SERVER_IP: " SSH_PASSWORD + echo "" + + if sshpass -p "$SSH_PASSWORD" ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "echo 'SSH connection successful'" 2>/dev/null; then + log_success "SSH connection established using password authentication" + USE_SSHPASS=true else - log_info "Admin password already configured" + log_error "Cannot connect to core server via SSH. Please check:" + echo " 1. SSH is running on the core server" + echo " 2. SSH keys are properly configured, or username/password are correct" + echo " 3. The core server IP is correct" + echo "" + read -p "Do you want to continue anyway? (y/N): " -n 1 -r + echo "" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + log_error "Certificate sharing cancelled. Please verify SSH access and try again." + exit 1 + fi + USE_SSHPASS=true # Assume password auth for copying fi fi + # Copy shared CA certificates from core server + log_info "Copying shared CA certificates from core server..." + mkdir -p "/opt/stacks/core/shared-ca" + + if [ "$USE_SSHPASS" = true ] && [ -n "$SSH_PASSWORD" ]; then + # Use password authentication + log_info "Running: sshpass -p [PASSWORD] scp -o StrictHostKeyChecking=no $SSH_USER@$CORE_SERVER_IP:/opt/stacks/core/shared-ca/ca.pem /opt/stacks/core/shared-ca/" + if sshpass -p "$SSH_PASSWORD" scp -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP:/opt/stacks/core/shared-ca/ca.pem" "$SSH_USER@$CORE_SERVER_IP:/opt/stacks/core/shared-ca/ca-key.pem" "/opt/stacks/core/shared-ca/" 2>&1; then + log_success "Shared CA certificates copied from core server" + else + log_warning "Could not copy shared CA certificates from core server." + log_info "Please ensure the certificates exist on the core server at: /opt/stacks/core/shared-ca/" + log_info "You may need to manually copy the certificates." + log_info "Required files: ca.pem, ca-key.pem" + echo "" + return 1 + fi + else + # Use key authentication + log_info "Running: scp -o StrictHostKeyChecking=no $SSH_USER@$CORE_SERVER_IP:/opt/stacks/core/shared-ca/ca.pem /opt/stacks/core/shared-ca/" + if scp -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP:/opt/stacks/core/shared-ca/ca.pem" "$SSH_USER@$CORE_SERVER_IP:/opt/stacks/core/shared-ca/ca-key.pem" "/opt/stacks/core/shared-ca/" 2>&1; then + log_success "Shared CA certificates copied from core server" + else + log_warning "Could not copy shared CA certificates from core server." + log_info "Please ensure the certificates exist on the core server at: /opt/stacks/core/shared-ca/" + log_info "You may need to manually copy the certificates." + log_info "Required files: ca.pem, ca-key.pem" + echo "" + return 1 + fi + fi + + # Update Docker daemon configuration to use shared CA + log_info "Updating Docker daemon to use shared CA for TLS..." + if [ -f "/opt/stacks/core/shared-ca/ca.pem" ]; then + # Update daemon.json to use the shared CA for both server and client verification + cat > /tmp/daemon.json < /dev/null && docker --version &> /dev/null; then log_success "Docker is already installed ($(docker --version))" + # Check if user is in docker group + if ! groups "$ACTUAL_USER" | grep -q docker; then + log_info "Adding $ACTUAL_USER to docker group..." + usermod -aG docker "$ACTUAL_USER" + NEEDS_LOGOUT=true + fi # Check if Docker service is running if ! systemctl is-active --quiet docker; then log_warning "Docker service is not running, starting it..." @@ -253,6 +385,7 @@ system_setup() { else curl -fsSL https://get.docker.com | sh usermod -aG docker "$ACTUAL_USER" + NEEDS_LOGOUT=true fi # Step 4: Install Docker Compose @@ -265,50 +398,47 @@ system_setup() { log_success "Docker Compose installed ($(docker-compose --version))" fi - # Step 5: Configure UFW firewall - log_info "Step 5/10: Configuring firewall..." + # Step 5: Generate shared CA for multi-server TLS + log_info "Step 5/10: Generating shared CA certificate for multi-server TLS..." + mkdir -p /opt/stacks/core/shared-ca + openssl genrsa -out /opt/stacks/core/shared-ca/ca-key.pem 4096 + openssl req -new -x509 -days 365 -key /opt/stacks/core/shared-ca/ca-key.pem -sha256 -out /opt/stacks/core/shared-ca/ca.pem -subj "/C=US/ST=State/L=City/O=Homelab/CN=Homelab-CA" + chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/stacks/core/shared-ca + + # Step 6: Configure Docker TLS + log_info "Step 6/10: Configuring Docker TLS..." + setup_docker_tls + + # Step 7: Configure UFW firewall + log_info "Step 7/10: Configuring firewall..." ufw --force enable ufw allow ssh ufw allow 80 ufw allow 443 + ufw allow 2376/tcp # Docker TLS port + log_success "Firewall configured" - # Step 6: Configure automatic updates - log_info "Step 6/10: Configuring automatic updates..." + # Step 8: Configure automatic updates + log_info "Step 8/10: Configuring automatic updates..." dpkg-reconfigure -f noninteractive unattended-upgrades - # Step 7: Create required directories - log_info "Step 7/10: Creating required directories..." - mkdir -p /opt/stacks/core - mkdir -p /opt/stacks/infrastructure - mkdir -p /opt/stacks/dashboards - mkdir -p /opt/dockge - - # Step 8: Set proper ownership - log_info "Step 8/10: Setting directory ownership..." + # Step 9: Set proper ownership + log_info "Step 9/10: Setting directory ownership..." chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/stacks chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/dockge - # Step 9: Create Docker networks - log_info "Step 9/10: Creating Docker networks..." + # Step 10: Create Docker networks + log_info "Step 10/10: Creating Docker networks..." docker network create homelab-network 2>/dev/null && log_success "Created homelab-network" || log_info "homelab-network already exists" docker network create traefik-network 2>/dev/null && log_success "Created traefik-network" || log_info "traefik-network already exists" docker network create media-network 2>/dev/null && log_success "Created media-network" || log_info "media-network already exists" - # Step 10: Generate SSH keys for Git (optional) - log_info "Step 10/10: SSH key setup (optional)..." - if [ ! -f "/home/$ACTUAL_USER/.ssh/id_rsa" ]; then - log_info "Generating SSH key for $ACTUAL_USER..." - sudo -u "$ACTUAL_USER" ssh-keygen -t rsa -b 4096 -f "/home/$ACTUAL_USER/.ssh/id_rsa" -N "" - log_info "SSH public key:" - cat "/home/$ACTUAL_USER/.ssh/id_rsa.pub" - echo "" - log_info "Add this key to your Git provider (GitHub, GitLab, etc.)" - fi - log_success "System setup completed!" echo "" - log_info "Please log out and back in for Docker group changes to take effect." - echo "" + if [ "$NEEDS_LOGOUT" = true ]; then + log_info "Please log out and back in for Docker group changes to take effect." + echo "" + fi } # Deployment function @@ -398,6 +528,13 @@ perform_deployment() { sed -i "s/\${DEFAULT_EMAIL}/${AUTHELIA_ADMIN_EMAIL}/g" /opt/stacks/core/authelia/users_database.yml sed -i "s|\$argon2id\$v=19\$m=65536,t=3,p=4\$CHANGEME|${AUTHELIA_ADMIN_PASSWORD}|g" /opt/stacks/core/authelia/users_database.yml + # Generate shared CA for multi-server TLS + log_info "Generating shared CA certificate for multi-server TLS..." + mkdir -p /opt/stacks/core/shared-ca + openssl genrsa -out /opt/stacks/core/shared-ca/ca-key.pem 4096 + openssl req -new -x509 -days 365 -key /opt/stacks/core/shared-ca/ca-key.pem -sha256 -out /opt/stacks/core/shared-ca/ca.pem -subj "/C=US/ST=State/L=City/O=Homelab/CN=Homelab-CA" + chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/stacks/core/shared-ca + # Deploy core stack cd /opt/stacks/core docker compose up -d @@ -479,7 +616,57 @@ perform_deployment() { fi } -# Setup stacks for Dockge function +# Setup Docker TLS function +setup_docker_tls() { + local TLS_DIR="/home/$ACTUAL_USER/EZ-Homelab/docker-tls" + + # Create TLS directory + mkdir -p "$TLS_DIR" + chown "$ACTUAL_USER:$ACTUAL_USER" "$TLS_DIR" + + # Use shared CA if available, otherwise generate local CA + if [ -f "/opt/stacks/core/shared-ca/ca.pem" ] && [ -f "/opt/stacks/core/shared-ca/ca-key.pem" ]; then + log_info "Using shared CA certificate for Docker TLS..." + cp "/opt/stacks/core/shared-ca/ca.pem" "$TLS_DIR/ca.pem" + cp "/opt/stacks/core/shared-ca/ca-key.pem" "$TLS_DIR/ca-key.pem" + else + log_info "Generating local CA certificate for Docker TLS..." + # Generate CA + openssl genrsa -out "$TLS_DIR/ca-key.pem" 4096 + openssl req -new -x509 -days 365 -key "$TLS_DIR/ca-key.pem" -sha256 -out "$TLS_DIR/ca.pem" -subj "/C=US/ST=State/L=City/O=Organization/CN=Docker-CA" + fi + + # Generate server key and cert + openssl genrsa -out "$TLS_DIR/server-key.pem" 4096 + openssl req -subj "/CN=$SERVER_IP" -new -key "$TLS_DIR/server-key.pem" -out "$TLS_DIR/server.csr" + echo "subjectAltName = DNS:$SERVER_IP,IP:$SERVER_IP,IP:127.0.0.1" > "$TLS_DIR/extfile.cnf" + openssl x509 -req -days 365 -in "$TLS_DIR/server.csr" -CA "$TLS_DIR/ca.pem" -CAkey "$TLS_DIR/ca-key.pem" -CAcreateserial -out "$TLS_DIR/server-cert.pem" -extfile "$TLS_DIR/extfile.cnf" + + # Generate client key and cert + openssl genrsa -out "$TLS_DIR/client-key.pem" 4096 + openssl req -subj "/CN=client" -new -key "$TLS_DIR/client-key.pem" -out "$TLS_DIR/client.csr" + openssl x509 -req -days 365 -in "$TLS_DIR/client.csr" -CA "$TLS_DIR/ca.pem" -CAkey "$TLS_DIR/ca-key.pem" -CAcreateserial -out "$TLS_DIR/client-cert.pem" + + # Configure Docker daemon + cat > /etc/docker/daemon.json <