L10b Betrieb & Aufräumen: Deploy – craftvia-worker, CI-Testjob, DEPLOY.md, Certvia-Doku archiviert

- docker-compose.coolify(.prebuilt).yml: Service craftvia-worker (Target worker, Chromium,
  shm_size 1gb, gleiche Härtung), Craftvia-Variablen für app und worker.
- Dockerfile: worker-Stage mit HOME=/home/app (Chromium-Profil für non-root); lokaler
  docker build der Targets runner und worker erfolgreich, PDF-Erzeugung im Image geprüft.
- .env.example/.env.prod.example/.env.coolify.example: alle Craftvia-Variablen inkl. RLS,
  KI-Provider, PDF_CHROMIUM_PATH, OFFLINE_MAX_DAYS, API_RATE_LIMIT_*, AI_GENERATION_RETENTION_DAYS,
  AI_MONTHLY_TOKEN_LIMIT.
- CI (.github, .gitea): Job gate mit Postgres (pgvector) und Redis als Service: migrate deploy,
  seed, Passwort für craftvia_app, tsc, lint, build, npm run test.
- docs/craftvia/DEPLOY.md (aus den Certvia-Deploy-Docs abgeleitet): Architektur, Domains, Secrets,
  Worker, Migrationen, RLS-Aktivierung, Backup/Restore, KI, Rate Limits, Aufbewahrung, Smoke,
  Update/Rollback. build-and-push-images.sh baut craftvia-worker.
- Certvia-/ISMS-Dokumente aus docs/ nach docs/_certvia-archiv/ (mit README); Verweise in README.md
  und Skript-Kommentaren angepasst.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 18:19:19 +02:00
