Harden authentication, tenant isolation and security boundaries

This commit is contained in:
t.indorf
2026-10-09 22:49:55 +02:00
parent ec3f4f933d
commit 656ada80bd
71 changed files with 3107 additions and 1200 deletions
+52 -39
View File
@@ -1,55 +1,68 @@
# Kita-Planer Deployment
# Sicherer Betrieb und Rollout
Dieses Verzeichnis enthaelt die ausfuellbaren Vorlagen fuer das Production-Deployment.
Die Anwendung startet ohne Schemaänderungen und ohne Demo-Seeding. Datenbankmigrationen laufen separat mit einer administrativen Rolle. Der App-Container verwendet ausschließlich `kita_runtime`, läuft als UID/GID 1001 und hat ein schreibgeschütztes Dateisystem mit persistenten Upload- und Cache-Volumes.
## Dateien
## Konfiguration
- `production.env.example`: Vorlage fuer alle benoetigten Environment Variables.
- `production-checklist.md`: Checkliste fuer Domain, Datenbank, Auth, Mail, Uploads und Rechtliches.
- `../Dockerfile`: Container-Build fuer Coolify oder andere Docker-Hosts.
- `../docker-compose.coolify.yml`: Compose-Vorlage fuer Coolify mit required Environment Variables.
Werte aus `production.env.example` als Hosting-Secrets hinterlegen. `APP_DATABASE_URL` enthält die eingeschränkte Rolle; `MIGRATION_DATABASE_URL` den Datenbankeigentümer. Für lokale CLI-Befehle heißt die jeweils benötigte URL `DATABASE_URL`. `APP_DATABASE_PASSWORD` ist das separate Runtime-Passwort (24–256 Zeichen); in URLs muss es URL-kodiert werden. Keine echten Secrets im Repository speichern.
## Empfohlener Ablauf mit Coolify und Gitea
`AUTH_URL`, `NEXTAUTH_URL` und `NEXT_PUBLIC_SITE_URL` müssen dieselbe kanonische HTTPS-Origin verwenden. Der Reverse Proxy terminiert TLS, routet intern auf App-Port 3000 und überschreibt weitergeleitete Host-/IP-Header. `TRUSTED_CLIENT_IP_HEADER` nur setzen, wenn der Proxy den gewählten Header (`x-real-ip` oder `cf-connecting-ip`) zuverlässig überschreibt. Ohne diese Einstellung gelten weiterhin gemeinsame globale und identitätsbezogene Datenbanklimits. Proxyseitig Body-Limit von höchstens 26 MiB, Timeouts und zusätzliche Request-Limits setzen.
1. Repository in deine Gitea-Instanz pushen.
2. In Coolify eine neue Application aus diesem Gitea-Repository anlegen.
3. Als Build Pack `Docker Compose` waehlen.
4. Als Compose-Datei `docker-compose.coolify.yml` eintragen.
5. Die bestehende Coolify-Postgres-Datenbank ueber ihre interne `DATABASE_URL` verbinden.
6. Environment Variables aus `production.env.example` in Coolify eintragen.
7. Domain auf den `app`-Service mit internem Port `3000` zeigen lassen.
8. Deploy starten.
Compose startet PostgreSQL standardmäßig intern. Bei einer bereits extern verwalteten Coolify-Datenbank den Stack entsprechend anpassen und deren interne URL verwenden; niemals versehentlich einen neuen leeren Datenbankdienst als Ersatz anschließen. pgAdmin ist nur im Profil `maintenance` und nur auf Loopback verfügbar; Zugriff über SSH-Tunnel.
Beim Container-Start fuehrt `scripts/start-production.sh` automatisch aus:
## Bestehende Installation aktualisieren
```bash
npx prisma db push
1. Datenbank **und** Upload-Volume sichern und Wiederherstellung überprüfen. Wartungsfenster ankündigen und alte App stoppen: das neue Schema und der neue Code gehören zusammen.
2. Den Zustand der bestehenden Datenbank mit `prisma/migrations/20261009000000_baseline/migration.sql` vergleichen. Die Baseline bildet das bisherige Schema ab. Bei Abweichungen zuerst die Unterschiede gezielt klären; eine Baseline-Markierung prüft das Schema nicht automatisch.
3. `scripts/check-tenant-integrity.sql` mit `psql "$MIGRATION_DATABASE_URL" -v ON_ERROR_STOP=1 -f scripts/check-tenant-integrity.sql` ausführen. **Keine Ergebniszeilen** bedeutet, dass keine der geprüften widersprüchlichen Tenant-Verknüpfungen vorliegt. Ergebnisse vor der Migration fachlich korrigieren; keine Daten automatisch einem anderen Mandanten zuweisen.
4. **Nur für eine bestehende, zur Baseline passende Datenbank** einmal die Baseline als bereits angewendet markieren. Bei einer leeren neuen Datenbank diesen Schritt auslassen.
5. Alle weiteren Migrationen anwenden, Runtime-Rolle bereitstellen und Größenmetadaten vorhandener Anhänge nachtragen.
6. Eigentümer bestehender Upload-/Cache-Volumes auf UID/GID 1001 vorbereiten, dann die neue App starten. Stichproben mit zwei verschiedenen Kitas und den echten E-Mail-Flows prüfen.
Compose-Beispiele (Environment-Secrets vorher bereitstellen):
```sh
docker compose -f docker-compose.coolify.yml build app migrate
# NUR bei bestehender, zuvor geprüfter Baseline-Datenbank:
docker compose -f docker-compose.coolify.yml --profile maintenance run --rm migrate npx prisma migrate resolve --applied 20261009000000_baseline
# Bei neuen und bestehenden Datenbanken:
docker compose -f docker-compose.coolify.yml --profile maintenance run --rm migrate npm run db:migrate:deploy
docker compose -f docker-compose.coolify.yml --profile maintenance run --rm migrate npm run db:provision-runtime
docker compose -f docker-compose.coolify.yml --profile maintenance run --rm migrate npm run uploads:backfill
# Nur auf dem vorgesehenen App-Upload-Volume:
docker compose -f docker-compose.coolify.yml --profile maintenance run --rm migrate chown -R 1001:1001 /app/uploads
# Falls ein bereits vorhandenes Cache-Volume root gehört: dessen Eigentümer separat korrigieren.
docker compose -f docker-compose.coolify.yml up -d app
```
Coolify erkennt Variablen aus `docker-compose.coolify.yml` und zeigt required Variablen
wie `DATABASE_URL`, `AUTH_SECRET`, `AUTH_URL`, `NEXTAUTH_URL`, `NEXT_PUBLIC_SITE_URL`,
`RESEND_API_KEY`, `EMAIL_FROM` und `ADMIN_EMAIL` in der UI an.
Bei einem Rollback nach Migration zuerst Kompatibilität prüfen; das alte Image allein zurückzusetzen reicht wegen neuer Pflichtfelder nicht aus. Im Zweifel das geprüfte Backup von Datenbank und Dateien gemeinsam wiederherstellen.
## Welche Coolify-Postgres-URL?
## Auswirkungen auf bestehende Konten
Wenn die App und die Postgres-DB im selben Coolify-Projekt/Netzwerk laufen, nimm
die interne Datenbank-URL aus Coolify, nicht die oeffentliche externe URL. Das ist
stabiler und vermeidet unnoetigen Traffic ueber den Host.
Alle JWT-Sitzungen vor diesem Release werden ungültig; erneute Anmeldung ist erforderlich. Administrative Sperren entwerten auch bei späterer Entsperrung dauerhaft die alten Sitzungen. Gründerkonten ohne Kita und ohne echte E-Mail-Prüfung registrieren sich mit derselben Adresse erneut und bestätigen den Link. Bereits bestehende Kita-Gründer behalten ihre bisherige Verifizierung; deren historische Adressen werden dadurch nicht nachträglich als echte Mailbox-Bestätigung nachgewiesen.
Das Format bleibt:
Neue Registrierungen bestätigen zuerst die E-Mail-Adresse, wählen anschließend ihr Passwort und erstellen erst danach eine Kita. Beim Ändern einer Elternadresse werden Passwort und bisherige Sitzungen entwertet; die neue Adresse erhält eine Einladung zum Festlegen eines neuen Passworts. Kita-Admins können die Einladung erneut senden. Neue Registrierungslinks gelten 30 Minuten, Einladungen sieben Tage und Notdienst-Bestätigungslinks höchstens 24 Stunden sowie nur für offene, noch relevante Einsätze. Bestehende UUID-Links bleiben innerhalb ihrer Laufzeit kompatibel. Zwei-Faktor-Anmeldung ist auf ausdrücklichen Wunsch vorerst nicht eingeführt.
```txt
postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public
## Dateien und Wartung
`UPLOAD_DIR` bezeichnet die bisherige Basis; die Anwendung ergänzt `announcements`. Compose verwendet unverändert `/app/uploads/announcements/announcements`. Backfill muss genau dieselben Dateien sehen. Fehlende Dateien werden mit Größe 0 erfasst und als Anzahl gemeldet: Ursache und Datenverlust prüfen. Solange Größenwerte `NULL` sind, blockiert die Anwendung neue Anhänge, damit das Gesamtlimit nicht umgangen werden kann.
Grenzen: 10 MiB pro Datei, fünf Dateien und 25 MiB pro Nachricht; standardmäßig 256 MiB pro Kita und 10 GiB insgesamt, konfigurierbar über die beiden Quota-Variablen. Erlaubt sind PDF, PNG und JPEG mit passenden Dateisignaturen und Endungen. Das ist keine Virenprüfung oder vollständige PDF-Bereinigung. Downloads erfordern Sitzung, Consent und die passende Kita; sie werden als Anhang ausgeliefert.
`npm run uploads:cleanup` zeigt zunächst nur verwaiste Dateien mit mindestens 24 Stunden Alter. Nach Prüfung entfernt `npm run uploads:cleanup -- --delete` diese Dateien. Backups, Speicherüberwachung und eine Aufbewahrungs-/Löschstrategie für Backups separat einrichten. Dateien gelöschter Datensätze werden auch unmittelbar bestmöglich entfernt.
Die SQL-Migrationen ergänzen **31 zusammengesetzte Tenant-Fremdschlüssel**, die Prisma wegen der vorhandenen optionalen Cascade-/SetNull-Beziehungen nicht vollständig abbildet. Zukünftige generierte Migrationen auf unbeabsichtigtes Entfernen dieser Constraints prüfen. Kein `prisma db push` auf verwalteten Datenbanken einsetzen. Runtime-Grants nach zukünftigen Migrationen erneut bereitstellen.
## Verifikation und Updates
```sh
npm ci
npm run build:prod
npx tsc --noEmit
npm run lint -- --quiet
npm audit --omit=dev
```
## Wichtige Hinweise
Die Integrationstests setzen eine ausdrücklich isolierte lokale PostgreSQL-Datenbank namens `kita_security_test` voraus; `SECURITY_TEST_DATABASE_URL` darf keine Anwendungsdaten enthalten. `npm run test:security` prüft Aktionen und echte DB-Transaktionen. `npm run test:security:http` benötigt den Production-Server auf localhost:3057 mit derselben Testdatenbank, `UPLOAD_DIR` und dem im Test dokumentierten Test-Secret. Niemals dieses Test-Secret in Produktion verwenden.
- Das aktuelle Projekt nutzt noch `prisma db push` statt versionierter Prisma-Migrationen.
Fuer den ersten MVP-Deploy ist das praktikabel, fuer laengerfristigen Betrieb sollten
Migrationen eingefuehrt werden.
- `UPLOAD_DIR` muss persistent sein. Die Coolify-Compose-Datei mountet dafuer ein
Docker-Volume nach `/app/uploads`.
- `NEXT_PUBLIC_SITE_URL` muss die echte Domain enthalten, damit OpenGraph-Bilder in
WhatsApp, LinkedIn und Co. korrekt als absolute URL aufgeloest werden.
- `NEXT_PUBLIC_SITE_URL` wird auch als Build-Arg an Docker uebergeben. In Coolify sollte
diese Variable deshalb fuer Build und Runtime verfuegbar sein.
Docker-Images sind auf Digests fixiert. Neue Sicherheitsreleases regelmäßig prüfen und die Digests bewusst aktualisieren. Zum Prüfzeitpunkt verbleibt eine ungepatchte `braces`-Schwachstelle in der Entwicklungs-Lint-Kette; `npm audit --force` löst sie nicht sinnvoll. Der Runtime-Audit ist ohne bekannte Findings. NextAuth bleibt eine Beta-Abhängigkeit und benötigt weiterhin Update-Beobachtung.
+30 -53
View File
@@ -1,60 +1,37 @@
# Production Checklist
# Production-Checkliste
## 1. Domain & Basis-URLs
## Vor dem Rollout
- [ ] Produktionsdomain festlegen: `https://[deine-domain.de]`
- [ ] `NEXT_PUBLIC_SITE_URL` auf die Produktionsdomain setzen
- [ ] `AUTH_URL` und `NEXTAUTH_URL` auf die Produktionsdomain setzen
- [ ] Nach Deployment die WhatsApp-/LinkedIn-Vorschau mit der finalen URL testen
- [ ] Datenbank und Uploads gesichert; Wiederherstellung geprüft.
- [ ] Alte App gestoppt; Wartungsfenster und erneute Anmeldung angekündigt.
- [ ] Bestehendes Schema mit Baseline verglichen; Tenant-Preflight ohne Befunde.
- [ ] Baseline nur bei bestehender passender DB markiert; anschließend alle Migrationen angewendet.
- [ ] Runtime-Rolle mit separatem Passwort bereitgestellt; App verwendet `APP_DATABASE_URL`.
- [ ] Upload-Größen nachgetragen; gemeldete fehlende Dateien geprüft.
- [ ] Upload- und Cache-Volumes für UID/GID 1001 beschreibbar; Upload-Pfad unverändert.
## 2. Datenbank
## Hosting und Konfiguration
- [ ] Interne `DATABASE_URL` der bestehenden Coolify-Postgres-DB kopieren
- [ ] `DATABASE_URL` in der Coolify-App eintragen
- [ ] Beim ersten App-Start wird das Schema automatisch mit `npx prisma db push --skip-generate` angewendet
- [ ] Fuer spaetere Releases idealerweise Prisma-Migrationen einfuehren
- [ ] Drei öffentliche Basis-URLs auf dieselbe kanonische HTTPS-Origin gesetzt.
- [ ] Starkes `AUTH_SECRET`; echte Secrets nur in Hosting-Konfiguration.
- [ ] TLS und Proxy-Header korrekt; kein öffentlicher DB-/App-/pgAdmin-Port.
- [ ] Request-Limits, Timeouts und Upload-Body-Limit am Proxy eingerichtet.
- [ ] Trusted-IP-Header nur bei garantierter Überschreibung aktiviert.
- [ ] Maildomain bei Resend verifiziert; Produktionsabsender gesetzt.
- [ ] Optionale Analytics nur auf öffentlichen Marketingseiten; keine Token-/Login-/Dashboard-Daten.
- [ ] Upload-Speicher und Backups überwacht; Löschfristen festgelegt.
- [ ] Impressum und Datenschutztexte an tatsächliche Betreiber/Anbieter angepasst.
## 3. Auth & Sicherheit
## Nach dem Rollout
- [ ] `AUTH_SECRET` neu generieren: `openssl rand -base64 32`
- [ ] Keine `.env`-Dateien mit echten Secrets committen
- [ ] Dashboard ist per Metadata auf `noindex,nofollow` gesetzt
- [ ] Produktionsdomain auf HTTPS betreiben
- [ ] Anmeldung und Registrierung mit Mailbox-Bestätigung getestet.
- [ ] Einladungen, Adressänderung und erneutes Senden getestet.
- [ ] Administrative Sperre entwertet bestehende Sitzung; Entsperrung verlangt neue Anmeldung.
- [ ] Zwei getrennte Kitas: Anhänge, Kalender und Mutationen bleiben isoliert.
- [ ] Anhänge heruntergeladen; Quotas und persistentes Volume geprüft.
- [ ] Privates Dashboard ohne Analytics und ohne Cache; CSP ohne Browserfehler.
- [ ] Kontakt-/Notdienst-Mailfluss und Fehlerbehandlung geprüft.
- [ ] Build, Lint, Typprüfung, Sicherheitstests und Runtime-Audit erfolgreich.
- [ ] Offenes Entwicklungs-Advisory und fixierte Image-Digests weiter beobachten.
## 4. Mail
- [ ] Eigene Versanddomain bei Resend verifizieren
- [ ] `RESEND_API_KEY` fuer Produktion setzen
- [ ] `EMAIL_FROM` auf eine verifizierte Absenderadresse setzen
- [ ] `ADMIN_EMAIL` fuer Kontaktformular setzen
- [ ] Kontaktformular und Einladung/E-Mail-Flows einmal in Produktion testen
## 5. Uploads
- [ ] Coolify-Volume `kita_planer_uploads` fuer `/app/uploads` verwenden
- [ ] Sicherstellen, dass Uploads nicht aus Versehen oeffentlich ausgeliefert werden
- [ ] Backup-Strategie fuer Uploads klaeren
## 6. Rechtliches
- [ ] Platzhalter im Impressum ersetzen
- [ ] Platzhalter in der Datenschutzerklaerung ersetzen
- [ ] Hosting-Anbieter und Mail-Anbieter in Datenschutztext eintragen
- [ ] Rechtliche Texte vor Livegang pruefen lassen
## 7. Build & Smoke Test
- [ ] Dependencies installieren: `npm ci`
- [ ] Production Build erstellen: `npm run build:prod`
- [ ] App starten: `npm run start:prod`
- [ ] Startseite, Login, Dashboard und Kontaktformular testen
## 8. Coolify
- [ ] Code nach Gitea pushen
- [ ] In Coolify eine Application aus dem Gitea-Repository anlegen
- [ ] Build Pack `Docker Compose` waehlen
- [ ] Compose-Datei `docker-compose.coolify.yml` verwenden
- [ ] Environment Variables in Coolify eintragen, nicht ins Repo committen
- [ ] Domain auf den `app`-Service und Container-Port `3000` routen
- [ ] Upload-Volume `kita_planer_uploads` bleibt zwischen Deployments erhalten
Details und Befehle stehen in [README.md](README.md). Lokale Tests ersetzen nicht die Kontrolle der produktiven TLS-, Proxy-, Backup-, Mail- und Container-Konfiguration.
+16 -2
View File
@@ -11,6 +11,11 @@ POSTGRES_DB="kita_planer"
POSTGRES_USER="kita_planer"
POSTGRES_PASSWORD="[GENERATE_ME]"
# Unterschiedliche DB-Rollen für Runtime und Migration. URL-Passwörter müssen URL-kodiert sein.
APP_DATABASE_PASSWORD="[GENERATE_ME]"
APP_DATABASE_URL="postgresql://kita_runtime:[URL_ENCODED_APP_PASSWORD]@postgres:5432/kita_planer?schema=public"
MIGRATION_DATABASE_URL="postgresql://kita_planer:[URL_ENCODED_OWNER_PASSWORD]@postgres:5432/kita_planer?schema=public"
# pgAdmin im Coolify-Compose-Stack
PGADMIN_DEFAULT_EMAIL="[admin@deine-domain.de]"
PGADMIN_DEFAULT_PASSWORD="[GENERATE_ME]"
@@ -40,5 +45,14 @@ ADMIN_EMAIL="[kontakt@deine-domain.de]"
# Muss auf dem Server dauerhaft erhalten bleiben und darf nicht im Repo liegen.
UPLOAD_DIR="/var/lib/kita-planer/uploads/announcements"
# Optional: Nur bewusst fuer initiales Demo-Seeding in Produktion setzen.
# ALLOW_DATABASE_SEED="true"
# Standard: 256 MiB je Kita, 10 GiB insgesamt. Compose verwendet diese Werte.
UPLOAD_TENANT_QUOTA_BYTES="268435456"
UPLOAD_GLOBAL_QUOTA_BYTES="10737418240"
# Compose setzt UPLOAD_DIR fest auf /app/uploads/announcements.
# Die Anwendung ergänzt /announcements: bestehende Dateien liegen dort unter
# /app/uploads/announcements/announcements. Diesen Pfad beim Backfill beibehalten.
# Demo-Seeding ist in Produktion deaktiviert.
# Nur hinter einem Proxy setzen, der diesen Header überschreibt:
# TRUSTED_CLIENT_IP_HEADER="x-real-ip"