#!/bin/bash # Complete Secrets Management Implementation # Comprehensive Docker secrets management for HomeAudit infrastructure set -euo pipefail # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" SECRETS_DIR="$PROJECT_ROOT/secrets" LOG_FILE="$PROJECT_ROOT/logs/secrets-management-$(date +%Y%m%d-%H%M%S).log" # Create directories mkdir -p "$SECRETS_DIR"/{env,files,docker,validation} "$(dirname "$LOG_FILE")" # Logging function log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" } # Generate secure random password generate_password() { local length="${1:-32}" openssl rand -base64 "$length" | tr -d "=+/" | cut -c1-"$length" } # Create Docker secret safely create_docker_secret() { local secret_name="$1" local secret_value="$2" local overwrite="${3:-false}" # Check if secret already exists if docker secret inspect "$secret_name" >/dev/null 2>&1; then if [[ "$overwrite" == "true" ]]; then log "⚠️ Secret $secret_name exists, removing..." docker secret rm "$secret_name" || true sleep 1 else log "✅ Secret $secret_name already exists, skipping" return 0 fi fi # Create the secret echo "$secret_value" | docker secret create "$secret_name" - >/dev/null log "✅ Created Docker secret: $secret_name" } # Collect existing secrets from running containers collect_existing_secrets() { log "Collecting existing secrets from running containers..." local secrets_inventory="$SECRETS_DIR/existing-secrets-inventory.yaml" cat > "$secrets_inventory" << 'EOF' # Existing Secrets Inventory # Collected from running containers secrets_found: EOF # Scan running containers docker ps --format "{{.Names}}" | while read -r container; do if [[ -z "$container" ]]; then continue; fi log "Scanning container: $container" # Extract environment variables (sanitized) local env_file="$SECRETS_DIR/env/${container}.env" docker exec "$container" env 2>/dev/null | \ grep -iE "(password|secret|key|token|api)" | \ sed 's/=.*$/=REDACTED/' > "$env_file" || touch "$env_file" # Check for mounted secret files local mounts_file="$SECRETS_DIR/files/${container}-mounts.txt" docker inspect "$container" 2>/dev/null | \ jq -r '.[].Mounts[]? | select(.Type=="bind") | .Source' | \ grep -iE "(secret|key|cert|password)" > "$mounts_file" 2>/dev/null || touch "$mounts_file" # Add to inventory if [[ -s "$env_file" || -s "$mounts_file" ]]; then cat >> "$secrets_inventory" << EOF $container: env_secrets: $(wc -l < "$env_file") mounted_secrets: $(wc -l < "$mounts_file") env_file: "$env_file" mounts_file: "$mounts_file" EOF fi done log "✅ Secrets inventory created: $secrets_inventory" } # Generate all required Docker secrets generate_docker_secrets() { log "Generating Docker secrets for all services..." # Database secrets create_docker_secret "pg_root_password" "$(generate_password 32)" create_docker_secret "mariadb_root_password" "$(generate_password 32)" create_docker_secret "redis_password" "$(generate_password 24)" # Application secrets create_docker_secret "nextcloud_db_password" "$(generate_password 32)" create_docker_secret "nextcloud_admin_password" "$(generate_password 24)" create_docker_secret "immich_db_password" "$(generate_password 32)" create_docker_secret "paperless_secret_key" "$(generate_password 64)" create_docker_secret "vaultwarden_admin_token" "$(generate_password 48)" create_docker_secret "grafana_admin_password" "$(generate_password 24)" # API tokens and keys create_docker_secret "ha_api_token" "$(generate_password 64)" create_docker_secret "jellyfin_api_key" "$(generate_password 32)" create_docker_secret "gitea_secret_key" "$(generate_password 64)" create_docker_secret "traefik_dashboard_password" "$(htpasswd -nbB admin $(generate_password 16) | cut -d: -f2)" # SSL/TLS certificates (if not using Let's Encrypt) if [[ ! -f "$SECRETS_DIR/files/tls.crt" ]]; then log "Generating self-signed SSL certificate..." openssl req -x509 -newkey rsa:4096 -keyout "$SECRETS_DIR/files/tls.key" -out "$SECRETS_DIR/files/tls.crt" -days 365 -nodes -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost" >/dev/null 2>&1 create_docker_secret "tls_certificate" "$(cat "$SECRETS_DIR/files/tls.crt")" create_docker_secret "tls_private_key" "$(cat "$SECRETS_DIR/files/tls.key")" fi log "✅ All Docker secrets generated successfully" } # Create secrets mapping file for stack updates create_secrets_mapping() { log "Creating secrets mapping configuration..." local mapping_file="$SECRETS_DIR/docker-secrets-mapping.yaml" cat > "$mapping_file" << 'EOF' # Docker Secrets Mapping # Maps environment variables to Docker secrets secrets_mapping: postgresql: POSTGRES_PASSWORD: pg_root_password POSTGRES_DB_PASSWORD: pg_root_password mariadb: MYSQL_ROOT_PASSWORD: mariadb_root_password MARIADB_ROOT_PASSWORD: mariadb_root_password redis: REDIS_PASSWORD: redis_password nextcloud: MYSQL_PASSWORD: nextcloud_db_password NEXTCLOUD_ADMIN_PASSWORD: nextcloud_admin_password immich: DB_PASSWORD: immich_db_password paperless: PAPERLESS_SECRET_KEY: paperless_secret_key vaultwarden: ADMIN_TOKEN: vaultwarden_admin_token homeassistant: SUPERVISOR_TOKEN: ha_api_token grafana: GF_SECURITY_ADMIN_PASSWORD: grafana_admin_password jellyfin: JELLYFIN_API_KEY: jellyfin_api_key gitea: GITEA__security__SECRET_KEY: gitea_secret_key # File secrets (certificates, keys) file_secrets: tls_certificate: /run/secrets/tls_certificate tls_private_key: /run/secrets/tls_private_key EOF log "✅ Secrets mapping created: $mapping_file" } # Update stack files to use Docker secrets update_stacks_with_secrets() { log "Updating stack files to use Docker secrets..." local stacks_dir="$PROJECT_ROOT/stacks" local backup_dir="$PROJECT_ROOT/backups/stacks-pre-secrets-$(date +%Y%m%d-%H%M%S)" # Create backup mkdir -p "$backup_dir" find "$stacks_dir" -name "*.yml" -exec cp {} "$backup_dir/" \; log "✅ Stack files backed up to: $backup_dir" # Update each stack file find "$stacks_dir" -name "*.yml" | while read -r stack_file; do local stack_name stack_name=$(basename "$stack_file" .yml) log "Updating stack file: $stack_name" # Create updated stack with secrets python3 << PYTHON_SCRIPT import yaml import re import sys stack_file = "$stack_file" try: # Load the stack file with open(stack_file, 'r') as f: stack_data = yaml.safe_load(f) # Ensure secrets section exists if 'secrets' not in stack_data: stack_data['secrets'] = {} # Process services if 'services' in stack_data: for service_name, service_config in stack_data['services'].items(): if 'environment' in service_config: env_vars = service_config['environment'] # Convert environment list to dict if needed if isinstance(env_vars, list): env_dict = {} for env in env_vars: if '=' in env: key, value = env.split('=', 1) env_dict[key] = value else: env_dict[env] = '' env_vars = env_dict service_config['environment'] = env_vars # Update password/secret environment variables secrets_added = [] for env_key, env_value in list(env_vars.items()): if any(keyword in env_key.lower() for keyword in ['password', 'secret', 'key', 'token']): # Convert to _FILE pattern for Docker secrets file_env_key = env_key + '_FILE' secret_name = env_key.lower().replace('_', '_') # Map common secret names secret_mappings = { 'postgres_password': 'pg_root_password', 'mysql_password': 'nextcloud_db_password', 'mysql_root_password': 'mariadb_root_password', 'db_password': service_name + '_db_password', 'admin_password': service_name + '_admin_password', 'secret_key': service_name + '_secret_key', 'api_token': service_name + '_api_token' } mapped_secret = secret_mappings.get(secret_name, secret_name) # Update environment to use secrets file env_vars[file_env_key] = f'/run/secrets/{mapped_secret}' if env_key in env_vars: del env_vars[env_key] # Add to secrets section stack_data['secrets'][mapped_secret] = {'external': True} secrets_added.append(mapped_secret) # Add secrets to service if any were added if secrets_added: if 'secrets' not in service_config: service_config['secrets'] = [] service_config['secrets'].extend(secrets_added) # Write updated stack file with open(stack_file, 'w') as f: yaml.dump(stack_data, f, default_flow_style=False, indent=2, sort_keys=False) print(f"✅ Updated {stack_file} with Docker secrets") except Exception as e: print(f"❌ Error updating {stack_file}: {e}") sys.exit(1) PYTHON_SCRIPT done log "✅ All stack files updated to use Docker secrets" } # Validate secrets configuration validate_secrets() { log "Validating secrets configuration..." local validation_report="$SECRETS_DIR/validation-report.yaml" cat > "$validation_report" << EOF secrets_validation: timestamp: "$(date -Iseconds)" docker_secrets: EOF # Check each secret local total_secrets=0 local valid_secrets=0 docker secret ls --format "{{.Name}}" | while read -r secret_name; do if [[ -n "$secret_name" ]]; then ((total_secrets++)) if docker secret inspect "$secret_name" >/dev/null 2>&1; then ((valid_secrets++)) echo " - name: \"$secret_name\"" >> "$validation_report" echo " status: \"valid\"" >> "$validation_report" echo " created: \"$(docker secret inspect "$secret_name" --format '{{.CreatedAt}}')\"" >> "$validation_report" else echo " - name: \"$secret_name\"" >> "$validation_report" echo " status: \"invalid\"" >> "$validation_report" fi fi done # Add summary cat >> "$validation_report" << EOF summary: total_secrets: $total_secrets valid_secrets: $valid_secrets validation_passed: $([ $total_secrets -eq $valid_secrets ] && echo "true" || echo "false") EOF log "✅ Secrets validation completed: $validation_report" if [[ $total_secrets -eq $valid_secrets ]]; then log "🎉 All secrets validated successfully" else log "❌ Some secrets failed validation" return 1 fi } # Create secrets rotation script create_rotation_script() { log "Creating secrets rotation automation..." cat > "$PROJECT_ROOT/scripts/rotate-secrets.sh" << 'EOF' #!/bin/bash # Automated secrets rotation script set -euo pipefail LOG_FILE="/var/log/secrets-rotation-$(date +%Y%m%d).log" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" } generate_password() { openssl rand -base64 32 | tr -d "=+/" | cut -c1-32 } rotate_secret() { local secret_name="$1" local new_value="$2" log "Rotating secret: $secret_name" # Remove old secret if docker secret inspect "$secret_name" >/dev/null 2>&1; then # Get services using this secret local services services=$(docker service ls --format "{{.Name}}" | xargs -I {} docker service inspect {} --format '{{.Spec.TaskTemplate.ContainerSpec.Secrets}}' | grep -l "$secret_name" | wc -l || echo "0") if [[ $services -gt 0 ]]; then log "Warning: $services services are using $secret_name" log "Manual intervention required for rotation" return 1 fi docker secret rm "$secret_name" sleep 2 fi # Create new secret echo "$new_value" | docker secret create "$secret_name" - log "✅ Secret $secret_name rotated successfully" } # Rotate non-critical secrets (quarterly) rotate_secret "grafana_admin_password" "$(generate_password)" rotate_secret "traefik_dashboard_password" "$(htpasswd -nbB admin $(generate_password 16) | cut -d: -f2)" log "✅ Secrets rotation completed" EOF chmod +x "$PROJECT_ROOT/scripts/rotate-secrets.sh" # Schedule quarterly rotation (first day of quarter at 3 AM) local rotation_cron="0 3 1 1,4,7,10 * $PROJECT_ROOT/scripts/rotate-secrets.sh" if ! crontab -l 2>/dev/null | grep -q "rotate-secrets.sh"; then (crontab -l 2>/dev/null; echo "$rotation_cron") | crontab - log "✅ Quarterly secrets rotation scheduled" fi } # Generate comprehensive documentation generate_documentation() { log "Generating secrets management documentation..." local docs_file="$SECRETS_DIR/SECRETS_MANAGEMENT.md" cat > "$docs_file" << 'EOF' # Secrets Management Documentation ## Overview This document describes the comprehensive secrets management implementation for the HomeAudit infrastructure using Docker Secrets. ## Architecture - **Docker Secrets**: Encrypted storage and distribution of sensitive data - **File-based secrets**: Environment variables read from files in `/run/secrets/` - **Automated rotation**: Quarterly rotation of non-critical secrets - **Validation**: Regular integrity checks of secrets configuration ## Secrets Inventory ### Database Secrets - `pg_root_password`: PostgreSQL root password - `mariadb_root_password`: MariaDB root password - `redis_password`: Redis authentication password ### Application Secrets - `nextcloud_db_password`: Nextcloud database password - `nextcloud_admin_password`: Nextcloud admin user password - `immich_db_password`: Immich database password - `paperless_secret_key`: Paperless-NGX secret key - `vaultwarden_admin_token`: Vaultwarden admin access token - `grafana_admin_password`: Grafana admin password ### API Tokens - `ha_api_token`: Home Assistant API token - `jellyfin_api_key`: Jellyfin API key - `gitea_secret_key`: Gitea secret key ### TLS Certificates - `tls_certificate`: TLS certificate for HTTPS - `tls_private_key`: TLS private key ## Usage in Stack Files ### Environment Variables ```yaml environment: - POSTGRES_PASSWORD_FILE=/run/secrets/pg_root_password - MYSQL_PASSWORD_FILE=/run/secrets/nextcloud_db_password ``` ### Secrets Section ```yaml secrets: - pg_root_password - nextcloud_db_password # At the bottom of the stack file secrets: pg_root_password: external: true nextcloud_db_password: external: true ``` ## Management Commands ### Create Secret ```bash echo "my-secret-value" | docker secret create my_secret_name - ``` ### List Secrets ```bash docker secret ls ``` ### Inspect Secret (metadata only) ```bash docker secret inspect my_secret_name ``` ### Remove Secret ```bash docker secret rm my_secret_name ``` ## Rotation Process 1. Identify services using the secret 2. Plan maintenance window if needed 3. Generate new secret value 4. Remove old secret 5. Create new secret with same name 6. Update services if required (usually automatic) ## Security Best Practices 1. **Never log secret values** 2. **Use Docker Secrets for all sensitive data** 3. **Rotate secrets regularly** 4. **Monitor secret access** 5. **Use strong, unique passwords** 6. **Backup secret metadata (not values)** ## Troubleshooting ### Secret Not Found - Check if secret exists: `docker secret ls` - Verify secret name matches stack file - Ensure secret is marked as external ### Permission Denied - Check if service has access to secret - Verify secret is listed in service's secrets section - Check Docker Swarm permissions ### Service Won't Start - Check logs: `docker service logs ` - Verify secret file path is correct - Test secret access in container ## Backup and Recovery - **Metadata backup**: Export secret names and creation dates - **Values backup**: Store encrypted copies of secret values securely - **Recovery**: Recreate secrets from encrypted backup values ## Monitoring and Alerts - Monitor secret creation/deletion - Alert on failed secret access - Track secret rotation schedule - Validate secret integrity regularly EOF log "✅ Documentation created: $docs_file" } # Main execution main() { case "${1:-complete}" in "--collect") collect_existing_secrets ;; "--generate") generate_docker_secrets create_secrets_mapping ;; "--update-stacks") update_stacks_with_secrets ;; "--validate") validate_secrets ;; "--rotate") create_rotation_script ;; "--complete"|"") log "Starting complete secrets management implementation..." collect_existing_secrets generate_docker_secrets create_secrets_mapping update_stacks_with_secrets validate_secrets create_rotation_script generate_documentation log "🎉 Complete secrets management implementation finished!" ;; "--help"|"-h") cat << 'EOF' Complete Secrets Management Implementation USAGE: complete-secrets-management.sh [OPTIONS] OPTIONS: --collect Collect existing secrets from running containers --generate Generate all required Docker secrets --update-stacks Update stack files to use Docker secrets --validate Validate secrets configuration --rotate Set up secrets rotation automation --complete Run complete implementation (default) --help, -h Show this help message EXAMPLES: # Complete implementation ./complete-secrets-management.sh # Just generate secrets ./complete-secrets-management.sh --generate # Validate current configuration ./complete-secrets-management.sh --validate NOTES: - Requires Docker Swarm mode - Creates backups before modifying files - All secrets are encrypted at rest - Documentation generated automatically EOF ;; *) log "❌ Unknown option: $1" log "Use --help for usage information" exit 1 ;; esac } # Execute main function main "$@"