84 lines
12 KiB
Markdown
84 lines
12 KiB
Markdown
# Sicherer Betrieb und Rollout
|
|
|
|
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.
|
|
|
|
## Konfiguration
|
|
|
|
Werte aus `production.env.example` als Hosting-Secrets hinterlegen. Im Compose-Stack erzeugt der Dienst `migrate` automatisch ein separates Runtime-Passwort im privaten Secret-Volume, wendet die Migrationen an, vergibt die eingeschränkten DB-Rechte und bereitet die Upload-/Cache-Volumes vor. Danach startet `app` mit UID/GID 1001. Administrative Zugangsdaten werden im App-Service ausdrücklich mit dem harmlosen Platzhalter `unavailable-in-app` überschrieben (leere Werte entfernt Coolify bei der Verarbeitung), damit eine zusätzliche globale `env_file`-Weitergabe durch das Hosting sie nicht in den App-Container einschleust. Der Start entfernt diese Variablen zusätzlich vor dem App- und Worker-Prozess. Das Eigentümerpasswort steht nur PostgreSQL und dem Initialisierungsdienst zur Verfügung. Für manuelle CLI-Befehle außerhalb von Compose bleibt `DATABASE_URL` die jeweilige administrative oder eingeschränkte URL. Keine echten Secrets im Repository speichern.
|
|
|
|
`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.
|
|
|
|
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.
|
|
|
|
## Neustart mit leerer Datenbank
|
|
|
|
Wenn vorhandene Daten ausdrücklich löschbare Testdaten sind: ausschließlich die zu dieser Installation gehörenden Datenbank- und Upload-Volumes zurücksetzen. Danach genügt ein normales Deployment mit `docker compose -f docker-compose.coolify.yml up -d --build app`. Es sind weder eine Baseline-Markierung noch manuelle Schema-Upgrades oder Demo-Seeding erforderlich. Der Initialisierungsdienst läuft vor der App; ein Fehler verhindert deren Start. Bestehende produktive Daten erfordern dagegen die unten beschriebene Vorbereitung.
|
|
|
|
Im Normalbetrieb bleiben Datenbank, Uploads, Cache und Runtime-Secret-Volume bei Deployments bestehen. Das Runtime-Passwort wird bei einem normalen Neustart wiederverwendet. Niemals die Datenbank als Startnebenwirkung löschen.
|
|
|
|
## Bestehende Installation aktualisieren
|
|
|
|
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 run --rm migrate npx prisma migrate resolve --applied 20261009000000_baseline
|
|
# Initialisierung läuft automatisch vor der App:
|
|
docker compose -f docker-compose.coolify.yml up -d app
|
|
```
|
|
|
|
Die Baseline-Markierung benötigt bei einem manuellen Aufruf eine administrative `DATABASE_URL`; für eine leere Datenbank entfällt sie vollständig. Vorhandene Uploads müssen vor dem Start kontrolliert und ihre Größen unter administrativem Zugang nachgetragen werden. Für bestehende root-eigene Unterverzeichnisse ist eine gezielte Eigentümerkorrektur nötig; die Initialisierung bereitet die Basisverzeichnisse vor.
|
|
|
|
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.
|
|
|
|
## Auswirkungen auf bestehende Konten
|
|
|
|
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.
|
|
|
|
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-Rückmeldelinks bis zum Ende des jeweiligen Einsatzes; eine gemeinsame Haushaltsantwort kann nur einmal gespeichert werden. Sperren entwerten vorhandene Rückmeldelinks dauerhaft. Die öffentlichen Token-Antworten zeigen nur die benötigten Einsatzangaben. Bestehende UUID-Links bleiben innerhalb ihrer Laufzeit kompatibel. Zwei-Faktor-Anmeldung ist auf ausdrücklichen Wunsch vorerst nicht eingeführt.
|
|
|
|
## 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 **44 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
|
|
```
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
|
|
## Notdienst-Erinnerungen und Veröffentlichungsmails
|
|
|
|
Die Migration `20261012000000_notdienst_notifications` ergänzt Mailaufträge und die monatliche Erinnerungsrunde. Vor dem Start der neuen App `npm run db:migrate:deploy` ausführen und die Runtime-Grants erneut bereitstellen. Der bestehende Initialisierungsdienst erledigt beide Schritte im Compose-Deployment. Kein `db push` verwenden; die bisherigen Tenant-Constraints bleiben erhalten.
|
|
|
|
`NOTDIENST_JOB_SECRET` (mindestens 32 Zeichen), `AUTH_URL`/`NEXT_PUBLIC_SITE_URL`, `RESEND_API_KEY` und `EMAIL_FROM` müssen eingerichtet sein. Ein neues Secret oder ein zusätzlicher externer Cronjob ist nicht nötig, wenn der vorhandene Notdienstjob bereits konfiguriert ist. Der Produktionsstart überwacht Webserver und Worker gemeinsam: der Worker wird nach einem Abbruch neu gestartet; beim Ende des Webservers werden beide Prozesse beendet.
|
|
|
|
Der geschützte `POST /api/jobs/notdienstalarme` verarbeitet weiterhin Alarme und zusätzlich Planungserinnerungen/gespeicherte Benachrichtigungen. Die JSON-Antwort ergänzt `planning` mit `queued`, `sent`, `failed` und `skipped`. Ein Lauf bearbeitet bis zu 100 fällige Kitas und zwölf Planungsmails (vier parallel). Der Worker ruft die Route minütlich auf. Versanddetails und manuelle Wiederholung finden sich in der Monatsplanung.
|
|
|
|
Die automatische Runde läuft einmal pro Kita und Folgemonat, ab 9 Uhr Berliner Zeit, standardmäßig ab dem 19. (sieben Tage vor Ende der Eintragsfrist am 26.). Ein verpasster Termin wird bis zum 26. nachgeholt. Die Einstellung 0 schaltet diese Runde ab; ein bereits veröffentlichter Plan beendet Verfügbarkeitserinnerungen. Manuelle Erinnerungen sind für den offenen Folgemonat möglich und je Konto/Tag mit der automatischen Runde zusammen dedupliziert.
|
|
|
|
Veröffentlichung und Mailvorbereitung sind eine Transaktion. Anschließend wird der Versand versucht; ein Mailfehler setzt den Plan nicht zurück. Wiederholungen prüfen aktuelle Konten, Haushalt, aktive Kinder und Einteilungen erneut. Pro Konto wird eine Mail mit allen aktuellen eigenen Diensten vorbereitet. Versandaufträge erhalten einen stabilen Idempotenzschlüssel je Nachrichteninhalt und eine fünfminütige Reservierung; Fehler erhalten zunehmende Wartezeiten und bleiben sichtbar. Der Mailtransport bricht HTTP-Anfragen nach zehn Sekunden ab. Anbieterannahme, wiederholter Versand nach unbekanntem Transportausgang und tatsächliche Postfachzustellung sind weiterhin durch die Garantien des Mailanbieters begrenzt.
|