From 89ca29918bb6eb3ad77aa7362418fa9eef96bab9 Mon Sep 17 00:00:00 2001 From: Kelin Date: Sun, 25 Jan 2026 23:08:01 -0500 Subject: [PATCH 1/9] Implement shared CA certificate system for multi-server TLS security - Generate shared CA during core deployment for consistent trust across servers - Modify setup_docker_tls() to use shared CA instead of per-server CAs - Update share_certs_with_core() to copy shared CA from core server - Re-enable TLS verification (DOCKER_TLS_VERIFY=1) in Sablier - Fix Sablier certificate mounting for proper TLS connection - Add docker-tls/ to .gitignore to prevent certificate leaks - Update documentation for shared CA approach --- .gitignore | 3 + docker-compose/core/docker-compose.yml | 12 +- .../media-management/docker-compose.yml | 8 +- docs/Ondemand-Remote-Services.md | 299 +++++++++++++ scripts/ez-homelab.sh | 423 +++++++++++++----- tasks.txt | 30 +- 6 files changed, 654 insertions(+), 121 deletions(-) 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 < Date: Mon, 26 Jan 2026 00:05:26 -0500 Subject: [PATCH 2/9] Fix share_certs_with_core() to handle missing shared CA gracefully - Check if shared CA exists on core server before attempting to copy - Generate local shared CA if core server doesn't have certificates - Provide clear instructions for manual certificate synchronization - Remove script failure when certificates can't be copied - Allow infrastructure deployment to continue with local CA generation --- scripts/ez-homelab.sh | 69 +++++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/scripts/ez-homelab.sh b/scripts/ez-homelab.sh index e681a19..9e8e516 100755 --- a/scripts/ez-homelab.sh +++ b/scripts/ez-homelab.sh @@ -289,34 +289,59 @@ share_certs_with_core() { log_info "Copying shared CA certificates from core server..." mkdir -p "/opt/stacks/core/shared-ca" + # First check if shared CA exists on core server + SHARED_CA_EXISTS=false 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 + if sshpass -p "$SSH_PASSWORD" ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/shared-ca/ca.pem ] && [ -f /opt/stacks/core/shared-ca/ca-key.pem ]" 2>/dev/null; then + SHARED_CA_EXISTS=true 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 + if ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/shared-ca/ca.pem ] && [ -f /opt/stacks/core/shared-ca/ca-key.pem ]" 2>/dev/null; then + SHARED_CA_EXISTS=true fi fi + if [ "$SHARED_CA_EXISTS" = true ]; then + # Copy existing shared CA from core server + if [ "$USE_SSHPASS" = true ] && [ -n "$SSH_PASSWORD" ]; then + 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 "Failed to copy shared CA certificates from core server" + SHARED_CA_EXISTS=false + fi + else + 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 "Failed to copy shared CA certificates from core server" + SHARED_CA_EXISTS=false + fi + fi + fi + + if [ "$SHARED_CA_EXISTS" = false ]; then + # Generate local shared CA if not available from core server + log_warning "Shared CA certificates not found on core server." + log_info "Generating local shared CA for infrastructure server..." + + 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" + + log_success "Local shared CA generated" + log_info "IMPORTANT: Copy these certificates to your core server at /opt/stacks/core/shared-ca/" + log_info "Run this command on your CORE server:" + echo " sudo mkdir -p /opt/stacks/core/shared-ca" + echo " sudo scp $SSH_USER@$SERVER_IP:/opt/stacks/core/shared-ca/ca.pem /opt/stacks/core/shared-ca/" + echo " sudo scp $SSH_USER@$SERVER_IP:/opt/stacks/core/shared-ca/ca-key.pem /opt/stacks/core/shared-ca/" + echo " sudo chown -R $SSH_USER:$SSH_USER /opt/stacks/core/shared-ca" + echo "" + log_info "After copying, restart the core server's Docker services for the changes to take effect." + echo "" + 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 From 3076232e8fc6c5d8dcc3c62c41c9d6ebf5924635 Mon Sep 17 00:00:00 2001 From: Kelin Date: Mon, 26 Jan 2026 00:08:41 -0500 Subject: [PATCH 3/9] Fix permission errors in perform_deployment() function - Use sudo for creating /opt directories during deployment - Use sudo for copying files to /opt/dockge and /opt/stacks - Ensure proper ownership of deployment directories - Fix mkdir command for dashboards directory --- scripts/ez-homelab.sh | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/ez-homelab.sh b/scripts/ez-homelab.sh index 9e8e516..09833c0 100755 --- a/scripts/ez-homelab.sh +++ b/scripts/ez-homelab.sh @@ -482,10 +482,12 @@ perform_deployment() { # Step 1: Create required directories log_info "Step 1: Creating required directories..." - mkdir -p /opt/stacks/core - mkdir -p /opt/stacks/infrastructure - mkdir -p /opt/stacks/dashboards - mkdir -p /opt/dockge + sudo mkdir -p /opt/stacks/core + sudo mkdir -p /opt/stacks/infrastructure + sudo mkdir -p /opt/stacks/dashboards + sudo mkdir -p /opt/dockge + sudo chown -R "$USER:$USER" /opt/stacks + sudo chown -R "$USER:$USER" /opt/dockge log_success "Directories created" # Step 2: Create Docker networks (if they don't exist) @@ -501,8 +503,8 @@ perform_deployment() { echo "" # Copy Dockge stack files - cp "$REPO_DIR/docker-compose/dockge/docker-compose.yml" /opt/dockge/docker-compose.yml - cp "$REPO_DIR/.env" /opt/dockge/.env + sudo cp "$REPO_DIR/docker-compose/dockge/docker-compose.yml" /opt/dockge/docker-compose.yml + sudo cp "$REPO_DIR/.env" /opt/dockge/.env # Deploy Dockge stack cd /opt/dockge @@ -519,8 +521,8 @@ perform_deployment() { echo "" # Copy core stack files - cp "$REPO_DIR/docker-compose/core/docker-compose.yml" /opt/stacks/core/docker-compose.yml - cp "$REPO_DIR/.env" /opt/stacks/core/.env + sudo cp "$REPO_DIR/docker-compose/core/docker-compose.yml" /opt/stacks/core/docker-compose.yml + sudo cp "$REPO_DIR/.env" /opt/stacks/core/.env # Copy configs if [ -d "/opt/stacks/core/traefik" ]; then @@ -617,7 +619,7 @@ perform_deployment() { echo "" # Create dashboards directory - mkdir -p /opt/stacks/dashboards + sudo mkdir -p /opt/stacks/dashboards # Copy dashboards compose file cp "$REPO_DIR/docker-compose/dashboards/docker-compose.yml" /opt/stacks/dashboards/docker-compose.yml From e2d28b5208dc088091164c065b1f893ab767a6e5 Mon Sep 17 00:00:00 2001 From: Kelin Date: Mon, 26 Jan 2026 16:34:25 -0500 Subject: [PATCH 4/9] feat: Improve TLS handling for multi-server deployments - Add CORE_SERVER_IP variable for remote server configuration - Implement setup_multi_server_tls() function for shared CA management - Change TLS failure handling from exit-on-error to warning-based approach - Add TLS_ISSUES_SUMMARY for end-of-deployment remediation guidance - Update documentation for automated TLS setup process - Add comprehensive AI assistant instructions for project management This allows deployments to complete successfully even with TLS issues, providing clear remediation steps instead of failing the entire setup. --- .github/copilot-instructions.md | 26 ++ docs/Ondemand-Remote-Services.md | 62 +++- scripts/ez-homelab.sh | 541 ++++++++++++++++++++----------- 3 files changed, 445 insertions(+), 184 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4b429de..bbcef6a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -47,6 +47,12 @@ docker-compose/ - **Gluetun network mode**: Download clients use `network_mode: "service:gluetun"` for VPN routing - **Port mapping**: Only core services expose ports (80/443 for Traefik); others route via Traefik labels +### TLS and Multi-Server Architecture +- **Shared CA**: Core server generates CA for signing certificates across all servers +- **Docker TLS**: Remote servers use TCP 2376 with mutual TLS for Sablier access +- **Certificate Management**: Automated generation and distribution of client/server certificates +- **Sablier Integration**: Core Sablier connects to remote Docker daemons via TLS for lazy loading + ## Critical Operational Principles ### 1. Security-First SSO Strategy @@ -284,6 +290,15 @@ Secrets auto-generated by `ez-homelab.sh`: - **Renewal**: Traefik handles automatically (90-day Let's Encrypt certs) - **Usage**: Services use `tls.certresolver=letsencrypt` label (no per-service cert requests) +### Remote Server TLS Setup +For multi-server deployments with Sablier lazy loading: +- **Core server**: Generates shared CA and client certificates +- **Remote servers**: Use `ez-homelab.sh` option 3, specify core server IP for CA import +- **Certificate chain**: Core has CA + client certs; remotes have CA + server certs +- **Sablier connection**: Uses TCP 2376 with mutual TLS to remote Docker daemons +- **Security**: All Docker API access encrypted and authenticated +- **Failure handling**: Setup fails if CA cannot be copied (prevents inconsistent TLS state) + ### Homepage Dashboard AI Configuration Homepage (`/opt/stacks/dashboards/`) uses dynamic variable replacement: - Services configured in `homepage/config/services.yaml` @@ -357,6 +372,17 @@ docker logs gluetun | grep -i wireguard # Verify connection ``` Verify: `SURFSHARK_PRIVATE_KEY` set in `.env`, service using `network_mode: "service:gluetun"`, ports mapped in Gluetun +### Sablier TLS Connection Issues +```bash +# Test Docker TLS connection from core to remote +cd /opt/stacks/core/shared-ca +docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem --host=tcp://REMOTE_IP:2376 ps + +# Check Sablier logs +docker compose -f /opt/stacks/core/docker-compose.yml logs sablier-service +``` +Verify: Remote Docker daemon configured with shared CA, certificates properly signed, firewall allows 2376/tcp + ### Wildcard Certificate Issues ```bash docker logs traefik | grep -i certificate diff --git a/docs/Ondemand-Remote-Services.md b/docs/Ondemand-Remote-Services.md index 4c87c36..78fe268 100644 --- a/docs/Ondemand-Remote-Services.md +++ b/docs/Ondemand-Remote-Services.md @@ -1,6 +1,66 @@ # On Demand Remote Services with Authelia, Sablier & Traefik -## 4 Step Process +## Overview + +This guide explains how to set up lazy-loading services on remote servers (like Raspberry Pi) that start automatically when accessed via Traefik. The core server runs Sablier, which connects to remote Docker daemons via TLS to manage container lifecycle. + +## Prerequisites + +- Core server with Traefik, Authelia, and Sablier deployed +- Remote server with Docker installed +- Shared TLS CA configured between core and remote servers + +## Automated Setup + +For new remote servers, use the automated script: + +1. On the remote server, run `ez-homelab.sh` and select option 3 (Infrastructure Only) +2. When prompted, enter the core server IP for shared TLS CA +3. The script will automatically: + - Copy shared CA from core server via SSH + - Configure Docker TLS with shared certificates + - Generate server certificates signed by shared CA + - Set up Docker daemon for TLS on port 2376 + +**Important**: The script will fail if it cannot copy the shared CA from the core server. Ensure SSH access is configured between servers before running option 3. + +## Manual Setup (if automated fails) + +If the automated setup fails, manually configure TLS: + +### On Core Server: +```bash +# Generate server certificates for remote server +cd /opt/stacks/core/shared-ca +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 +``` + +### On Remote Server: +```bash +# Copy certificates +scp user@core-server:/opt/stacks/core/shared-ca/ca.pem /opt/stacks/core/shared-ca/ +scp user@core-server:/opt/stacks/core/shared-ca/server-cert.pem /opt/stacks/core/shared-ca/ +scp user@core-server:/opt/stacks/core/shared-ca/server-key.pem /opt/stacks/core/shared-ca/ + +# Update Docker daemon +sudo tee /etc/docker/daemon.json > /dev/null </dev/null; then + log_success "Shared CA copied from core server" + else + log_warning "Failed to copy shared CA from core server $CORE_SERVER_IP" + log_warning "This will create TLS certificate mismatches between core and remote servers" + log_warning "Sablier will not be able to connect to this remote Docker daemon" + TLS_ISSUES_SUMMARY="⚠️ TLS Configuration Issue: Could not copy shared CA from core server $CORE_SERVER_IP + This will prevent Sablier from connecting to remote Docker daemons. + + To fix this: + 1. Ensure SSH access works: ssh $ACTUAL_USER@$CORE_SERVER_IP + 2. Verify core server has: /opt/stacks/core/shared-ca/ca.pem + 3. Manually copy CA: scp $ACTUAL_USER@$CORE_SERVER_IP:/opt/stacks/core/shared-ca/ca.pem $ca_dir/ + 4. Regenerate server certificates on this server with shared CA + 5. Restart Docker: sudo systemctl restart docker + 6. Test connection: docker --tlsverify --tlscacert=$ca_dir/ca.pem --tlscert=$ca_dir/cert.pem --tlskey=$ca_dir/key.pem --host=tcp://localhost:2376 ps + + Then restart Sablier on the core server to reconnect." + generate_shared_ca + fi + + # Setup Docker TLS with shared CA + setup_docker_tls + else + log_info "No core server specified, setting up local TLS..." + generate_shared_ca + setup_docker_tls + fi +} + # Get script directory and repo directory SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" REPO_DIR="$( cd "$SCRIPT_DIR/.." && pwd )" @@ -43,6 +124,7 @@ fi # Default values DOMAIN="" SERVER_IP="" +CORE_SERVER_IP="" ADMIN_USER="" ADMIN_EMAIL="" ADMIN_PASSWORD="" @@ -50,6 +132,7 @@ DEPLOY_CORE=false DEPLOY_INFRASTRUCTURE=false DEPLOY_DASHBOARDS=false SETUP_STACKS=false +TLS_ISSUES_SUMMARY="" # Load existing .env file if it exists load_env_file() { @@ -158,6 +241,7 @@ prompt_for_values() { # 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_CORE_SERVER_IP="${CORE_SERVER_IP:-}" DEFAULT_SERVER_HOSTNAME="${SERVER_HOSTNAME:-$(hostname)}" DEFAULT_TZ="${TZ:-America/New_York}" @@ -168,6 +252,12 @@ prompt_for_values() { echo " Server Hostname: $DEFAULT_SERVER_HOSTNAME" echo " Timezone: $DEFAULT_TZ" + if [ "$DEPLOY_CORE" = false ] && [ -z "$DEFAULT_CORE_SERVER_IP" ]; then + echo " Core Server IP: [Will be prompted for multi-server TLS]" + elif [ -n "$DEFAULT_CORE_SERVER_IP" ]; then + echo " Core Server IP: $DEFAULT_CORE_SERVER_IP" + fi + if [ "$DEPLOY_CORE" = true ]; then DEFAULT_ADMIN_USER="${DEFAULT_USER:-admin}" DEFAULT_ADMIN_EMAIL="${DEFAULT_EMAIL:-${DEFAULT_ADMIN_USER}@${DEFAULT_DOMAIN}}" @@ -199,6 +289,12 @@ prompt_for_values() { read -p "Timezone [$DEFAULT_TZ]: " TZ TZ="${TZ:-$DEFAULT_TZ}" + # Core server IP (for multi-server setup) + if [ "$DEPLOY_CORE" = false ]; then + echo "" + read -p "Core server IP (for shared TLS CA): " CORE_SERVER_IP + fi + # Admin credentials (only if deploying core) if [ "$DEPLOY_CORE" = true ]; then echo "" @@ -229,6 +325,7 @@ prompt_for_values() { SERVER_IP="$DEFAULT_SERVER_IP" SERVER_HOSTNAME="$DEFAULT_SERVER_HOSTNAME" TZ="$DEFAULT_TZ" + CORE_SERVER_IP="$DEFAULT_CORE_SERVER_IP" if [ "$DEPLOY_CORE" = true ]; then ADMIN_USER="$DEFAULT_ADMIN_USER" @@ -239,14 +336,14 @@ prompt_for_values() { 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..." +# Certificate fetching function for infrastructure-only deployments +get_certs_from_core_server() { + log_info "Infrastructure-only deployment detected. Fetching certificate from core server 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" + log_warning "Core server IP is required for certificate fetching" read -p "Enter the IP address of your core server: " CORE_SERVER_IP done @@ -278,75 +375,89 @@ share_certs_with_core() { 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." + log_error "Certificate fetching 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" + # Fetch shared CA certificates from core server + log_info "Fetching shared CA certificates from core server..." + sudo mkdir -p "/opt/stacks/core/shared-ca" - # First check if shared CA exists on core server + # First check if shared CA exists on core server (check both old and new locations) SHARED_CA_EXISTS=false if [ "$USE_SSHPASS" = true ] && [ -n "$SSH_PASSWORD" ]; then - if sshpass -p "$SSH_PASSWORD" ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/shared-ca/ca.pem ] && [ -f /opt/stacks/core/shared-ca/ca-key.pem ]" 2>/dev/null; then + if sshpass -p "$SSH_PASSWORD" ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/shared-ca/ca.pem ] && [ -f /opt/stacks/core/shared-ca/ca-key.pem ] && [ -r /opt/stacks/core/shared-ca/ca.pem ] && [ -r /opt/stacks/core/shared-ca/ca-key.pem ]" 2>/dev/null; then SHARED_CA_EXISTS=true + SHARED_CA_PATH="/opt/stacks/core/shared-ca" + log_info "Detected CA certificate and key in shared-ca location" + elif sshpass -p "$SSH_PASSWORD" ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/docker-tls/ca.pem ] && [ -f /opt/stacks/core/docker-tls/ca-key.pem ] && [ -r /opt/stacks/core/docker-tls/ca.pem ] && [ -r /opt/stacks/core/docker-tls/ca-key.pem ]" 2>/dev/null; then + SHARED_CA_EXISTS=true + SHARED_CA_PATH="/opt/stacks/core/docker-tls" + log_info "Detected CA certificate and key in docker-tls location" fi else - if ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/shared-ca/ca.pem ] && [ -f /opt/stacks/core/shared-ca/ca-key.pem ]" 2>/dev/null; then + if ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/shared-ca/ca.pem ] && [ -f /opt/stacks/core/shared-ca/ca-key.pem ] && [ -r /opt/stacks/core/shared-ca/ca.pem ] && [ -r /opt/stacks/core/shared-ca/ca-key.pem ]" 2>/dev/null; then SHARED_CA_EXISTS=true + SHARED_CA_PATH="/opt/stacks/core/shared-ca" + log_info "Detected CA certificate and key in shared-ca location" + elif ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/docker-tls/ca.pem ] && [ -f /opt/stacks/core/docker-tls/ca-key.pem ] && [ -r /opt/stacks/core/docker-tls/ca.pem ] && [ -r /opt/stacks/core/docker-tls/ca-key.pem ]" 2>/dev/null; then + SHARED_CA_EXISTS=true + SHARED_CA_PATH="/opt/stacks/core/docker-tls" + log_info "Detected CA certificate and key in docker-tls location" fi fi if [ "$SHARED_CA_EXISTS" = true ]; then # Copy existing shared CA from core server if [ "$USE_SSHPASS" = true ] && [ -n "$SSH_PASSWORD" ]; then - 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" + log_info "Running: sshpass -p [PASSWORD] scp -o StrictHostKeyChecking=no $SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca.pem $SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca-key.pem /opt/stacks/core/shared-ca/" + if sshpass -p "$SSH_PASSWORD" scp -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca.pem" "$SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca-key.pem" "/opt/stacks/core/shared-ca/" 2>&1; then + log_success "Shared CA certificate and key fetched from core server" + # Generate server certificates signed by the shared CA + setup_docker_tls else - log_warning "Failed to copy shared CA certificates from core server" + log_warning "Failed to fetch shared CA certificate and key from core server" SHARED_CA_EXISTS=false fi else - 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" + log_info "Running: scp -o StrictHostKeyChecking=no $SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca.pem $SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca-key.pem /opt/stacks/core/shared-ca/" + if scp -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca.pem" "$SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca-key.pem" "/opt/stacks/core/shared-ca/" 2>&1; then + log_success "Shared CA certificate and key fetched from core server" + # Generate server certificates signed by the shared CA + setup_docker_tls else - log_warning "Failed to copy shared CA certificates from core server" + log_warning "Failed to fetch shared CA certificate and key from core server" SHARED_CA_EXISTS=false fi fi fi if [ "$SHARED_CA_EXISTS" = false ]; then - # Generate local shared CA if not available from core server log_warning "Shared CA certificates not found on core server." - log_info "Generating local shared CA for infrastructure server..." - - 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" - - log_success "Local shared CA generated" - log_info "IMPORTANT: Copy these certificates to your core server at /opt/stacks/core/shared-ca/" - log_info "Run this command on your CORE server:" + log_info "Please ensure the core server has been set up first and has generated the shared CA certificates." + log_info "You can manually copy the certificates later (check both locations on core server):" echo " sudo mkdir -p /opt/stacks/core/shared-ca" - echo " sudo scp $SSH_USER@$SERVER_IP:/opt/stacks/core/shared-ca/ca.pem /opt/stacks/core/shared-ca/" - echo " sudo scp $SSH_USER@$SERVER_IP:/opt/stacks/core/shared-ca/ca-key.pem /opt/stacks/core/shared-ca/" + echo " # Try shared-ca location:" + echo " sudo scp $SSH_USER@$CORE_SERVER_IP:/opt/stacks/core/shared-ca/ca.pem /opt/stacks/core/shared-ca/" + echo " sudo scp $SSH_USER@$CORE_SERVER_IP:/opt/stacks/core/shared-ca/ca-key.pem /opt/stacks/core/shared-ca/" + echo " # Or docker-tls location:" + echo " sudo scp $SSH_USER@$CORE_SERVER_IP:/opt/stacks/core/docker-tls/ca.pem /opt/stacks/core/shared-ca/" + echo " sudo scp $SSH_USER@$CORE_SERVER_IP:/opt/stacks/core/docker-tls/ca-key.pem /opt/stacks/core/shared-ca/" echo " sudo chown -R $SSH_USER:$SSH_USER /opt/stacks/core/shared-ca" echo "" - log_info "After copying, restart the core server's Docker services for the changes to take effect." + log_info "Then restart the Docker daemon: sudo systemctl reload docker" echo "" 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 < /tmp/daemon.json </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" echo "" - # Step 3: Deploy Dockge (always deployed) - log_info "Step 3: Deploying Dockge stack manager..." - log_info " - Dockge (Docker Compose Manager)" - echo "" + # Step 4: Deploy Dockge (always deployed) + deploy_dockge - # Copy Dockge stack files - sudo cp "$REPO_DIR/docker-compose/dockge/docker-compose.yml" /opt/dockge/docker-compose.yml - sudo cp "$REPO_DIR/.env" /opt/dockge/.env - - # Deploy Dockge stack - cd /opt/dockge - docker compose up -d - log_success "Dockge deployed" - echo "" - - # Deploy core infrastructure + # Deploy core stack if [ "$DEPLOY_CORE" = true ]; then - log_info "Step 4: Deploying core infrastructure stack..." - log_info " - DuckDNS (Dynamic DNS)" - log_info " - Traefik (Reverse Proxy with SSL)" - log_info " - Authelia (Single Sign-On)" - echo "" - - # Copy core stack files - sudo cp "$REPO_DIR/docker-compose/core/docker-compose.yml" /opt/stacks/core/docker-compose.yml - sudo cp "$REPO_DIR/.env" /opt/stacks/core/.env - - # Copy configs - if [ -d "/opt/stacks/core/traefik" ]; then - mv /opt/stacks/core/traefik /opt/stacks/core/traefik.backup.$(date +%Y%m%d_%H%M%S) - fi - cp -r "$REPO_DIR/config-templates/traefik" /opt/stacks/core/ - - # Replace ACME email placeholder - sed -i "s/ACME_EMAIL_PLACEHOLDER/${AUTHELIA_ADMIN_EMAIL}/g" /opt/stacks/core/traefik/traefik.yml - - # Replace domain placeholders in traefik dynamic configs - find /opt/stacks/core/traefik/dynamic -name "*.yml" -exec sed -i "s/\${DOMAIN}/${DOMAIN}/g" {} \; - find /opt/stacks/core/traefik/dynamic -name "*.yml" -exec sed -i "s/\${SERVER_HOSTNAME}/${SERVER_HOSTNAME}/g" {} \; - - if [ -d "/opt/stacks/core/authelia" ]; then - mv /opt/stacks/core/authelia /opt/stacks/core/authelia.backup.$(date +%Y%m%d_%H%M%S) - fi - cp -r "$REPO_DIR/config-templates/authelia" /opt/stacks/core/ - - # Replace domain placeholders - sed -i "s/your-domain.duckdns.org/${DOMAIN}/g" /opt/stacks/core/authelia/configuration.yml - sed -i "s/\${DOMAIN}/${DOMAIN}/g" /opt/stacks/core/authelia/configuration.yml - - # Replace secret placeholders - sed -i "s|\${AUTHELIA_JWT_SECRET}|${AUTHELIA_JWT_SECRET}|g" /opt/stacks/core/authelia/configuration.yml - sed -i "s|\${AUTHELIA_SESSION_SECRET}|${AUTHELIA_SESSION_SECRET}|g" /opt/stacks/core/authelia/configuration.yml - sed -i "s|\${AUTHELIA_STORAGE_ENCRYPTION_KEY}|${AUTHELIA_STORAGE_ENCRYPTION_KEY}|g" /opt/stacks/core/authelia/configuration.yml - sed -i "s/admin/${AUTHELIA_ADMIN_USER}/g" /opt/stacks/core/authelia/users_database.yml - sed -i "s/admin@example.com/${AUTHELIA_ADMIN_EMAIL}/g" /opt/stacks/core/authelia/users_database.yml - 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 - log_success "Core infrastructure deployed" - echo "" + deploy_core fi # Deploy infrastructure stack if [ "$DEPLOY_INFRASTRUCTURE" = true ]; then - step_num=$([ "$DEPLOY_CORE" = true ] && echo "5" || echo "4") - log_info "Step $step_num: Deploying infrastructure stack..." - log_info " - Pi-hole (DNS Ad Blocker)" - log_info " - Watchtower (Container Updates)" - log_info " - Dozzle (Log Viewer)" - log_info " - Glances (System Monitor)" - log_info " - Docker Proxy (Security)" - echo "" - - # Copy infrastructure stack - cp "$REPO_DIR/docker-compose/infrastructure/docker-compose.yml" /opt/stacks/infrastructure/docker-compose.yml - cp "$REPO_DIR/.env" /opt/stacks/infrastructure/.env - - # Copy any additional config directories - for config_dir in "$REPO_DIR/docker-compose/infrastructure"/*/; do - if [ -d "$config_dir" ] && [ "$(basename "$config_dir")" != "." ]; then - cp -r "$config_dir" /opt/stacks/infrastructure/ - fi - done - - # If core is not deployed, remove Authelia middleware references - if [ "$DEPLOY_CORE" = false ]; then - log_info "Core infrastructure not deployed - removing Authelia middleware references..." - sed -i '/middlewares=authelia@docker/d' /opt/stacks/infrastructure/docker-compose.yml - fi - - # Deploy infrastructure stack - cd /opt/stacks/infrastructure - docker compose up -d - log_success "Infrastructure stack deployed" - echo "" + step_num=$([ "$DEPLOY_CORE" = true ] && echo "6" || echo "5") + deploy_infrastructure fi # Deploy dashboard stack if [ "$DEPLOY_DASHBOARDS" = true ]; then if [ "$DEPLOY_CORE" = true ] && [ "$DEPLOY_INFRASTRUCTURE" = true ]; then - step_num=6 + step_num=7 elif [ "$DEPLOY_CORE" = true ] || [ "$DEPLOY_INFRASTRUCTURE" = true ]; then - step_num=5 + step_num=6 else - step_num=4 + step_num=5 fi - log_info "Step $step_num: Deploying dashboard stack..." - log_info " - Homepage (Application Dashboard)" - log_info " - Homarr (Modern Dashboard)" - echo "" - - # Create dashboards directory - sudo mkdir -p /opt/stacks/dashboards - - # Copy dashboards compose file - cp "$REPO_DIR/docker-compose/dashboards/docker-compose.yml" /opt/stacks/dashboards/docker-compose.yml - cp "$REPO_DIR/.env" /opt/stacks/dashboards/.env - - # Copy homepage config - if [ -d "$REPO_DIR/docker-compose/dashboards/homepage" ]; then - cp -r "$REPO_DIR/docker-compose/dashboards/homepage" /opt/stacks/dashboards/ - fi - - # Deploy dashboards stack - cd /opt/stacks/dashboards - docker compose up -d - log_success "Dashboard stack deployed" - echo "" + deploy_dashboards fi # Setup stacks for Dockge if [ "$SETUP_STACKS" = true ]; then setup_stacks_for_dockge fi + + # Report any missing variables + if [ -n "$MISSING_VARS_SUMMARY" ]; then + log_warning "The following environment variables were missing and may cause issues:" + echo "$MISSING_VARS_SUMMARY" + log_info "Please update your .env file and redeploy affected stacks." + fi + + # Report any TLS issues + if [ -n "$TLS_ISSUES_SUMMARY" ]; then + echo "" + log_warning "TLS Configuration Issues Detected:" + echo "$TLS_ISSUES_SUMMARY" + echo "" + fi } # Setup Docker TLS function @@ -675,7 +846,7 @@ setup_docker_tls() { 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 < /dev/null < Date: Mon, 26 Jan 2026 18:16:35 -0500 Subject: [PATCH 5/9] Fix multi-server TLS setup to support password authentication and improve error handling --- docs/Ondemand-Remote-Services.md | 2 +- scripts/ez-homelab.sh | 288 ++++++++++++------------------- 2 files changed, 116 insertions(+), 174 deletions(-) diff --git a/docs/Ondemand-Remote-Services.md b/docs/Ondemand-Remote-Services.md index 78fe268..e42d333 100644 --- a/docs/Ondemand-Remote-Services.md +++ b/docs/Ondemand-Remote-Services.md @@ -190,7 +190,7 @@ This section provides a complete deployment plan for scenarios where the core in ## Prerequisites - Both servers must be on the same network and able to communicate -- SSH access configured between servers (passwordless recommended for automation) +- SSH access configured between servers (key-based or password authentication supported) - 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) diff --git a/scripts/ez-homelab.sh b/scripts/ez-homelab.sh index f016c72..122d661 100755 --- a/scripts/ez-homelab.sh +++ b/scripts/ez-homelab.sh @@ -71,42 +71,131 @@ generate_shared_ca() { # Function to setup multi-server TLS for remote servers setup_multi_server_tls() { + local ca_dir="/opt/stacks/core/shared-ca" + sudo mkdir -p "$ca_dir" + sudo chown "$ACTUAL_USER:$ACTUAL_USER" "$ca_dir" + if [ -n "$CORE_SERVER_IP" ]; then log_info "Setting up multi-server TLS using shared CA from core server $CORE_SERVER_IP..." - - # Create shared-ca directory - local ca_dir="/opt/stacks/core/shared-ca" - sudo mkdir -p "$ca_dir" - sudo chown "$ACTUAL_USER:$ACTUAL_USER" "$ca_dir" - - # Copy shared CA from core server - if scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$ACTUAL_USER@$CORE_SERVER_IP:/opt/stacks/core/shared-ca/ca.pem" "$ca_dir/" 2>/dev/null; then - log_success "Shared CA copied from core server" + else + # Prompt for core server IP if not set + 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 shared TLS" + read -p "Enter the IP address of your core server: " CORE_SERVER_IP + done + log_info "Setting up multi-server TLS using shared CA from core server $CORE_SERVER_IP..." + fi + + # Prompt for SSH username if not set + if [ -z "$SSH_USER" ]; then + 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}" + fi + + # 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_warning "Failed to copy shared CA from core server $CORE_SERVER_IP" - log_warning "This will create TLS certificate mismatches between core and remote servers" - log_warning "Sablier will not be able to connect to this remote Docker daemon" - TLS_ISSUES_SUMMARY="⚠️ TLS Configuration Issue: Could not copy shared CA from core server $CORE_SERVER_IP + 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 "" + TLS_ISSUES_SUMMARY="⚠️ TLS Configuration Issue: Cannot connect to core server $CORE_SERVER_IP via SSH This will prevent Sablier from connecting to remote Docker daemons. To fix this: - 1. Ensure SSH access works: ssh $ACTUAL_USER@$CORE_SERVER_IP - 2. Verify core server has: /opt/stacks/core/shared-ca/ca.pem - 3. Manually copy CA: scp $ACTUAL_USER@$CORE_SERVER_IP:/opt/stacks/core/shared-ca/ca.pem $ca_dir/ - 4. Regenerate server certificates on this server with shared CA - 5. Restart Docker: sudo systemctl restart docker - 6. Test connection: docker --tlsverify --tlscacert=$ca_dir/ca.pem --tlscert=$ca_dir/cert.pem --tlskey=$ca_dir/key.pem --host=tcp://localhost:2376 ps + 1. Ensure SSH is running on the core server + 2. Configure SSH keys or provide correct password + 3. Verify the core server IP is correct + 4. Test SSH connection: ssh $SSH_USER@$CORE_SERVER_IP + + Without SSH access, shared CA cannot be fetched for secure multi-server TLS." + return + fi + fi + + # Fetch shared CA certificates from core server + log_info "Fetching shared CA certificates from core server..." + SHARED_CA_EXISTS=false + + # Check if shared CA exists on core server (check both old and new locations) + if [ "$USE_SSHPASS" = true ] && [ -n "$SSH_PASSWORD" ]; then + if sshpass -p "$SSH_PASSWORD" ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/shared-ca/ca.pem ] && [ -f /opt/stacks/core/shared-ca/ca-key.pem ] && [ -r /opt/stacks/core/shared-ca/ca.pem ] && [ -r /opt/stacks/core/shared-ca/ca-key.pem ]" 2>/dev/null; then + SHARED_CA_EXISTS=true + SHARED_CA_PATH="/opt/stacks/core/shared-ca" + log_info "Detected CA certificate and key in shared-ca location" + elif sshpass -p "$SSH_PASSWORD" ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/docker-tls/ca.pem ] && [ -f /opt/stacks/core/docker-tls/ca-key.pem ] && [ -r /opt/stacks/core/docker-tls/ca.pem ] && [ -r /opt/stacks/core/docker-tls/ca-key.pem ]" 2>/dev/null; then + SHARED_CA_EXISTS=true + SHARED_CA_PATH="/opt/stacks/core/docker-tls" + log_info "Detected CA certificate and key in docker-tls location" + fi + else + if ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/shared-ca/ca.pem ] && [ -f /opt/stacks/core/shared-ca/ca-key.pem ] && [ -r /opt/stacks/core/shared-ca/ca.pem ] && [ -r /opt/stacks/core/shared-ca/ca-key.pem ]" 2>/dev/null; then + SHARED_CA_EXISTS=true + SHARED_CA_PATH="/opt/stacks/core/shared-ca" + log_info "Detected CA certificate and key in shared-ca location" + elif ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/docker-tls/ca.pem ] && [ -f /opt/stacks/core/docker-tls/ca-key.pem ] && [ -r /opt/stacks/core/docker-tls/ca.pem ] && [ -r /opt/stacks/core/docker-tls/ca-key.pem ]" 2>/dev/null; then + SHARED_CA_EXISTS=true + SHARED_CA_PATH="/opt/stacks/core/docker-tls" + log_info "Detected CA certificate and key in docker-tls location" + fi + fi + + if [ "$SHARED_CA_EXISTS" = true ]; then + # Copy existing shared CA from core server + set +e + scp_output=$(scp -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca.pem" "$SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca-key.pem" "$ca_dir/" 2>&1) + scp_exit_code=$? + set -e + if [ $scp_exit_code -eq 0 ]; then + log_success "Shared CA certificate and key fetched from core server" + setup_docker_tls + else + log_error "Failed to fetch shared CA certificate and key from core server" + TLS_ISSUES_SUMMARY="⚠️ TLS Configuration Issue: Could not copy shared CA from core server $CORE_SERVER_IP + SCP Error: $scp_output + + To fix this: + 1. Ensure SSH key authentication works: ssh $ACTUAL_USER@$CORE_SERVER_IP + 2. Verify core server has: $SHARED_CA_PATH/ca.pem and ca-key.pem + 3. Check file permissions on core server: ls -la $SHARED_CA_PATH/ + 4. Manually copy CA: scp $ACTUAL_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca.pem $ca_dir/ + scp $ACTUAL_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca-key.pem $ca_dir/ + 5. Regenerate server certificates: run setup_docker_tls after copying + 6. Restart Docker: sudo systemctl restart docker Then restart Sablier on the core server to reconnect." - generate_shared_ca + return fi - - # Setup Docker TLS with shared CA - setup_docker_tls else - log_info "No core server specified, setting up local TLS..." - generate_shared_ca - setup_docker_tls + log_warning "Shared CA certificates not found on core server." + log_info "Please ensure the core server has been set up first and has generated the shared CA certificates." + TLS_ISSUES_SUMMARY="⚠️ TLS Configuration Issue: Shared CA certificates not found on core server $CORE_SERVER_IP + This will prevent Sablier from connecting to remote Docker daemons. + + To fix this: + 1. Ensure the core server is set up and has generated shared CA certificates + 2. Verify SSH access: ssh $ACTUAL_USER@$CORE_SERVER_IP + 3. Check core server locations: /opt/stacks/core/shared-ca/ or /opt/stacks/core/docker-tls/ + 4. Manually copy CA certificates if needed + 5. Re-run the infrastructure deployment + + Without shared CA, remote Docker access will not work securely." + return fi } @@ -336,148 +425,6 @@ prompt_for_values() { echo "" } -# Certificate fetching function for infrastructure-only deployments -get_certs_from_core_server() { - log_info "Infrastructure-only deployment detected. Fetching certificate from core server 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 fetching" - 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_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 fetching cancelled. Please verify SSH access and try again." - exit 1 - fi - USE_SSHPASS=true # Assume password auth for copying - fi - fi - - # Fetch shared CA certificates from core server - log_info "Fetching shared CA certificates from core server..." - sudo mkdir -p "/opt/stacks/core/shared-ca" - - # First check if shared CA exists on core server (check both old and new locations) - SHARED_CA_EXISTS=false - if [ "$USE_SSHPASS" = true ] && [ -n "$SSH_PASSWORD" ]; then - if sshpass -p "$SSH_PASSWORD" ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/shared-ca/ca.pem ] && [ -f /opt/stacks/core/shared-ca/ca-key.pem ] && [ -r /opt/stacks/core/shared-ca/ca.pem ] && [ -r /opt/stacks/core/shared-ca/ca-key.pem ]" 2>/dev/null; then - SHARED_CA_EXISTS=true - SHARED_CA_PATH="/opt/stacks/core/shared-ca" - log_info "Detected CA certificate and key in shared-ca location" - elif sshpass -p "$SSH_PASSWORD" ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/docker-tls/ca.pem ] && [ -f /opt/stacks/core/docker-tls/ca-key.pem ] && [ -r /opt/stacks/core/docker-tls/ca.pem ] && [ -r /opt/stacks/core/docker-tls/ca-key.pem ]" 2>/dev/null; then - SHARED_CA_EXISTS=true - SHARED_CA_PATH="/opt/stacks/core/docker-tls" - log_info "Detected CA certificate and key in docker-tls location" - fi - else - if ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/shared-ca/ca.pem ] && [ -f /opt/stacks/core/shared-ca/ca-key.pem ] && [ -r /opt/stacks/core/shared-ca/ca.pem ] && [ -r /opt/stacks/core/shared-ca/ca-key.pem ]" 2>/dev/null; then - SHARED_CA_EXISTS=true - SHARED_CA_PATH="/opt/stacks/core/shared-ca" - log_info "Detected CA certificate and key in shared-ca location" - elif ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/docker-tls/ca.pem ] && [ -f /opt/stacks/core/docker-tls/ca-key.pem ] && [ -r /opt/stacks/core/docker-tls/ca.pem ] && [ -r /opt/stacks/core/docker-tls/ca-key.pem ]" 2>/dev/null; then - SHARED_CA_EXISTS=true - SHARED_CA_PATH="/opt/stacks/core/docker-tls" - log_info "Detected CA certificate and key in docker-tls location" - fi - fi - - if [ "$SHARED_CA_EXISTS" = true ]; then - # Copy existing shared CA from core server - if [ "$USE_SSHPASS" = true ] && [ -n "$SSH_PASSWORD" ]; then - log_info "Running: sshpass -p [PASSWORD] scp -o StrictHostKeyChecking=no $SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca.pem $SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca-key.pem /opt/stacks/core/shared-ca/" - if sshpass -p "$SSH_PASSWORD" scp -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca.pem" "$SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca-key.pem" "/opt/stacks/core/shared-ca/" 2>&1; then - log_success "Shared CA certificate and key fetched from core server" - # Generate server certificates signed by the shared CA - setup_docker_tls - else - log_warning "Failed to fetch shared CA certificate and key from core server" - SHARED_CA_EXISTS=false - fi - else - log_info "Running: scp -o StrictHostKeyChecking=no $SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca.pem $SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca-key.pem /opt/stacks/core/shared-ca/" - if scp -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca.pem" "$SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca-key.pem" "/opt/stacks/core/shared-ca/" 2>&1; then - log_success "Shared CA certificate and key fetched from core server" - # Generate server certificates signed by the shared CA - setup_docker_tls - else - log_warning "Failed to fetch shared CA certificate and key from core server" - SHARED_CA_EXISTS=false - fi - fi - fi - - if [ "$SHARED_CA_EXISTS" = false ]; then - log_warning "Shared CA certificates not found on core server." - log_info "Please ensure the core server has been set up first and has generated the shared CA certificates." - log_info "You can manually copy the certificates later (check both locations on core server):" - echo " sudo mkdir -p /opt/stacks/core/shared-ca" - echo " # Try shared-ca location:" - echo " sudo scp $SSH_USER@$CORE_SERVER_IP:/opt/stacks/core/shared-ca/ca.pem /opt/stacks/core/shared-ca/" - echo " sudo scp $SSH_USER@$CORE_SERVER_IP:/opt/stacks/core/shared-ca/ca-key.pem /opt/stacks/core/shared-ca/" - echo " # Or docker-tls location:" - echo " sudo scp $SSH_USER@$CORE_SERVER_IP:/opt/stacks/core/docker-tls/ca.pem /opt/stacks/core/shared-ca/" - echo " sudo scp $SSH_USER@$CORE_SERVER_IP:/opt/stacks/core/docker-tls/ca-key.pem /opt/stacks/core/shared-ca/" - echo " sudo chown -R $SSH_USER:$SSH_USER /opt/stacks/core/shared-ca" - echo "" - log_info "Then restart the Docker daemon: sudo systemctl reload docker" - echo "" - fi - - # Update Docker daemon configuration to use shared CA - if [ "$SHARED_CA_EXISTS" = true ]; then - 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 < Date: Mon, 26 Jan 2026 18:38:53 -0500 Subject: [PATCH 6/9] Add multi-server TLS setup instructions to manual setup guide --- docs/manual-setup.md | 57 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/docs/manual-setup.md b/docs/manual-setup.md index 9a75d98..c5cf143 100644 --- a/docs/manual-setup.md +++ b/docs/manual-setup.md @@ -150,6 +150,63 @@ cd /opt/stacks/dashboards docker compose up -d ``` +## Step 10.5: Multi-Server TLS Setup (Optional) + +If you plan to deploy services on remote servers (like Raspberry Pi) that will be managed by Sablier for lazy loading, set up shared TLS certificates. + +### On Core Server (where Traefik/Authelia run): + +```bash +# Create shared CA directory +sudo mkdir -p /opt/stacks/core/shared-ca +sudo chown $USER:$USER /opt/stacks/core/shared-ca + +# Generate shared CA certificate +cd /opt/stacks/core/shared-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=Homelab/CN=Homelab-CA" + +# Set proper permissions +chmod 600 ca-key.pem +chmod 644 ca.pem +``` + +### On Remote Servers: + +```bash +# Create TLS directory +sudo mkdir -p /opt/stacks/core/shared-ca +sudo chown $USER:$USER /opt/stacks/core/shared-ca + +# Copy shared CA from core server (replace CORE_IP with your core server IP) +scp user@CORE_IP:/opt/stacks/core/shared-ca/ca.pem /opt/stacks/core/shared-ca/ +scp user@CORE_IP:/opt/stacks/core/shared-ca/ca-key.pem /opt/stacks/core/shared-ca/ + +# Generate client certificate for Docker client connections +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 + +# Configure Docker TLS +sudo tee /etc/docker/daemon.json > /dev/null < Date: Mon, 26 Jan 2026 19:11:16 -0500 Subject: [PATCH 7/9] Update Docker Compose files for various stacks --- docker-compose/core/docker-compose.yml | 4 +-- docker-compose/dashboards/docker-compose.yml | 4 +-- .../homeassistant/docker-compose.yml | 16 ++++++++++ docker-compose/media/docker-compose.yml | 2 ++ docker-compose/monitoring/docker-compose.yml | 30 ++++++++++++------- .../productivity/docker-compose.yml | 10 +++---- docker-compose/utilities/docker-compose.yml | 8 ++--- docker-compose/vpn/docker-compose.yml | 2 +- docker-compose/wikis/docker-compose.yml | 6 ++-- tasks.txt | 24 --------------- 10 files changed, 55 insertions(+), 51 deletions(-) diff --git a/docker-compose/core/docker-compose.yml b/docker-compose/core/docker-compose.yml index 256ae09..505fe20 100644 --- a/docker-compose/core/docker-compose.yml +++ b/docker-compose/core/docker-compose.yml @@ -134,6 +134,6 @@ networks: x-dockge: urls: - https://auth.${DOMAIN} - - https://{$SERVER_IP}:9091 + - https://${SERVER_IP}:9091 - https://traefik.${DOMAIN} - - https://{$SERVER_IP}:8080 \ No newline at end of file + - https://${SERVER_IP}:8080 \ No newline at end of file diff --git a/docker-compose/dashboards/docker-compose.yml b/docker-compose/dashboards/docker-compose.yml index 19bf844..a5005d2 100644 --- a/docker-compose/dashboards/docker-compose.yml +++ b/docker-compose/dashboards/docker-compose.yml @@ -114,9 +114,9 @@ x-dockge: urls: # Proxied URLs (through Traefik) - https://homepage.${DOMAIN} - - https://{$SERVER_IP}:3003 + - https://${SERVER_IP}:3003 - https://homarr.${DOMAIN} - - https://{$SERVER_IP}:7575 + - https://${SERVER_IP}:7575 networks: homelab-network: diff --git a/docker-compose/homeassistant/docker-compose.yml b/docker-compose/homeassistant/docker-compose.yml index 396de8f..50d8c7a 100644 --- a/docker-compose/homeassistant/docker-compose.yml +++ b/docker-compose/homeassistant/docker-compose.yml @@ -240,3 +240,19 @@ networks: external: true traefik-network: external: true + +x-dockge: + urls: + # Proxied URLs (through Traefik) + - https://ha.${DOMAIN} + - http://${SERVER_IP}:8123 + - https://esphome.${DOMAIN} + - http://${SERVER_IP}:6052 + - https://tasmoadmin.${DOMAIN} + - http://${SERVER_IP}:8084 + - https://motioneye.${DOMAIN} + - http://${SERVER_IP}:8765 + - https://nodered.${DOMAIN} + - http://${SERVER_IP}:1880 + - mqtt://${SERVER_IP}:1883 + - https://zigbee2mqtt.${DOMAIN} \ No newline at end of file diff --git a/docker-compose/media/docker-compose.yml b/docker-compose/media/docker-compose.yml index 53ab220..555b2a8 100644 --- a/docker-compose/media/docker-compose.yml +++ b/docker-compose/media/docker-compose.yml @@ -128,7 +128,9 @@ x-dockge: urls: # Proxied URLs (through Traefik) - https://jellyfin.${DOMAIN} + - http://${SERVER_IP}:8096 - https://calibre.${DOMAIN} + - http://${SERVER_IP}:8083 networks: homelab-network: diff --git a/docker-compose/monitoring/docker-compose.yml b/docker-compose/monitoring/docker-compose.yml index 04a9df3..4b1cb96 100644 --- a/docker-compose/monitoring/docker-compose.yml +++ b/docker-compose/monitoring/docker-compose.yml @@ -8,18 +8,18 @@ # - See individual service comments for specific reasoning # Service Access URLs: -# - Prometheus: http://server-ip:9090 (or configure Traefik) -# - Grafana: http://server-ip:3000 (or configure Traefik) +# - Prometheus: http://${SERVER_IP}:9090 (or configure Traefik) +# - Grafana: http://${SERVER_IP}:3000 (or configure Traefik) # - Uptime Kuma: https://status.${DOMAIN} -# - Node Exporter: http://server-ip:9100/metrics -# - cAdvisor: http://server-ip:8082 -# - Loki: http://server-ip:3100 +# - Node Exporter: http://${SERVER_IP}:9100/metrics +# - cAdvisor: http://${SERVER_IP}:8082 +# - Loki: http://${SERVER_IP}:3100 # NOTE: Prometheus, Grafana, Loki use ports because they need to be accessible to other services # Add Traefik labels if you want https://prometheus.${DOMAIN} access services: # Prometheus - Metrics collection and storage - # Access at: http://server-ip:9090 + # Access at: http://${SERVER_IP}:9090 prometheus: image: prom/prometheus:v2.48.1 deploy: @@ -66,7 +66,7 @@ services: - "traefik.http.services.prometheus.loadbalancer.server.port=9090" # Grafana - Metrics visualization - # Access at: http://server-ip:3000 + # Access at: http://${SERVER_IP}:3000 # Default credentials: admin / admin (change on first login) grafana: image: grafana/grafana:10.2.3 @@ -115,7 +115,7 @@ services: - "traefik.http.services.grafana.loadbalancer.server.port=3000" # Node Exporter - Host metrics exporter - # Metrics at: http://server-ip:9100/metrics + # Metrics at: http://${SERVER_IP}:9100/metrics node-exporter: image: prom/node-exporter:v1.7.0 container_name: node-exporter @@ -138,7 +138,7 @@ services: - "homelab.description=Hardware and OS metrics exporter" # cAdvisor - Container metrics exporter - # Access at: http://server-ip:8082 + # Access at: http://${SERVER_IP}:8082 cadvisor: image: gcr.io/cadvisor/cadvisor:v0.47.2 container_name: cadvisor @@ -215,7 +215,7 @@ services: - "traefik.http.services.uptime-kuma.loadbalancer.server.port=3001" # Loki - Log aggregation - # Access at: http://server-ip:3100 + # Access at: http://${SERVER_IP}:3100 loki: image: grafana/loki:2.9.3 deploy: @@ -289,3 +289,13 @@ networks: external: true traefik-network: external: true + +x-dockge: + urls: + # Proxied URLs (through Traefik) + - http://${SERVER_IP}:9090 + - http://${SERVER_IP}:3000 + - https://uptime-kuma.${DOMAIN} + - http://${SERVER_IP}:9100/metrics + - http://${SERVER_IP}:8082 + - http://${SERVER_IP}:3100 diff --git a/docker-compose/productivity/docker-compose.yml b/docker-compose/productivity/docker-compose.yml index d417c31..dcda945 100644 --- a/docker-compose/productivity/docker-compose.yml +++ b/docker-compose/productivity/docker-compose.yml @@ -327,12 +327,12 @@ x-dockge: urls: # Proxied URLs (through Traefik) - https://nextcloud.${DOMAIN} - - https://{$SERVER_IP}:8089 + - https://${SERVER_IP}:8089 - https://mealie.${DOMAIN} - - https://{$SERVER_IP}:9000 + - https://${SERVER_IP}:9000 - https://wordpress.${DOMAIN} - - https://{$SERVER_IP}:8088 + - https://${SERVER_IP}:8088 - https://gitea.${DOMAIN} - - https://{$SERVER_IP}:3010 + - https://${SERVER_IP}:3010 - https://jupyter.${DOMAIN} - - https://{$SERVER_IP}:8890 + - https://${SERVER_IP}:8890 diff --git a/docker-compose/utilities/docker-compose.yml b/docker-compose/utilities/docker-compose.yml index 78665ff..d1cfd33 100644 --- a/docker-compose/utilities/docker-compose.yml +++ b/docker-compose/utilities/docker-compose.yml @@ -237,10 +237,10 @@ networks: x-dockge: urls: - https://backrest.${DOMAIN} - - https://{$SERVER_IP}:9898 + - https://${SERVER_IP}:9898 - https://duplicati.${DOMAIN} - - https://{$SERVER_IP}:8200 + - https://${SERVER_IP}:8200 - https://forms.${DOMAIN} - - https://{$SERVER_IP}:3002 + - https://${SERVER_IP}:3002 - https://vault.${DOMAIN} - - https://{$SERVER_IP}:8091 \ No newline at end of file + - https://${SERVER_IP}:8091 \ No newline at end of file diff --git a/docker-compose/vpn/docker-compose.yml b/docker-compose/vpn/docker-compose.yml index 8cd71a6..44183f5 100644 --- a/docker-compose/vpn/docker-compose.yml +++ b/docker-compose/vpn/docker-compose.yml @@ -54,7 +54,7 @@ services: - "traefik.http.services.qbittorrent.loadbalancer.server.port=8081" # Sablier configuration - "sablier.enable=true" - - "sablier.group=qbittorrent" + - "sablier.group=${SERVER_HOSTNAME}-qbittorrent" - "sablier.sessionDuration=1h" # qBittorrent - Torrent client diff --git a/docker-compose/wikis/docker-compose.yml b/docker-compose/wikis/docker-compose.yml index 3d2c432..3693699 100644 --- a/docker-compose/wikis/docker-compose.yml +++ b/docker-compose/wikis/docker-compose.yml @@ -181,8 +181,8 @@ x-dockge: urls: # Proxied URLs (through Traefik) - https://bookstack.${DOMAIN} - - https://{$SERVER_IP}:6875 + - https://${SERVER_IP}:6875 - https://dokuwiki.${DOMAIN} - - https://{$SERVER_IP}:8087 + - https://${SERVER_IP}:8087 - https://mediawiki.${DOMAIN} - - https://{$SERVER_IP}:8086 + - https://${SERVER_IP}:8086 diff --git a/tasks.txt b/tasks.txt index ca5fcd5..0ad27f2 100644 --- a/tasks.txt +++ b/tasks.txt @@ -4,27 +4,3 @@ I am developing the EZ-Homelab repository. The focus is on the repo not the test I'm troubleshooting the ez-homelab.sh setup process. Install notes - -Ran install script directly on fresh Debian 12 install without creating the .env file first. - There was a permission error creating .env file. - Note in order to install docker it must be run with sudo, then logout/login and rerun script without sudo. - When running with sudo it forces you to logout/login even if docker was already installed and running. If docker installed it should skip that part and continue. - -SSHed in using VS Code, manually created the .env file. - ran ez-homelab.sh completed without errors - -Sablier lazyloading wasn't working, the services were available by ip:port - -Required configure docker daemon on server to listen on port 2375 unsecured and -sudo ufw allow 2375/tcp - Need to implement TLS and configure sablier to use TLS - -Resolution - -- Fixed ez-homelab.sh to handle .env file creation as the actual user to avoid permission errors. -- Added check for Docker group membership to only prompt logout when necessary. -- Implemented TLS for Docker API (port 2376) with proper certificates. -- Configured Sablier to use TLS with mounted certificates and DOCKER_HOST=tcp://${SERVER_IP}:2376. -- Updated healthchecks in media-management services to use localhost instead of ${SERVER_IP}. -- Verified TLS connection working, Sablier detecting groups successfully. - From 7b70675aa1053b732112f2ffb468d7e38bc2c1a9 Mon Sep 17 00:00:00 2001 From: Kelin Date: Mon, 26 Jan 2026 19:51:44 -0500 Subject: [PATCH 8/9] Update README.md documentation --- docs/README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/README.md b/docs/README.md index b9bb75e..90ec496 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,6 +2,42 @@ Welcome to the AI-Homelab documentation! This is your comprehensive guide to deploying and managing a production-ready homelab infrastructure with 50+ pre-configured services. +# Mission Statement + +> ### Provide a fast, convenient, and free way to deploy homelab servers with popular features +> * DuckDNS subdomains with LetsEncrypt SSL wildcard certificates +> * Security First - Secure Single Sign On with Authelia +> * Traefik - Proxy Host for an entire homelab +> * Sablier - Lazy Loading of services +> * TLS Certificates for docker proxy +>  + +# Deployment Scenarios + +1. **Single server** + + **Select Option 1** + * Use Dockge to start the desired stacks + * Use Homepage to explore your services + + +2. **Core Server + Remote Server** + + **Core Server** + * A small low power device, like a Raspberry Pi 4 4GB. + * Deploy core stack. (Authelia, DuckDNS, Traefik, Sablier, Homepage) + * Functions as a gateway between the internet and your servers. + * Configure a remote host file for each Remote Server. + * Configure sablier.yml for services to lazyload. + * Generate TLS certificate, all servers use the same TLS certificate for dockerproxy + + **Remote Server(s)** + * Your old office or gaming PC, SBCs, second hand PCs, etc + * Select Option 3 to install Infrastructure only + * Ensure the TLS certificate was copied correctly and docker daemon configured + * Use Dockge to start the desired stacks + * Use Homepage to explore your services + ## 📚 Documentation Structure ### 🚀 Getting Started From ea91151829f1f412de1410d89c8540d593f6aa35 Mon Sep 17 00:00:00 2001 From: Kelin Date: Mon, 26 Jan 2026 23:24:23 -0500 Subject: [PATCH 9/9] Minor fixes and improvements to ez-homelab.sh --- scripts/ez-homelab.sh | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/scripts/ez-homelab.sh b/scripts/ez-homelab.sh index 122d661..457ce9c 100755 --- a/scripts/ez-homelab.sh +++ b/scripts/ez-homelab.sh @@ -503,17 +503,16 @@ system_setup() { log_info "Step 8/10: Configuring automatic updates..." dpkg-reconfigure -f noninteractive unattended-upgrades - # 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 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 9: Set proper ownership + log_info "Step 9/10: Setting directory ownership..." + chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt + log_success "System setup completed!" echo "" if [ "$NEEDS_LOGOUT" = true ]; then @@ -766,8 +765,8 @@ 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" + sudo mkdir -p "$TLS_DIR" + sudo 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