co-authored by Claude Opus 5
parent 21d6dc016a
commit cadaedc6cc
38 changed files with 902 additions and 76 deletions
@@ -0,0 +1,57 @@
# Konzept — Konfigurierbarer Backup-Zielspeicher (Backend + Lokal) (certvia)
> Status: **Konzept/Entscheidungsvorlage** (kein Code). Produkt: **certvia**. Folge-Feature der Backup-Lane. Ziel: den Zielspeicher für Backup-/DSGVO-Artefakte **im Betreiber-Portal konfigurierbar** machen und **lokale (persistente) Speicherung** als vollwertige Option anbieten — nicht nur über Env.
## 1. Ist-Zustand (`src/server/storage/backup-store.ts`)
- Es gibt bereits eine **`BackupStore`-Abstraktion** (`put/get/list/remove`) mit zwei echten Implementierungen: **`S3BackupStore`** (MinIO/S3) und **`LocalBackupStore`** (echte Byte-Persistenz).
- **Aber:** Die Wahl trifft `createBackupStore()` **einmalig beim Prozessstart, rein Env-basiert**: alle `S3_*` gesetzt → S3, sonst lokaler Ordner (`BACKUP_LOCAL_DIR` bzw. `<cwd>/.backups`). Exportiert als **statisches Singleton** `backupStore`.
- **Nur 3 Nutzer:** `src/server/backup/export.ts`, `restore.ts`, `ops.ts`.
- **Schwächen:** (a) nicht im Backend wählbar; (b) der lokale Ordner liegt im Container → beim Redeploy **flüchtig**; (c) S3-Config nur als Env, nicht pro Betreiber pflegbar.
## 2. Zielbild
- **Betreiber wählt im Portal** das Ziel: **Lokal** oder **S3/MinIO**, inkl. Config, mit „Verbindung testen".
- **Lokal ist persistent** (gemountetes Volume), nicht flüchtig.
- Env bleibt als **Fallback** funktionsfähig (Rückwärtskompatibilität).
## 3. Datenmodell (`PlatformSetting`, Singleton erweitern)
Aktuell nur `mfaRequired`. Ergänzen:
- `backupTarget String @default("local")` — `local` | `s3`
- `backupLocalDir String?` — Pfad des lokalen Ziels (muss auf ein **gemountetes** Volume zeigen)
- `backupS3Endpoint / backupS3Bucket / backupS3Region / backupS3AccessKey String?`
- `backupS3SecretKeyEnc String?` — S3-Secret **verschlüsselt at-rest** über `src/server/secret-crypto.ts` (`encryptSecret`/`decryptSecret`, Schlüssel `MFA_ENC_KEY`/`AUTH_SECRET`) — **nie** Klartext in der DB.
Migration additiv (nullable, Default `local`). Kein Backfill nötig.
## 4. Store-Factory umbauen
- `backupStore`-Singleton → **`getBackupStore(): Promise<BackupStore>`**: liest `PlatformSetting`, baut den passenden Store, entschlüsselt den S3-Key.
- **Präzedenz:** DB-Config (wenn `backupTarget` gesetzt/vollständig) → **sonst** Env (`S3_*` / `BACKUP_LOCAL_DIR`) → **sonst** lokaler Default `.backups`. So bleibt bestehendes Env-Deployment lauffähig.
- **Caching + Invalidierung:** Store memoisieren, bei Änderung der Backup-Settings invalidieren (Version/Timestamp aus `PlatformSetting.updatedAt`).
- Die **3 Call-Sites** (`export.ts`, `restore.ts`, `ops.ts`) von `backupStore` auf `await getBackupStore()` umstellen.
- **Fail-secure:** unvollständige S3-Config → klarer Fehler (nicht still auf lokal fallen, wenn `backupTarget=s3` gewählt wurde).
## 5. Betreiber-UI
- Neue Seite im Plattform-Portal, z. B. **`/admin/backup`** (oder Abschnitt in den Plattform-Einstellungen).
- Gated: **`requirePlatformFullAdmin` + MFA-Step-up** (`assertPlatformStepUp`) — Betreiber-Config mit Credentials.
- Felder: Ziel-Radio (Lokal/S3), je nach Wahl die Config; **„Verbindung testen"** (Probe-`put`+`get`+`remove` eines winzigen Test-Keys) mit klarer Rückmeldung; Speichern über eine Action analog `setPlatformMfaRequired` (`platformSetting.upsert`, S3-Secret vor dem Schreiben verschlüsseln).
## 6. Persistenz für „Lokal" (Compose)
- In `docker-compose.coolify.yml` ein **persistentes Volume** ergänzen (analog `pgdata`/`miniodata`): `backups:` und in **app + worker** unter dem Pfad aus `backupLocalDir` mounten (z. B. `/app/.backups`). Ohne Mount bleibt Lokal flüchtig.
- Doku-Hinweis: `backupLocalDir` muss innerhalb des gemounteten Pfads liegen.
## 7. Sicherheit
- S3-Secret **nur verschlüsselt** in der DB (`secret-crypto`).
- Config-Bearbeitung nur **Full-Admin + Step-up**, auditiert.
- **`BACKUP_ENC_KEY`** (Artefakt-Verschlüsselung, `src/server/backup/crypto.ts`) bleibt **getrennt** vom Zielspeicher — Verschlüsselung des Inhalts ≠ Wahl des Speicherorts.
- Umgebungs-Secret-Kohärenz (Restore) unverändert: `BACKUP_ENC_KEY`/`PASSWORD_PEPPER`/`MFA_ENC_KEY` sind Env, nicht Artefakt (siehe `KONZEPT-backup-restore.md` §9).
## 8. Tests
- `scripts/test-backup-*` erweitern: Store-Auflösung aus DB-Config (local & s3), Präzedenz DB→Env→Default, „Verbindung testen"-Pfad, Fail-secure bei unvollständiger S3-Config.
- Export→Restore end-to-end gegen **beide** Backends.
## 9. Sofort testbar (ohne Umbau)
Schon heute: ohne `S3_*` fällt der Store auf **lokal** zurück → Export/Restore/DSGVO laufen (Artefakte im Container-`.backups`, **flüchtig**). Für einen schnellen Funktionstest genügt: **backup-worker + Redis** (vorhanden) + `BACKUP_ENC_KEY` (Fallback `AUTH_SECRET`). Der Umbau macht das Ziel **wählbar** und **persistent**.
## 10. Offene Entscheidungen
- Eigene Seite `/admin/backup` vs. Abschnitt in bestehenden Plattform-Einstellungen.
- Mehrere Ziele/Profile (Primär + Offsite) — jetzt 1 Ziel, Mehrfachziele als Phase 2.
- Ob der lokale Pfad frei wählbar ist oder auf den gemounteten Volume-Pfad festgelegt wird (empfohlen: fest, um Fehlkonfiguration zu vermeiden).