/** * Fail-Secure-Startprüfung der sicherheitskritischen Umgebungsvariablen (F-01, Teil 3). * * WARUM LAZY UND MEMOISIERT — bitte NICHT zu einem Import-Zeit-`throw` "aufräumen": * Zur Docker-Build-Zeit lädt `next build` alle Module, aber es sind KEINE echten * Secrets gesetzt — `DATABASE_URL` ist in der builder-Stage nur ein Platzhalter * (siehe Dockerfile) und `AUTH_SECRET` ist überhaupt nicht gesetzt. Ein `throw` auf * Modulebene würde daher jeden Build zerlegen. Deshalb prüft `assertSecureEnv()` * erst beim ersten TATSÄCHLICHEN Auth-Zugriff (aus `requireSession()`, * `requirePlatformSession()` und den beiden `authorize()`-Callbacks heraus) und * merkt sich das Ergebnis, statt bei jedem Aufruf neu zu prüfen. * * Ziel: Fehlende oder zu kurze Secrets führen zu einem harten Abbruch der * Auth-Verarbeitung (fail secure) statt zu einem stillen Weiterlaufen, bei dem * Auth.js bei Fehlkonfiguration ein Session-Objekt mit Fehlerzustand liefern * könnte (fail open, CWE-636). */ // Mindestlänge für Secrets (Auth.js empfiehlt >= 32 zufällige Zeichen; 16 ist die // harte Untergrenze, unter der wir den Start als Fehlkonfiguration werten). const MIN_SECRET_LENGTH = 16; let verified = false; /** * Prüft einmalig (memoisiert), dass die sicherheitskritischen Umgebungsvariablen * gesetzt und plausibel sind. Wirft bei Fehlkonfiguration, sonst still. */ export function assertSecureEnv(): void { if (verified) return; // Auth.js v5 nutzt AUTH_SECRET; NEXTAUTH_SECRET wird als Fallback akzeptiert, // damit bestehende Deployments nicht fälschlich als unsicher gewertet werden. const authSecret = process.env.AUTH_SECRET ?? process.env.NEXTAUTH_SECRET; if (!authSecret || authSecret.length < MIN_SECRET_LENGTH) { throw new Error( `Startabbruch: AUTH_SECRET fehlt oder ist zu kurz (mind. ${MIN_SECRET_LENGTH} Zeichen). ` + "Fail-Secure statt Fail-Open — Auth-Zugriff wird abgebrochen.", ); } const databaseUrl = process.env.DATABASE_URL; if (!databaseUrl || databaseUrl.length < MIN_SECRET_LENGTH) { throw new Error("Startabbruch: DATABASE_URL fehlt oder ist ungültig."); } // Härtung §1: PASSWORD_PEPPER ist Pflicht (32-Byte hex = 64 Hexzeichen). Er wird // in jeden Argon2-Hash/-Verify gemischt (src/server/password.ts) und ist NICHT aus // AUTH_SECRET ableitbar (eigenständiges, nicht rotierbares Umgebungs-Secret). const pepper = process.env.PASSWORD_PEPPER ?? ""; if (!/^[0-9a-fA-F]{64}$/.test(pepper)) { throw new Error( "Startabbruch: PASSWORD_PEPPER fehlt oder ist kein 32-Byte-Hex (64 Hexzeichen). " + "Erzeuge ein pro-Umgebung eindeutiges Secret: openssl rand -hex 32. Fail-Secure.", ); } verified = true; }