""" Zentrale Konfiguration der Jumphost-Anwendung. Secrets (KEK, Session-Signaturschluessel) werden bevorzugt ueber systemd Credentials geladen (siehe systemd LoadCredentialEncrypted= im Unit-File, $CREDENTIALS_DIRECTORY zur Laufzeit). Fuer lokale Entwicklung/Tests wird auf Umgebungsvariablen bzw. eine lokale .env-Datei zurueckgefallen -- das ist NICHT fuer den Produktivbetrieb gedacht. """ from __future__ import annotations import os import secrets from dataclasses import dataclass, field from pathlib import Path def _read_credential(name: str, env_fallback: str | None = None, *, required: bool = True) -> bytes | None: """Liest ein Secret aus $CREDENTIALS_DIRECTORY (systemd-creds) oder Fallback-Env. Der Wert wird als Hex-String erwartet (z.B. per `openssl rand -hex 32` erzeugt) und hier zu den zugrundeliegenden Rohbytes dekodiert -- passend zum selben Format, das auch fuer JUMPHOST_DEV_KEK/JUMPHOST_DEV_SESSION_SECRET im development-Zweig unten verwendet wird (s. Konzept 6.4). Vorher wurde dieser Wert hier NICHT dekodiert, sondern roh als Bytes interpretiert -- das fuehrte bei einem korrekt per `openssl rand -hex 32` erzeugten 64-Zeichen-Hex-String (= 32 Byte Entropie) zu einer 64-Byte-Rohlaenge und damit zum Fehler "KEK muss genau 32 Bytes lang sein". """ cred_dir = os.environ.get("CREDENTIALS_DIRECTORY") raw: bytes | None = None if cred_dir: cred_path = Path(cred_dir) / name if cred_path.exists(): raw = cred_path.read_bytes().strip() if raw is None and env_fallback and env_fallback in os.environ: raw = os.environ[env_fallback].strip().encode() if raw is None: if required: raise RuntimeError( f"Secret '{name}' weder ueber systemd-creds noch ueber Env-Variable " f"'{env_fallback}' verfuegbar. In Produktion MUSS dies ueber " f"systemd LoadCredentialEncrypted= bereitgestellt werden." ) return None try: return bytes.fromhex(raw.decode("ascii")) except (ValueError, UnicodeDecodeError) as exc: raise RuntimeError( f"Secret '{name}' ist kein gueltiger Hex-String (erwartet z.B. das " f"Ergebnis von `openssl rand -hex 32`)." ) from exc @dataclass class Settings: app_env: str = os.environ.get("JUMPHOST_ENV", "development") data_dir: Path = Path(os.environ.get("JUMPHOST_DATA_DIR", "/var/lib/jumphost")) db_path: Path = field(init=False) recordings_dir: Path = field(init=False) # Key-Encryption-Key fuer AES-256-GCM (verschluesselt SSH-Keys/TOTP-Secrets in der DB) kek: bytes = field(init=False) # separater Schluessel fuer Session-Cookie-Signatur (Schluesseltrennung, siehe Konzept 6.2) session_secret: bytes = field(init=False) listen_uds: str = os.environ.get("JUMPHOST_LISTEN_UDS", "/run/jumphost/app.sock") guacd_host: str = os.environ.get("JUMPHOST_GUACD_HOST", "127.0.0.1") guacd_port: int = int(os.environ.get("JUMPHOST_GUACD_PORT", "4822")) session_idle_timeout_s: int = int(os.environ.get("JUMPHOST_SESSION_IDLE_TIMEOUT", "900")) session_absolute_timeout_s: int = int(os.environ.get("JUMPHOST_SESSION_ABS_TIMEOUT", "28800")) max_failed_logins: int = int(os.environ.get("JUMPHOST_MAX_FAILED_LOGINS", "5")) lockout_base_seconds: int = int(os.environ.get("JUMPHOST_LOCKOUT_BASE_SECONDS", "30")) def __post_init__(self) -> None: self.db_path = self.data_dir / "jumphost.db" self.recordings_dir = self.data_dir / "recordings" if self.app_env == "development": # Nur fuer lokale Entwicklung: deterministisch aus Env oder Zufallswert je Prozessstart. kek_hex = os.environ.get("JUMPHOST_DEV_KEK") self.kek = bytes.fromhex(kek_hex) if kek_hex else secrets.token_bytes(32) sess_hex = os.environ.get("JUMPHOST_DEV_SESSION_SECRET") self.session_secret = bytes.fromhex(sess_hex) if sess_hex else secrets.token_bytes(32) else: self.kek = _read_credential("jumphost_kek", "JUMPHOST_KEK") self.session_secret = _read_credential("jumphost_session_secret", "JUMPHOST_SESSION_SECRET") if len(self.kek) != 32: raise RuntimeError("KEK muss genau 32 Bytes (256 Bit) lang sein.") if len(self.session_secret) != 32: raise RuntimeError("Session-Secret muss genau 32 Bytes (256 Bit) lang sein.") settings = Settings()