From ed17bf295aa575fc0990ee66a28c71466461e347 Mon Sep 17 00:00:00 2001 From: Kelin Date: Tue, 3 Feb 2026 21:07:36 -0500 Subject: [PATCH] Fix variable substitution in users_database.yml to preserve password hashes - Modified load_env_file_safely to prevent expansion of $ in .env values - Updated localize_users_database_file to handle nested variables correctly - Added fresh template copying in deploy-core.sh to ensure reliable processing - Fixed password hash corruption during deployment --- docker-compose/core/deploy-core.sh | 55 ++++++++ scripts/common.sh | 217 +++++++++++++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100755 docker-compose/core/deploy-core.sh create mode 100644 scripts/common.sh diff --git a/docker-compose/core/deploy-core.sh b/docker-compose/core/deploy-core.sh new file mode 100755 index 0000000..66c491f --- /dev/null +++ b/docker-compose/core/deploy-core.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Deploy core stack script +# Run from /opt/stacks/core/ + +set -e + +# Source common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="/home/kelin/EZ-Homelab" # Fixed repo path since script runs from /opt/stacks/core +source "$REPO_DIR/scripts/common.sh" + +log_info "Deploying core stack..." + +# Load environment +load_env_file_safely .env + +# Copy fresh templates +cp "$REPO_DIR/docker-compose/core/authelia/secrets/users_database.yml" "./authelia/secrets/users_database.yml" + +# Localize labels in compose file (only replaces variables in labels, not environment sections) +localize_compose_labels docker-compose.yml + +# Localize config files - Process all YAML config files (excluding docker-compose.yml) +# This performs FULL variable replacement on config files like: +# - authelia/config/configuration.yml +# - authelia/secrets/users_database.yml <- HANDLED SPECIALLY to preserve password hashes +# - traefik/dynamic/*.yml +# +# Why exclude docker-compose.yml? +# - It was already processed above with localize_compose_labels (labels-only replacement) +# - Config files need full replacement (including nested variables) while compose labels +# should only have selective replacement to avoid Docker interpreting $ characters +# +# The localize_config_file function uses envsubst with recursive expansion to handle +# nested variables like ${AUTHELIA_ADMIN_PASSWORD_HASH} or ${SERVICE_NAME}.${DOMAIN} +# The localize_users_database_file function handles password hashes specially to avoid corruption +for config_file in $(find . -name "*.yml" -o -name "*.yaml" | grep -v docker-compose.yml); do + if [[ "$config_file" == *"users_database.yml" ]]; then + localize_users_database_file "$config_file" + else + localize_config_file "$config_file" + fi +done + +# Deploy +run_cmd docker compose up -d + +# Validate +if docker ps | grep -q traefik && docker ps | grep -q authelia; then + log_success "Core stack deployed successfully" + exit 0 +else + log_error "Core stack deployment failed" + exit 1 +fi diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100644 index 0000000..68271d0 --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,217 @@ +#!/bin/bash +# EZ-Homelab Common Functions Library +# Shared utilities for deploy scripts + +# Debug logging configuration +DEBUG=${DEBUG:-false} +VERBOSE=${VERBOSE:-false} # New verbosity toggle +DEBUG_LOG_FILE="/tmp/ez-homelab-debug.log" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Debug logging function +debug_log() { + if [ "$DEBUG" = true ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') [DEBUG] $1" >> "$DEBUG_LOG_FILE" + fi +} + +# Initialize debug log +if [ "$DEBUG" = true ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') [DEBUG] ===== EZ-HOMELAB COMMON LIBRARY STARTED =====" > "$DEBUG_LOG_FILE" + debug_log "Common library loaded" +fi + +# Log functions +log_info() { + if [ "$VERBOSE" = true ]; then + echo -e "${BLUE}[INFO]${NC} $1" + fi + debug_log "[INFO] $1" +} + +log_success() { + if [ "$VERBOSE" = true ]; then + echo -e "${GREEN}[SUCCESS]${NC} $1" + fi + debug_log "[SUCCESS] $1" +} + +log_warning() { + if [ "$VERBOSE" = true ]; then + echo -e "${YELLOW}[WARNING]${NC} $1" + fi + debug_log "[WARNING] $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" + debug_log "[ERROR] $1" +} + +# Safely load environment variables from .env file +load_env_file_safely() { + local env_file="$1" + debug_log "Loading env file safely: $env_file" + + if [ ! -f "$env_file" ]; then + debug_log "Env file does not exist: $env_file" + return 1 + fi + + # Read the .env file line by line and export variables safely + while IFS= read -r line || [ -n "$line" ]; do + # Skip comments and empty lines + [[ $line =~ ^[[:space:]]*# ]] && continue + [[ -z "$line" ]] && continue + + # Parse KEY=VALUE, handling quoted values + if [[ $line =~ ^([^=]+)=(.*)$ ]]; then + local key="${BASH_REMATCH[1]}" + local value=$(printf '%s\n' "${BASH_REMATCH[2]}" | sed 's/^"//' | sed 's/"$//' | sed "s/^'//" | sed "s/'$//") + + # Strip inline comments + value=${value%%#*} + + # Trim whitespace from key and value + key=$(echo "$key" | xargs) + value=$(echo "$value" | xargs) + + # Strip surrounding quotes if present + if [[ $value =~ ^\"(.*)\"$ ]]; then + value="${BASH_REMATCH[1]}" + elif [[ $value =~ ^\'(.*)\'$ ]]; then + value="${BASH_REMATCH[1]}" + fi + + # Strip carriage return if present (DOS line endings) + value=${value%$'\r'} + + # Export the variable + export "$key"="$value" + + debug_log "Exported $key=[HIDDEN]" # Don't log actual values for security + fi + done < "$env_file" + + debug_log "Env file loaded successfully" +} + +# Function to localize compose labels (only labels, not environment variables) +localize_compose_labels() { + local file_path="$1" + debug_log "localize_compose_labels called for file: $file_path" + + if [ ! -f "$file_path" ]; then + log_warning "File $file_path does not exist, skipping compose labels localization" + return + fi + + # Create a temporary file for processing + temp_file="$file_path.tmp" + cp "$file_path" "$temp_file" + + # Use envsubst to replace ${VAR} in labels only, with nested expansion + # This handles labels like "traefik.http.routers.${SERVICE_NAME}.rule=Host(`${SERVICE_NAME}.${DOMAIN}`)" + if command -v envsubst >/dev/null 2>&1; then + changed=true + while [ "$changed" = true ]; do + changed=false + new_content=$(envsubst < "$temp_file") + if [ "$new_content" != "$(cat "$temp_file")" ]; then + changed=true + echo "$new_content" > "$temp_file" + fi + done + mv "$temp_file" "$file_path" + debug_log "Replaced variables in compose labels for $file_path" + else + log_warning "envsubst not available, cannot localize compose labels for $file_path" + rm -f "$temp_file" + return + fi +} + +# Function to localize users_database.yml with special handling for password hashes +localize_users_database_file() { + local file_path="$1" + debug_log "localize_users_database_file called for file: $file_path" + + if [ ! -f "$file_path" ]; then + log_warning "File $file_path does not exist, skipping users database localization" + return + fi + + # Create a temporary file for processing + temp_file="$file_path.tmp" + cp "$file_path" "$temp_file" + + # Resolve nested variables first + local resolved_user="${AUTHELIA_ADMIN_USER}" + local resolved_email=$(eval echo "${AUTHELIA_ADMIN_EMAIL}") + local resolved_password="${AUTHELIA_ADMIN_PASSWORD_HASH}" + + # Escape $ in password hash for sed + local escaped_password=$(printf '%s\n' "$resolved_password" | sed 's/\$/\\$/g') + + # Use sed to substitute the resolved values + sed -i "s|\${AUTHELIA_ADMIN_USER}|$resolved_user|g" "$temp_file" + sed -i "s|\${AUTHELIA_ADMIN_EMAIL}|$resolved_email|g" "$temp_file" + sed -i "s|\${AUTHELIA_ADMIN_PASSWORD_HASH}|$escaped_password|g" "$temp_file" + sed -i "s|\${DEFAULT_EMAIL}|$resolved_email|g" "$temp_file" + + mv "$temp_file" "$file_path" + debug_log "Replaced variables in users database file $file_path" +} + +# Function to localize config files (full replacement) +localize_config_file() { + local file_path="$1" + debug_log "localize_config_file called for file: $file_path" + + if [ ! -f "$file_path" ]; then + log_warning "File $file_path does not exist, skipping config file localization" + return + fi + + # Use envsubst to replace all ${VAR} with environment values, handling nested variables + if command -v envsubst >/dev/null 2>&1; then + temp_file="$file_path.tmp" + cp "$file_path" "$temp_file" + changed=true + while [ "$changed" = true ]; do + changed=false + new_content=$(envsubst < "$temp_file") + if [ "$new_content" != "$(cat "$temp_file")" ]; then + changed=true + echo "$new_content" > "$temp_file" + fi + done + mv "$temp_file" "$file_path" + debug_log "Replaced variables in config file $file_path" + else + log_warning "envsubst not available, cannot localize config file $file_path" + rm -f "$temp_file" + return + fi +} + +# Enhanced command execution with error handling +run_cmd() { + if [ "$DRY_RUN" = true ] || [ "$TEST_MODE" = true ]; then + echo "[DRY-RUN/TEST] $@" + return 0 + else + if "$@"; then + return 0 + else + log_error "Command failed: $@" + return 1 + fi + fi +}