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
This commit is contained in:
Kelin
2026-01-25 23:08:01 -05:00
parent 1b3c4ff9ff
commit 89ca29918b
6 changed files with 654 additions and 121 deletions

3
.gitignore vendored
View File

@@ -71,6 +71,9 @@ yarn-error.log*
*.pfx *.pfx
acme.json acme.json
# Docker TLS certificates directory
docker-tls/
# Nextcloud application files (should be mounted via volumes) # Nextcloud application files (should be mounted via volumes)
docker-compose/productivity/nextcloud/html/ docker-compose/productivity/nextcloud/html/

View File

@@ -99,10 +99,10 @@ services:
# Sablier - Lazy loading service for Docker containers # Sablier - Lazy loading service for Docker containers
# Controls startup/shutdown of lazy-loaded services, must always run # Controls startup/shutdown of lazy-loaded services, must always run
# REQUIREMENTS FOR DOCKER API ACCESS: # 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 # 2. DOCKER_HOST environment variable must point to accessible Docker API endpoint
# 3. Firewall must allow TCP connections to Docker API port (default 2375) # 3. Firewall must allow TCP connections to Docker API port (2376)
# 4. For production, consider using TLS for Docker API communication # 4. TLS certificates must be mounted and environment variables set
# 5. Ensure dockerproxy service is running and accessible # 5. Ensure dockerproxy service is running and accessible
sablier-service: sablier-service:
image: sablierapp/sablier:latest image: sablierapp/sablier:latest
@@ -115,7 +115,11 @@ services:
- SABLIER_DOCKER_API_VERSION=1.51 - SABLIER_DOCKER_API_VERSION=1.51
- SABLIER_DOCKER_NETWORK=traefik-network - SABLIER_DOCKER_NETWORK=traefik-network
- SABLIER_LOG_LEVEL=debug - 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: ports:
- 10000:10000 - 10000:10000
labels: labels:

View File

@@ -28,7 +28,7 @@ services:
- PGID=${PGID} - PGID=${PGID}
- TZ=${TZ} - TZ=${TZ}
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://${SERVER_IP}:8989/"] test: ["CMD", "curl", "-f", "http://localhost:8989/"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
@@ -73,7 +73,7 @@ services:
- PGID=${PGID} - PGID=${PGID}
- TZ=${TZ} - TZ=${TZ}
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://${SERVER_IP}:7878/"] test: ["CMD", "curl", "-f", "http://localhost:7878/"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
@@ -116,7 +116,7 @@ services:
- PGID=${PGID} - PGID=${PGID}
- TZ=${TZ} - TZ=${TZ}
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://${SERVER_IP}:9696/"] test: ["CMD", "curl", "-f", "http://localhost:9696/"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
@@ -315,7 +315,7 @@ services:
- LOG_LEVEL=info - LOG_LEVEL=info
- TZ=${TZ} - TZ=${TZ}
healthcheck: healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://${SERVER_IP}:5055/"] test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:5055/"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3

View File

@@ -115,3 +115,302 @@ docker stop <service>
Access your service by the proxy url. 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=<SERVER_IP>" -new -key server-key.pem -out server.csr
echo "subjectAltName = DNS:<SERVER_IP>,IP:<SERVER_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.<your-domain>
```
## 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://<REMOTE_SERVER_IP>: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=<REMOTE_HOSTNAME>-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-<remote_server>.yml`
```yaml
http:
routers:
sonarr-remote:
rule: "Host(`sonarr.<DOMAIN>`)"
entrypoints:
- websecure
service: sonarr-remote
tls:
certResolver: letsencrypt
middlewares:
- sablier-<remote_hostname>-arr@file
- authelia@docker
services:
sonarr-remote:
loadBalancer:
servers:
- url: "http://<REMOTE_IP>:8989"
passHostHeader: true
```
2. **Create Sablier middleware configuration**:
`/opt/stacks/core/traefik/dynamic/sablier.yml`
```yaml
http:
middlewares:
sablier-<remote_hostname>-arr:
plugin:
sablier:
sablierUrl: http://sablier-service:10000
group: <remote_hostname>-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.<DOMAIN>`
- 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.

View File

