From 658c9d01b6638cd8cbcc2ed12de03a9f256b444c Mon Sep 17 00:00:00 2001 From: Joachim Hummel Date: Tue, 27 Jan 2026 15:19:39 +0000 Subject: [PATCH] added keygen script --- keygen.sh | 353 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100755 keygen.sh diff --git a/keygen.sh b/keygen.sh new file mode 100755 index 0000000..bcb91b5 --- /dev/null +++ b/keygen.sh @@ -0,0 +1,353 @@ +#!/bin/bash +# keygen.sh - Production Environment Generator +# Generates cryptographically secure secrets and configures .env files +# +# Usage: +# ./keygen.sh # Interactive mode +# ./keygen.sh --dry-run # Show what would be generated +# ./keygen.sh --show-secrets # Show generated secrets (debug only) + +set -e # Exit on error +set -u # Exit on undefined variable +set -o pipefail # Exit on pipe failure + +# Cleanup on exit +cleanup() { + if [ -f .env.tmp ]; then + rm -f .env.tmp + fi +} +trap cleanup EXIT + +# Script version +readonly VERSION="1.0.0" + +# Parse command line arguments +DRY_RUN=false +SHOW_SECRETS=false + +for arg in "$@"; do + case $arg in + --dry-run) + DRY_RUN=true + ;; + --show-secrets) + SHOW_SECRETS=true + ;; + --help|-h) + echo "keygen.sh v${VERSION} - Production Environment Generator" + echo "" + echo "Usage:" + echo " ./keygen.sh Interactive mode" + echo " ./keygen.sh --dry-run Show what would be generated" + echo " ./keygen.sh --show-secrets Show secrets (debug only)" + echo "" + exit 0 + ;; + *) + echo "Unknown option: $arg" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Preflight checks +check_dependencies() { + echo "Prüfe Abhängigkeiten..." + + # Check for openssl + if ! command -v openssl &> /dev/null; then + echo -e "${RED}❌ Fehler: openssl ist nicht installiert${NC}" + echo " Debian/Ubuntu: apt-get install openssl" + echo " Alpine: apk add openssl" + exit 1 + fi + echo -e "${GREEN}✓${NC} openssl verfügbar" +} + +check_env_example() { + if [ ! -f .env.example ]; then + echo -e "${RED}❌ Fehler: .env.example nicht gefunden${NC}" + echo " Script muss im Projekt-Root ausgeführt werden" + echo " Aktuelles Verzeichnis: $(pwd)" + exit 1 + fi + echo -e "${GREEN}✓${NC} .env.example gefunden" +} + +check_write_permissions() { + if [ ! -w . ]; then + echo -e "${RED}❌ Fehler: Keine Schreibrechte im aktuellen Verzeichnis${NC}" + echo " Verzeichnis: $(pwd)" + exit 1 + fi + echo -e "${GREEN}✓${NC} Schreibrechte vorhanden" +} + +# Run all preflight checks +run_preflight_checks() { + echo "=== Preflight Checks ===" + check_dependencies + check_env_example + check_write_permissions + echo "" +} + +# Secret generation functions +generate_password() { + # Generate 32-character password from base64, removing confusing characters + openssl rand -base64 48 | tr -d "=+/" | cut -c1-32 +} + +generate_jwt_secret() { + # Generate JWT secret with maximum entropy + openssl rand -base64 48 +} + +generate_encryption_key() { + # Generate exactly 32 characters for AES-256-GCM + local key=$(openssl rand -base64 32 | cut -c1-32) + + # Critical validation: MUST be exactly 32 characters + if [ ${#key} -ne 32 ]; then + echo -e "${RED}❌ KRITISCH: Encryption Key hat ${#key} Zeichen statt 32${NC}" + echo " Generierter Key: '$key'" + exit 1 + fi + + echo "$key" +} + +# Generate all secrets +generate_secrets() { + echo "Generiere Secrets..." + + POSTGRES_PASSWORD=$(generate_password) + REDIS_PASSWORD=$(generate_password) + JWT_ACCESS_SECRET=$(generate_jwt_secret) + JWT_REFRESH_SECRET=$(generate_jwt_secret) + ENCRYPTION_KEY=$(generate_encryption_key) + + # Validate all secrets were generated + if [ -z "$POSTGRES_PASSWORD" ] || [ -z "$REDIS_PASSWORD" ] || \ + [ -z "$JWT_ACCESS_SECRET" ] || [ -z "$JWT_REFRESH_SECRET" ] || \ + [ -z "$ENCRYPTION_KEY" ]; then + echo -e "${RED}❌ Fehler: Secret-Generierung fehlgeschlagen${NC}" + exit 1 + fi + + echo -e "${GREEN}✓${NC} Alle Secrets generiert" + + # Show secrets only if --show-secrets flag is set + if [ "$SHOW_SECRETS" = true ]; then + echo "" + echo "=== Generierte Secrets (DEBUG MODE) ===" + echo "POSTGRES_PASSWORD: $POSTGRES_PASSWORD (${#POSTGRES_PASSWORD} Zeichen)" + echo "REDIS_PASSWORD: $REDIS_PASSWORD (${#REDIS_PASSWORD} Zeichen)" + echo "JWT_ACCESS_SECRET: $JWT_ACCESS_SECRET (${#JWT_ACCESS_SECRET} Zeichen)" + echo "JWT_REFRESH_SECRET: $JWT_REFRESH_SECRET (${#JWT_REFRESH_SECRET} Zeichen)" + echo "ENCRYPTION_KEY: $ENCRYPTION_KEY (${#ENCRYPTION_KEY} Zeichen)" + echo "" + fi +} + +# Domain configuration +get_domain_configuration() { + echo "=== Secure Portal - Production Environment Setup ===" + echo "" + + # Get domain from user + read -p "Domain (z.B. portal.example.com): " DOMAIN + + # Validate domain is not empty + if [ -z "$DOMAIN" ]; then + echo -e "${RED}❌ Fehler: Domain ist erforderlich${NC}" + exit 1 + fi + + # Generate URLs from domain + FRONTEND_URL="https://$DOMAIN" + VITE_API_URL="https://$DOMAIN/api" + PASSWORD_RESET_URL="https://$DOMAIN/password-reset" + CORS_ORIGIN="https://$DOMAIN" + + # Show configuration + echo "" + echo "=== Konfiguration ===" + echo "Domain: $DOMAIN" + echo "Frontend URL: $FRONTEND_URL" + echo "API URL: $VITE_API_URL" + echo "" + + # Confirm configuration + read -p "Konfiguration korrekt? (ja/nein): " confirm + if [ "$confirm" != "ja" ]; then + echo "Abgebrochen." + exit 0 + fi + + echo "" +} + +# Backup management +cleanup_old_backups() { + # Keep only last 5 backups + local backups=$(ls -t .env.backup-* 2>/dev/null | tail -n +6) + if [ -n "$backups" ]; then + echo "$backups" | xargs rm -f 2>/dev/null + local count=$(echo "$backups" | wc -l) + echo -e "${GREEN}✓${NC} $count alte Backups gelöscht" + fi +} + +check_existing_env() { + if [ ! -f .env ]; then + return 0 # No .env exists, safe to proceed + fi + + echo -e "${YELLOW}⚠️ WARNUNG: .env existiert bereits!${NC}" + + # Show last modification time + if command -v stat &> /dev/null; then + if stat -c %y .env &> /dev/null 2>&1; then + # GNU stat + echo " Letzte Änderung: $(stat -c %y .env)" + else + # BSD stat (macOS) + echo " Letzte Änderung: $(stat -f %Sm .env)" + fi + fi + + # Check if it contains production data + if grep -qv "change_this\|your_" .env 2>/dev/null; then + echo -e " ${YELLOW}⚠️ PRODUKTIV-DATEN GEFUNDEN!${NC}" + fi + + # Ask for confirmation + read -p " Überschreiben? Backup wird erstellt (ja/nein): " confirm + if [ "$confirm" != "ja" ]; then + echo "Abgebrochen." + exit 0 + fi + + # Create backup with timestamp + backup=".env.backup-$(date +%Y%m%d-%H%M%S)" + cp .env "$backup" + echo -e " ${GREEN}✓${NC} Backup erstellt: $backup" + echo "" + + # Cleanup old backups + cleanup_old_backups +} + +# .env file generation +generate_env_file() { + echo "Generiere .env Datei..." + + if [ "$DRY_RUN" = true ]; then + echo "" + echo "=== DRY RUN MODE - Änderungen werden NICHT gespeichert ===" + echo "" + fi + + # Create temporary file + local tmpfile=".env.tmp" + + # Copy template + cp .env.example "$tmpfile" + + # Replace secrets (using | as delimiter for URLs) + sed -i "s|POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$POSTGRES_PASSWORD|" "$tmpfile" || { + echo -e "${RED}❌ Fehler beim Ersetzen von POSTGRES_PASSWORD${NC}" + rm "$tmpfile" + exit 1 + } + sed -i "s|REDIS_PASSWORD=.*|REDIS_PASSWORD=$REDIS_PASSWORD|" "$tmpfile" + sed -i "s|JWT_ACCESS_SECRET=.*|JWT_ACCESS_SECRET=$JWT_ACCESS_SECRET|" "$tmpfile" + sed -i "s|JWT_REFRESH_SECRET=.*|JWT_REFRESH_SECRET=$JWT_REFRESH_SECRET|" "$tmpfile" + sed -i "s|ENCRYPTION_KEY=.*|ENCRYPTION_KEY=$ENCRYPTION_KEY|" "$tmpfile" + + # Replace URLs + sed -i "s|FRONTEND_URL=.*|FRONTEND_URL=$FRONTEND_URL|" "$tmpfile" + sed -i "s|VITE_API_URL=.*|VITE_API_URL=$VITE_API_URL|" "$tmpfile" + sed -i "s|PASSWORD_RESET_URL=.*|PASSWORD_RESET_URL=$PASSWORD_RESET_URL|" "$tmpfile" + sed -i "s|CORS_ORIGIN=.*|CORS_ORIGIN=$CORS_ORIGIN|" "$tmpfile" + + # Verify all replacements succeeded + if grep -q "change_this" "$tmpfile"; then + echo -e "${YELLOW}⚠️ Warnung: Einige Platzhalter wurden nicht ersetzt${NC}" + echo " Dies ist normal für Werte die manuell konfiguriert werden müssen" + fi + + if [ "$DRY_RUN" = true ]; then + # Show what would be generated + echo "=== Generated .env content (preview) ===" + grep -E "POSTGRES_PASSWORD|REDIS_PASSWORD|JWT_ACCESS_SECRET|JWT_REFRESH_SECRET|ENCRYPTION_KEY|FRONTEND_URL|VITE_API_URL" "$tmpfile" | head -20 + echo "..." + echo "" + rm "$tmpfile" + echo "Dry run complete. No files were modified." + return 0 + fi + + # Move to final location with secure permissions + mv "$tmpfile" .env + chmod 600 .env + + echo -e "${GREEN}✓${NC} .env erstellt mit sicheren Permissions (600)" +} + +# Success output +show_success() { + echo "" + echo -e "${GREEN}✅ .env erfolgreich erstellt!${NC}" + echo "" + echo "=== Generierte Secrets ===" + echo "✓ PostgreSQL Passwort (32 Zeichen)" + echo "✓ Redis Passwort (32 Zeichen)" + echo "✓ JWT Access Secret (~64 Zeichen)" + echo "✓ JWT Refresh Secret (~64 Zeichen)" + echo "✓ Encryption Key (exakt 32 Zeichen)" + echo "" + echo "=== Konfigurierte URLs ===" + echo "✓ Frontend: $FRONTEND_URL" + echo "✓ API: $VITE_API_URL" + echo "" + echo "=== Nächste Schritte ===" + echo "1. Email-Konfiguration anpassen (optional):" + echo " nano .env" + echo "" + echo "2. Container starten:" + echo " docker compose up -d" + echo "" + + if [ -n "${backup:-}" ]; then + echo "3. Backup gespeichert: $backup" + echo "" + fi +} + +# Main workflow +main() { + run_preflight_checks + check_existing_env + get_domain_configuration + generate_secrets + generate_env_file + + if [ "$DRY_RUN" = false ]; then + show_success + fi +} + +# Main execution starts here +echo "keygen.sh v${VERSION}" +echo "" +main