@@ -33,6 +33,13 @@ log_error() {
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
REPO_DIR="$( cd "$SCRIPT_DIR/.." && 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 # Default values
DOMAIN="" DOMAIN=""
SERVER_IP="" SERVER_IP=""
@@ -56,8 +63,12 @@ load_env_file() {
echo " Domain: ${DOMAIN:-Not set}" echo " Domain: ${DOMAIN:-Not set}"
echo " Server IP: ${SERVER_IP:-Not set}" echo " Server IP: ${SERVER_IP:-Not set}"
echo " Server Hostname: ${SERVER_HOSTNAME:-Not set}" echo " Server Hostname: ${SERVER_HOSTNAME:-Not set}"
echo " Admin User: ${AUTHELIA_ADMIN_USER:-Not set}" echo " Default User: ${DEFAULT_USER:-Not set}"
echo " Admin Email: ${AUTHELIA_ADMIN_EMAIL:-Not set}" if [ -n "${DEFAULT_PASSWORD:-}" ]; then
echo " Default Password: [HIDDEN]"
else
echo " Default Password: Not set"
fi
echo " Timezone: ${TZ:-Not set}" echo " Timezone: ${TZ:-Not set}"
echo "" echo ""
@@ -74,27 +85,29 @@ save_env_file() {
# Create .env file if it doesn't exist # Create .env file if it doesn't exist
if [ ! -f "$REPO_DIR/.env" ]; then 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 fi
# Update values # Update values as the actual user
sed -i "s%DOMAIN=.*%DOMAIN=$DOMAIN%" "$REPO_DIR/.env" sudo -u "$ACTUAL_USER" sed -i "s%DOMAIN=.*%DOMAIN=$DOMAIN%" "$REPO_DIR/.env"
sed -i "s%SERVER_IP=.*%SERVER_IP=$SERVER_IP%" "$REPO_DIR/.env" sudo -u "$ACTUAL_USER" sed -i "s%SERVER_IP=.*%SERVER_IP=$SERVER_IP%" "$REPO_DIR/.env"
sed -i "s%SERVER_HOSTNAME=.*%SERVER_HOSTNAME=$SERVER_HOSTNAME%" "$REPO_DIR/.env" sudo -u "$ACTUAL_USER" sed -i "s%SERVER_HOSTNAME=.*%SERVER_HOSTNAME=$SERVER_HOSTNAME%" "$REPO_DIR/.env"
sed -i "s%TZ=.*%TZ=$TZ%" "$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 if [ "$DEPLOY_CORE" = true ]; then
# Ensure we have admin credentials # Ensure we have admin credentials
if [ -z "$ADMIN_USER" ]; then if [ -z "$ADMIN_USER" ]; then
ADMIN_USER="admin" ADMIN_USER="${DEFAULT_USER:-admin}"
fi fi
if [ -z "$ADMIN_EMAIL" ]; then if [ -z "$ADMIN_EMAIL" ]; then
ADMIN_EMAIL="${ADMIN_USER}@${DOMAIN}" ADMIN_EMAIL="${DEFAULT_EMAIL:-${ADMIN_USER}@${DOMAIN}}"
fi fi
if [ -z "$ADMIN_PASSWORD" ]; then if [ -z "$ADMIN_PASSWORD" ]; then
log_info "Using default admin password (changeme123) - please change this after setup!" ADMIN_PASSWORD="${DEFAULT_PASSWORD:-changeme123}"
ADMIN_PASSWORD="changeme123" if [ "$ADMIN_PASSWORD" = "changeme123" ]; then
log_info "Using default admin password (changeme123) - please change this after setup!"
fi
fi fi
if [ -z "$AUTHELIA_JWT_SECRET" ]; then if [ -z "$AUTHELIA_JWT_SECRET" ]; then
@@ -108,11 +121,11 @@ save_env_file() {
fi fi
# Save Authelia settings to .env # Save Authelia settings to .env
sed -i "s%AUTHELIA_JWT_SECRET=.*%AUTHELIA_JWT_SECRET=$AUTHELIA_JWT_SECRET%" "$REPO_DIR/.env" sudo -u "$ACTUAL_USER" 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" sudo -u "$ACTUAL_USER" 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" sudo -u "$ACTUAL_USER" 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" sudo -u "$ACTUAL_USER" 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_ADMIN_EMAIL=.*%AUTHELIA_ADMIN_EMAIL=$ADMIN_EMAIL%" "$REPO_DIR/.env"
# Generate password hash if needed # Generate password hash if needed
if [ -z "$AUTHELIA_ADMIN_PASSWORD" ]; then if [ -z "$AUTHELIA_ADMIN_PASSWORD" ]; then
@@ -129,8 +142,8 @@ save_env_file() {
fi fi
# Save password hash # Save password hash
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"
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 fi
log_success "Configuration saved to .env file" log_success "Configuration saved to .env file"
@@ -139,79 +152,192 @@ save_env_file() {
# Prompt for required values # Prompt for required values
prompt_for_values() { prompt_for_values() {
echo "" echo ""
log_info "Please provide the following information:" log_info "Configuration Setup:"
echo " (Press Enter without typing to keep the current/default value shown in brackets)"
echo "" echo ""
# Domain # Set defaults from env file or hardcoded fallbacks
if [ -z "$DOMAIN" ]; then DEFAULT_DOMAIN="${DOMAIN:-example.duckdns.org}"
read -p "Enter your domain (e.g., example.duckdns.org): " DOMAIN DEFAULT_SERVER_IP="${SERVER_IP:-$(hostname -I | awk '{print $1}')}"
while [ -z "$DOMAIN" ]; do DEFAULT_SERVER_HOSTNAME="${SERVER_HOSTNAME:-$(hostname)}"
log_warning "Domain is required" DEFAULT_TZ="${TZ:-America/New_York}"
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
# Server IP # Display current/default configuration
if [ -z "$SERVER_IP" ]; then echo "Please review the following configuration:"
read -p "Enter your server IP address: " SERVER_IP echo " Domain: $DEFAULT_DOMAIN"
while [ -z "$SERVER_IP" ]; do echo " Server IP: $DEFAULT_SERVER_IP"
log_warning "Server IP is required" echo " Server Hostname: $DEFAULT_SERVER_HOSTNAME"
read -p "Enter your server IP address: " SERVER_IP echo " Timezone: $DEFAULT_TZ"
done
else
read -p "Server IP [$SERVER_IP] (press Enter to keep current): " input
[ -n "$input" ] && SERVER_IP="$input"
fi
# 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 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 "" echo ""
log_info "Authelia Admin Credentials:"
if [ -z "$ADMIN_USER" ]; then # Domain
ADMIN_USER="admin" 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 fi
read -p "Admin username [$ADMIN_USER] (press Enter to keep current): " input else
[ -n "$input" ] && ADMIN_USER="$input" # Use defaults
DOMAIN="$DEFAULT_DOMAIN"
SERVER_IP="$DEFAULT_SERVER_IP"
SERVER_HOSTNAME="$DEFAULT_SERVER_HOSTNAME"
TZ="$DEFAULT_TZ"
if [ -z "$ADMIN_EMAIL" ]; then if [ "$DEPLOY_CORE" = true ]; then
ADMIN_EMAIL="${ADMIN_USER}@${DOMAIN}" ADMIN_USER="$DEFAULT_ADMIN_USER"
ADMIN_EMAIL="$DEFAULT_ADMIN_EMAIL"
fi fi
read -p "Admin email [$ADMIN_EMAIL] (press Enter to keep current): " input fi
[ -n "$input" ] && ADMIN_EMAIL="$input"
if [ -z "$ADMIN_PASSWORD" ]; then echo ""
while [ -z "$ADMIN_PASSWORD" ]; do }
read -s -p "Admin password (will be hashed): " ADMIN_PASSWORD
echo "" # Certificate sharing function for infrastructure-only deployments
if [ ${#ADMIN_PASSWORD} -lt 8 ]; then share_certs_with_core() {
log_warning "Password must be at least 8 characters" log_info "Infrastructure-only deployment detected. Setting up certificate sharing for remote Docker control..."
ADMIN_PASSWORD=""
fi # Prompt for core server IP
done 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 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
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 <<EOF
{
"tls": true,
"tlsverify": true,
"tlscacert": "/opt/stacks/core/shared-ca/ca.pem",
"tlscert": "/home/$USER/EZ-Homelab/docker-tls/server-cert.pem",
"tlskey": "/home/$USER/EZ-Homelab/docker-tls/server-key.pem"
}
EOF
sudo cp /tmp/daemon.json /etc/docker/daemon.json
sudo systemctl reload docker
log_success "Docker daemon updated to use shared CA"
log_info "Core server can now securely connect to this Docker daemon using shared CA"
else
log_warning "Shared CA certificate not found, daemon configuration not updated"
fi
echo "" echo ""
} }
@@ -235,12 +361,18 @@ system_setup() {
# Step 2: Install required packages # Step 2: Install required packages
log_info "Step 2/10: Installing required packages..." log_info "Step 2/10: Installing required packages..."
apt-get install -y curl wget git htop nano vim ufw fail2ban unattended-upgrades apt-listchanges apt-get install -y curl wget git htop nano vim ufw fail2ban unattended-upgrades apt-listchanges sshpass
# Step 3: Install Docker # Step 3: Install Docker
log_info "Step 3/10: Installing Docker..." log_info "Step 3/10: Installing Docker..."
if command -v docker &> /dev/null && docker --version &> /dev/null; then if command -v docker &> /dev/null && docker --version &> /dev/null; then
log_success "Docker is already installed ($(docker --version))" 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 # Check if Docker service is running
if ! systemctl is-active --quiet docker; then if ! systemctl is-active --quiet docker; then
log_warning "Docker service is not running, starting it..." log_warning "Docker service is not running, starting it..."
@@ -253,6 +385,7 @@ system_setup() {
else else
curl -fsSL https://get.docker.com | sh curl -fsSL https://get.docker.com | sh
usermod -aG docker "$ACTUAL_USER" usermod -aG docker "$ACTUAL_USER"
NEEDS_LOGOUT=true
fi fi
# Step 4: Install Docker Compose # Step 4: Install Docker Compose
@@ -265,50 +398,47 @@ system_setup() {
log_success "Docker Compose installed ($(docker-compose --version))" log_success "Docker Compose installed ($(docker-compose --version))"
fi fi
# Step 5: Configure UFW firewall # Step 5: Generate shared CA for multi-server TLS
log_info "Step 5/10: Configuring firewall..." 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 --force enable
ufw allow ssh ufw allow ssh
ufw allow 80 ufw allow 80
ufw allow 443 ufw allow 443
ufw allow 2376/tcp # Docker TLS port
log_success "Firewall configured"
# Step 6: Configure automatic updates # Step 8: Configure automatic updates
log_info "Step 6/10: Configuring automatic updates..." log_info "Step 8/10: Configuring automatic updates..."
dpkg-reconfigure -f noninteractive unattended-upgrades dpkg-reconfigure -f noninteractive unattended-upgrades
# Step 7: Create required directories # Step 9: Set proper ownership
log_info "Step 7/10: Creating required directories..." log_info "Step 9/10: Setting directory ownership..."
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..."
chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/stacks chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/stacks
chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/dockge chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/dockge
# Step 9: Create Docker networks # Step 10: Create Docker networks
log_info "Step 9/10: Creating 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 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 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" 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!" log_success "System setup completed!"
echo "" echo ""
log_info "Please log out and back in for Docker group changes to take effect." if [ "$NEEDS_LOGOUT" = true ]; then
echo "" log_info "Please log out and back in for Docker group changes to take effect."
echo ""
fi
} }
# Deployment function # 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/\${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 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 # Deploy core stack
cd /opt/stacks/core cd /opt/stacks/core
docker compose up -d docker compose up -d
@@ -479,7 +616,57 @@ perform_deployment() {
fi 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 <<EOF
{
"tls": true,
"tlsverify": true,
"tlscacert": "$TLS_DIR/ca.pem",
"tlscert": "$TLS_DIR/server-cert.pem",
"tlskey": "$TLS_DIR/server-key.pem"
}
EOF
# Update systemd service
sed -i 's|-H fd://|-H fd:// -H tcp://0.0.0.0:2376|' /lib/systemd/system/docker.service
# Reload and restart Docker
systemctl daemon-reload
systemctl restart docker
log_success "Docker TLS configured on port 2376"
}
setup_stacks_for_dockge() { setup_stacks_for_dockge() {
log_info "Setting up all stacks for Dockge..." log_info "Setting up all stacks for Dockge..."
@@ -627,12 +814,32 @@ main() {
fi fi
fi fi
# Ensure required directories exist
log_info "Ensuring required directories exist..."
if [ "$EUID" -eq 0 ]; then
mkdir -p /opt/stacks/core
mkdir -p /opt/stacks/infrastructure
mkdir -p /opt/stacks/dashboards
mkdir -p /opt/dockge
else
sudo mkdir -p /opt/stacks/core
sudo mkdir -p /opt/stacks/infrastructure
sudo mkdir -p /opt/stacks/dashboards
sudo mkdir -p /opt/dockge
fi
log_success "Directories ready"
# Prompt for configuration values # Prompt for configuration values
prompt_for_values prompt_for_values
# Save configuration # Save configuration
save_env_file save_env_file
# Handle certificate sharing for infrastructure-only deployments
if [ "$MAIN_CHOICE" = "3" ]; then
share_certs_with_core
fi
# Perform deployment # Perform deployment
perform_deployment perform_deployment

View File

@@ -1,10 +1,30 @@
Info Info
I am developing the EZ-Homelab repository. The focus is on the repo not the test system. I am developing the EZ-Homelab repository. The focus is on the repo not the test system.
I'm troubleshooting the ez-homelab.sh setup process. The script works pretty well. However I noticed some of the compose files I'm troubleshooting the ez-homelab.sh setup process.
are missing a ports section. In order for a remote traefik & sablier install to work, the sablier.yml and traefik external host yml
files are configured on the remote server using the ip:port of the service. Therefore all services in all compose files must include
port mapping if it has a webui.
Make the changes in the repo folder, then I will run the ez-homelab.sh script and check the results.
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.