PROJ-66 (Planned): kein App- oder Cron-Backup für /var/archivmail/store, Keyfile, PostgreSQL auf 131 und 132 vorhanden. Nutzer-Rückmeldung ergänzt: Infra-Ebene sichert bereits per Proxmox Backup Server + Sync auf Zweitserver, das entschärft den Off-Site-Blocker deutlich - verbleibende Kernlücke ist ein nie getesteter Restore, nicht das fehlende Backup-Ziel. Bitwarden- Keyfile-Escrow als zusätzlicher, von PBS unabhängiger Wiederherstellungsweg ergänzt. PROJ-65: Deploy-Agent hatte Status/QA-Ergebnisse bereits lokal geschrieben, aber noch nicht committed.
360 lines
21 KiB
Markdown
360 lines
21 KiB
Markdown
---
|
|
id: PROJ-65
|
|
title: Physische Tenant-Trennung im Storage-Layer
|
|
status: Deployed
|
|
created: 2026-07-04
|
|
---
|
|
|
|
## Entscheidung (Nutzer, 2026-07-04)
|
|
Option A (Hardlink-Ordner pro Tenant, Dedup bleibt erhalten) **plus** Option B
|
|
(Storage-Permissions härten, Status-Check). Bestandsdaten werden einmalig per
|
|
CLI-Backfill nachgezogen (nicht nur neue Mails).
|
|
|
|
## Kontext
|
|
|
|
GoBD/DSGVO-Checkliste (`docs/GOBD_DSGVO_CHECKLIST.md`, Punkt 15) bewertet die
|
|
Mandantentrennung als "Erfüllt", vermerkt aber als bekannte Restlücke: die
|
|
Isolation läuft ausschließlich logisch über PostgreSQL (`emails.tenant_id`,
|
|
`email_refs`), NICHT physisch auf Dateisystem-Ebene. Manticore hat bereits
|
|
physisch getrennte Indizes (`emails_tenant_<id>`), der verschlüsselte
|
|
Mail-Storage (`internal/storage/`) dagegen nicht.
|
|
|
|
Diese Spec bewertet, ob/wie physische Trennung sinnvoll nachgerüstet werden
|
|
kann, ohne bestehende Dedup-Mechanismen zu brechen.
|
|
|
|
## Ist-Zustand (verifiziert, 2026-07-04)
|
|
|
|
- `filePath(id)` (`internal/storage/storage.go:1288`) legt jede Mail unter
|
|
`store/<hash-prefix>/<content-hash>` ab — der Pfad ist rein content-adressiert,
|
|
keine Tenant-Information im Pfad.
|
|
- `email_refs`-Tabelle (`storage.go:273-281`) ist eine M:N-Verknüpfung
|
|
`email_id <-> tenant_id`: **eine physische Mail-Datei kann zu mehreren
|
|
Tenants gehören** (Content-Dedup, siehe PROJ-32 Message-ID-Dedup + PROJ-37
|
|
Attachment-Deduplication). Das passiert real z.B. bei einer Rundmail an
|
|
Empfänger in unterschiedlichen Mandanten, oder wenn zwei Tenants dieselbe
|
|
Mail per BCC-Journal UND IMAP-Import erhalten.
|
|
- Zugriffskontrolle erfolgt ausschließlich über SQL-JOIN auf `email_refs`
|
|
(`Load()`/`Delete()`/Listing-Queries) — kein direkter Dateisystemzugriff
|
|
ohne DB-Gate im aktuellen Code (verifiziert: alle API-Handler gehen über
|
|
`Store`-Methoden, keine Pfad-Konstruktion in `internal/api/`).
|
|
|
|
## Warum "ein Ordner pro Tenant" nicht trivial ist
|
|
|
|
Eine naive Umsetzung ("Mail-Datei nach `store/tenant_<id>/<hash>` statt
|
|
`store/<hash-prefix>/<hash>` ablegen") bricht am Cross-Tenant-Dedup-Modell:
|
|
- Eine Mail mit **zwei** `email_refs`-Einträgen (zwei Tenants) hätte keinen
|
|
eindeutigen "richtigen" Ordner mehr — Datei müsste doppelt vorgehalten
|
|
werden (Speicherplatz-Verdopplung, widerspricht PROJ-36/PROJ-37-Ziel) oder
|
|
über Hardlinks/Symlinks in beide Tenant-Ordner verknüpft werden.
|
|
- Hardlinks würden auf den meisten Filesystemen funktionieren (gleiche
|
|
Partition vorausgesetzt), sind aber selbst kein zusätzlicher Zugriffsschutz
|
|
— ein Prozess mit Dateisystemzugriff auf einen Tenant-Ordner sieht trotzdem
|
|
den vollen (unverschlüsselten Struktur-Namen preisgebenden) Inhalt, nur der
|
|
Pfad ist getrennt. Der eigentliche Schutz bleibt die AES-256-GCM-Verschlüsselung
|
|
pro Datei (PROJ-49), nicht die Ordnerstruktur.
|
|
|
|
## Ziel
|
|
|
|
Physische Trennung so weit erhöhen, wie es ohne Aufgabe des Dedup-Modells und
|
|
ohne Speicherplatz-Verdopplung möglich ist — als zusätzliche Verteidigungsebene
|
|
("defense in depth"), nicht als Ersatz für die DB-gestützte Zugriffskontrolle.
|
|
|
|
## Entscheidung (zur Nutzer-Freigabe)
|
|
|
|
Drei Optionen, aufsteigender Aufwand:
|
|
|
|
**Option A — Tenant-Verzeichnis nur für eindeutig einem Tenant zugehörige Mails,
|
|
Hardlink-Farm für Mehrfach-Tenant-Fälle**
|
|
- Mails mit genau einem `email_refs`-Eintrag: Ablage/zusätzlicher Hardlink
|
|
unter `store/tenant_<id>/<hash-prefix>/<hash>`.
|
|
- Mails mit >1 Tenant (Dedup-Fall, laut Auswertung <1% der Bestandsmails):
|
|
Hardlink in jedes betroffene Tenant-Verzeichnis — physische Datei bleibt
|
|
einmal auf der Platte (kein Speicher-Overhead), aber über mehrere Pfade
|
|
erreichbar.
|
|
- Root-Ordner `store/<hash-prefix>/<hash>` bleibt zusätzlich bestehen
|
|
(Kompatibilität, Rückwärtskompatibilität für Backup-Skripte).
|
|
- Aufwand: mittel (Hardlink-Verwaltung bei Save/Delete/Ref-Änderung,
|
|
Hardlink-Zähler beim Löschen beachten — Datei erst physisch löschen wenn
|
|
letzter Link entfernt wird, sonst Datenverlust für den verbleibenden Tenant).
|
|
|
|
**Option B — Nur Metadaten-Trennung verstärken, kein Dateisystem-Umbau**
|
|
- Kein Pfad-Umbau. Stattdessen: OS-Level-Zugriffskontrolle prüfen/dokumentieren
|
|
(z.B. `archivmail`-Prozess läuft unter eigenem User, Storage-Verzeichnis
|
|
`0700`), plus Audit-Log-Nachweis, dass jeder Lesezugriff über `tenant_id`-Filter
|
|
lief (bereits Stand heute). Checklist-Punkt 15 bleibt "bekannte Design-Grenze,
|
|
bewusst akzeptiert" statt als offener Fix geführt.
|
|
- Aufwand: klein (nur Doku + Dateisystem-Berechtigungs-Audit).
|
|
- Realistisch für die meisten Kunden ausreichend, da DB-Zugriffskontrolle
|
|
bereits der einzige Zugriffspfad im Code ist.
|
|
|
|
**Option C — Dedup nur noch pro Tenant (kein Cross-Tenant-Dedup mehr)**
|
|
- Ändert `Save()` so, dass Content-Hash-Dedup nur INNERHALB eines Tenants
|
|
greift, nicht mehr global. Ermöglicht echte 1:1-Ordner-pro-Tenant-Struktur
|
|
ohne Hardlinks.
|
|
- Nachteil: Speicherplatz-Mehrverbrauch bei Mails, die aktuell tenant-übergreifend
|
|
dedupliziert werden (Größenordnung unbekannt, müsste vor Umsetzung gemessen
|
|
werden: `SELECT email_id, COUNT(DISTINCT tenant_id) FROM email_refs GROUP BY email_id HAVING COUNT(DISTINCT tenant_id) > 1`).
|
|
- Bricht rückwirkend nicht (Altbestand bleibt dedupliziert, nur neue Mails
|
|
betroffen) — aber inkonsistentes Modell (alt vs. neu) muss dokumentiert sein.
|
|
- Aufwand: mittel-hoch (Save-Logik ändern, Migration/Messung vorab nötig).
|
|
|
|
**Empfehlung:** Option B zuerst umsetzen (schnell, dokumentiert den Ist-Zustand
|
|
ehrlich), Option A nur wenn ein konkreter Kunde/Auditor physische Trennung
|
|
explizit fordert (dann Aufwand gerechtfertigt).
|
|
|
|
## Acceptance Criteria (Option B, empfohlener Scope)
|
|
|
|
- [x] Storage-Verzeichnis-Berechtigungen geprüft/dokumentiert: `store/` gehört
|
|
dem `archivmail`-Prozess-User, Modus `0700` (kein Gruppen-/World-Zugriff).
|
|
- [x] `archivmail status` (`cmd_status.go`) bekommt einen Prüfpunkt, der die
|
|
Storage-Verzeichnis-Permissions verifiziert und bei zu offenen Rechten warnt
|
|
(analog `checkEncryption` aus PROJ-49).
|
|
- [x] GoBD-Checkliste Punkt 15 umformuliert (siehe Implementation Notes).
|
|
- [ ] Messung durchgeführt und dokumentiert: Anteil der Mails mit >1
|
|
`email_refs`-Tenant-Zuordnung am Gesamtbestand — steht als QA-Schritt
|
|
auf dem Testserver noch aus (siehe Implementation Notes).
|
|
|
|
## Acceptance Criteria (Option A, gewählter Scope)
|
|
|
|
- [x] Hardlink-Erstellung bei `Save()` für jeden `email_refs`-Eintrag (inkl.
|
|
Message-ID- und SHA-256-Dedup-Zweige, nicht nur den initialen Schreibpfad).
|
|
- [x] Hardlink-Bereinigung bei `Delete()` berücksichtigt alle Tenants, die die
|
|
Mail referenzierten (Tenant-Set wird VOR dem DB-Löschen erfasst, da
|
|
`Delete()` die Mail komplett entfernt statt nur eine Tenant-Referenz).
|
|
- [x] Kein Speicherplatz-Mehrverbrauch gegenüber heutigem Zustand (Hardlinks
|
|
teilen sich denselben Inode).
|
|
- [ ] Backup-/Restore-Prozess berücksichtigt Hardlinks korrekt — Hinweis an
|
|
devops-deploy nötig (`-H`/Hardlink-Erhalt beim Backup-Tool), noch nicht
|
|
verifiziert.
|
|
- [x] Bestehende Dateisystem-Operationen (`filePath`, `Load`, `Delete`) bleiben
|
|
für den Root-Pfad `store/<hash-prefix>/<hash>` unverändert kompatibel —
|
|
Tenant-Hardlinks sind rein additiv, keine bestehende Funktion liest von
|
|
dort.
|
|
- [x] Backfill für Bestandsdaten: neues CLI-Subcommand
|
|
`archivmail migrate-tenant-dirs`, idempotent (füllt nur Lücken).
|
|
|
|
## Edge Cases
|
|
|
|
- Mail wird nach Save einem zweiten Tenant zugeordnet (Dedup-Treffer bei
|
|
späterem Import) → neuer Hardlink muss nachträglich angelegt werden, nicht
|
|
nur beim initialen `Save()`.
|
|
- Tenant wird gelöscht, Mail hatte nur diesen einen `email_refs`-Eintrag →
|
|
Hardlink-Entfernung darf nicht die letzte verbleibende Kopie löschen, wenn
|
|
parallel (Race) noch ein zweiter Tenant referenziert.
|
|
- Migration von Bestandsdaten (Altmails ohne Tenant-Ordner) — muss einmalig
|
|
nachgezogen werden (`archivmail migrate-tenants` erweitern oder neues
|
|
Subcommand), sonst inkonsistenter Zustand alt/neu.
|
|
|
|
## Betroffene Dateien (Option A, bei Umsetzung)
|
|
|
|
- `internal/storage/storage.go` (`filePath`, `Save`, `Delete`)
|
|
- `cmd/archivmail/cmd_migrate_tenants.go` (Backfill für Bestandsdaten)
|
|
- `docs/GOBD_DSGVO_CHECKLIST.md` (Punkt 15 Update nach Umsetzung)
|
|
|
|
## Betroffene Dateien (Option B, bei Umsetzung)
|
|
|
|
- `cmd/archivmail/cmd_status.go` (neuer Prüfpunkt Storage-Permissions)
|
|
- `docs/GOBD_DSGVO_CHECKLIST.md` (Punkt 15 Neuformulierung)
|
|
- ggf. `install.sh`/`update.sh` (Verzeichnis-Permissions beim Deploy setzen,
|
|
falls noch nicht der Fall)
|
|
|
|
---
|
|
|
|
## Tech Design (Solution Architect)
|
|
Übersprungen — additive Ergänzung zum bestehenden Storage-Layer (kein neuer
|
|
Zugriffspfad, keine Änderung der DB-gestützten Zugriffskontrolle), analog
|
|
PROJ-55/56.
|
|
|
|
## Implementation Notes (2026-07-04)
|
|
|
|
### Neue Datei `internal/storage/tenant_dirs.go`
|
|
- `tenantFilePath(tenantID, id)`: `store/tenant_<id>/<hash-prefix>/<hash>` —
|
|
gleiches 2-Zeichen-Sharding wie `filePath()`, nur zusätzlich unter einem
|
|
Tenant-Ordner genestet.
|
|
- `linkTenantDir(id, tenantID)`: legt Hardlink von der kanonischen Datei
|
|
(`filePath(id)`) auf den Tenant-Pfad an, `MkdirAll(..., 0o700)`. Best-effort
|
|
— ein Fehler hier darf `Save()`/Import niemals scheitern lassen (Warn-Log
|
|
via `slog.Default()`, kein Fehler-Return). Idempotent (Stat-Check vor
|
|
`os.Link`, `os.ErrExist` wird ignoriert).
|
|
- `unlinkTenantDirs(id, tenantIDs)`: entfernt die Hardlinks aus allen
|
|
übergebenen Tenant-Verzeichnissen, ignoriert `os.ErrNotExist`.
|
|
- `TenantsForMail(ctx, id)`: liefert die Vereinigung aus `emails.tenant_id`
|
|
(primärer Tenant) und allen `email_refs`-Einträgen (Cross-Tenant-Dedup) —
|
|
das vollständige Sichtbarkeits-Set für eine Mail.
|
|
- `BackfillTenantDirs(ctx)`: iteriert `GetAllIDs()`, legt fehlende Hardlinks
|
|
nach; zählt `linked`/`errCount`. Idempotent, für den Backfill-Befehl.
|
|
|
|
### Wiring in `internal/storage/storage.go`
|
|
- `Save()`: `linkTenantDir()` an allen drei Stellen ergänzt, an denen bisher
|
|
`email_refs` per `INSERT ... ON CONFLICT DO NOTHING` befüllt wurde
|
|
(Message-ID-Dedup-Treffer, Race-Conflict-Resolution, finaler
|
|
"ensure email_ref"-Block) — sonst hätte ein dedupliziertes Cross-Tenant-
|
|
Save keinen Hardlink für den zweiten Tenant bekommen (Edge Case aus der
|
|
Spec).
|
|
- `Delete()`: `TenantsForMail(ctx, id)` wird VOR dem Start der Lösch-Transaktion
|
|
aufgerufen (Kommentar im Code erklärt warum: `Delete()` entfernt eine Mail
|
|
komplett inkl. aller `email_refs`, nicht nur eine einzelne Tenant-Referenz —
|
|
nach dem `DELETE FROM email_refs` wüsste der Code nicht mehr, welche
|
|
Tenant-Ordner überhaupt einen Link hatten). `unlinkTenantDirs()` läuft erst
|
|
NACH dem erfolgreichen `os.Remove()` der kanonischen Datei.
|
|
|
|
### Neues CLI-Subcommand `archivmail migrate-tenant-dirs`
|
|
- `cmd/archivmail/cmd_migrate_tenant_dirs.go`, registriert in `main.go`.
|
|
- Backfill für Bestandsdaten (Nutzer-Entscheidung: "einmalig migrieren").
|
|
Ruft `Store.BackfillTenantDirs()`, idempotent, kann gefahrlos mehrfach
|
|
laufen (füllt nur fehlende Links).
|
|
- Kein Flag für Tenant-Einschränkung nötig — iteriert ohnehin über
|
|
`TenantsForMail()` pro Mail, kein Vollscan-Performance-Problem erwartet
|
|
bei den aktuellen Datenmengen (siehe Memory: Datenvolumen aktuell trivial).
|
|
|
|
### `cmd/archivmail/cmd_status.go`: neuer Check `checkStoragePermissions`
|
|
- Analog `checkEncryption`/`checkRetention`: warnt (OK bleibt `true`, kein
|
|
Hard-Fail) wenn `store_path` Modus `&0o077 != 0` ist (Gruppe/Andere haben
|
|
Zugriff). `New()` legt neue Verzeichnisse bereits mit `0o700` an — dieser
|
|
Check fängt Alt-Installationen mit abweichenden Rechten ab.
|
|
|
|
### Was bewusst NICHT geändert wurde
|
|
- Kein Zugriffspfad in `internal/api/` liest direkt vom Dateisystem — alle
|
|
Handler gehen über `Store`-Methoden. Die Tenant-Hardlinks sind rein additiv
|
|
und werden von keinem bestehenden Code gelesen; sie sind ausschließlich für
|
|
manuelle Dateisystem-Audits/Auditor-Einsicht gedacht (Verzeichnis
|
|
`find store/tenant_<id>/` zeigt physisch genau das, was ein Tenant sehen
|
|
darf).
|
|
- Kein Vollscan-Backup-Anpassung vorgenommen — siehe offener AC-Punkt
|
|
"Backup-/Restore-Prozess" (Handoff an devops-deploy).
|
|
|
|
### Offene Punkte / Handoff
|
|
- **Messung Cross-Tenant-Dedup-Quote** (AC aus Option B) noch nicht
|
|
durchgeführt — steht auf dem Testserver aus:
|
|
`SELECT email_id, COUNT(DISTINCT tenant_id) FROM email_refs GROUP BY email_id HAVING COUNT(DISTINCT tenant_id) > 1;`
|
|
- **Backup-Prozess-Check**: devops-deploy sollte verifizieren, dass das
|
|
eingesetzte Backup-Tool Hardlinks erhält (z.B. `rsync -H`, `tar` erhält
|
|
Hardlinks standardmäßig; ein naives `cp -r` würde sie zu vollen Kopien
|
|
auflösen — kein Datenverlust, aber Speicherplatz-Verdopplung im Backup).
|
|
- **`migrate-tenant-dirs` einmalig auf Produktiv ausführen** nach Deploy, dann
|
|
optional in `update.sh` als informativer Hinweis (nicht automatisch bei
|
|
jedem Deploy laufen lassen — reiner Backfill, macht nach dem ersten Lauf
|
|
nichts mehr).
|
|
- Kein lokaler `go build`/`go test` möglich (kein Toolchain im
|
|
Arbeitsverzeichnis) — Build-/Testverifikation erfolgt separat auf dem
|
|
Testserver.
|
|
|
|
### Geänderte/neue Dateien
|
|
- `internal/storage/tenant_dirs.go` (NEU)
|
|
- `internal/storage/storage.go` (`Save`, `Delete` — Hardlink-Wiring)
|
|
- `cmd/archivmail/cmd_migrate_tenant_dirs.go` (NEU)
|
|
- `cmd/archivmail/main.go` (Subcommand registriert)
|
|
- `cmd/archivmail/cmd_status.go` (`checkStoragePermissions`)
|
|
- `docs/GOBD_DSGVO_CHECKLIST.md` (Punkt 15, siehe separater Commit)
|
|
|
|
## QA Test Results (2026-07-04, Testserver 192.168.1.132)
|
|
|
|
**Build:** `CGO_ENABLED=0 go build ./cmd/archivmail/` auf 192.168.1.132 erfolgreich
|
|
(go1.24.4), 0 Fehler/Warnungen. Getestet mit dem frisch gebauten Binary
|
|
(`/tmp/archivmail-proj65`, nach Test entfernt). Hinweis: der laufende Dienst
|
|
auf 132 ist noch das alte 0.9.1-Binary ohne PROJ-65 — Produktiv-Deploy +
|
|
einmaliger Backfill sind Handoff an devops-deploy (siehe unten).
|
|
|
|
**Testdaten-Hygiene:** Backfill wurde als QA-Test auf dem echten Test-Store
|
|
gefahren (51304 Mails), danach die erzeugten `store/tenant_<id>/`-Verzeichnisse
|
|
und das Test-Binary wieder entfernt, da der laufende Dienst noch das alte Binary
|
|
ist (sonst inkonsistenter Zustand: Backfill-Links vorhanden, aber neue Mails ohne
|
|
Link). Kanonische Dateien nach Cleanup intakt verifiziert (Linkzähler zurück auf
|
|
1, Dateigröße unverändert). Keine echten Passwörter/Configs überschrieben; für
|
|
den 0755/0750-Test wurde eine temporäre Kopie der Config mit umgebogenem
|
|
`store_path` in einem `mktemp -d` genutzt, danach gelöscht.
|
|
|
|
### Acceptance Criteria — Option B
|
|
|
|
| # | Kriterium | Ergebnis |
|
|
|---|-----------|----------|
|
|
| B1 | store/ gehört archivmail-User, Modus 0700 | **PASS** — `/var/archivmail/store` = `700 archivmail:archivmail` |
|
|
| B2 | `archivmail status` prüft Storage-Permissions, warnt bei zu offenen Rechten | **PASS** — neuer Check `Storage-Rechte` erscheint in Text- und JSON-Ausgabe; 0700 → OK, 0755 + 0750 → WARN (nie Hard-Fail, Exit 0), 0700 → OK. Alle drei chmod-Fälle live verifiziert. |
|
|
| B3 | GoBD-Checkliste Punkt 15 umformuliert | **PASS** — Punkt 15 nennt PROJ-65, Hardlink-Dirs, Backfill, 0700-Überwachung, Defense-in-Depth-Einordnung |
|
|
| B4 | Messung Cross-Tenant-Dedup-Quote | **PASS/erledigt** — 8 von 51304 Mails mit >1 Tenant-Zuordnung (~0,016 %), bestätigt die Spec-Annahme "<1%" |
|
|
|
|
### Acceptance Criteria — Option A
|
|
|
|
| # | Kriterium | Ergebnis |
|
|
|---|-----------|----------|
|
|
| A1 | Hardlink-Erstellung bei Save (alle Dedup-Zweige) | **PASS (via Backfill verifiziert)** — Backfill legte 50015 Links an (== `COUNT(*) email_refs`), 0 Fehler. Save-Wiring code-reviewed (drei email_refs-INSERT-Stellen). Laufzeit-Verifikation der Save-Pfade steht mit Deploy noch aus (alter Dienst). |
|
|
| A2 | Delete berücksichtigt alle referenzierenden Tenants | **PASS (Hardlink-Semantik verifiziert)** — Entfernen des Tenant-Links ließ die kanonische Datei intakt (Linkzähler 1, Größe unverändert); `TenantsForMail()` erfasst Primär-Tenant + email_refs vor DB-Delete (code-reviewed). Voller Delete-API-Durchlauf nicht gefahren (kein Löschen echter archivierter Mails / GoBD). |
|
|
| A3 | Kein Speicherplatz-Mehrverbrauch (gleicher Inode) | **PASS** — Einzel-Tenant-Mail: Inode 32623 in Root + Tenant-Dir identisch, Linkzähler 2. Cross-Tenant-Mail (Tenants 1+3): Inode 33626 in Root + tenant_1 + tenant_3 identisch, Linkzähler 3. |
|
|
| A4 | Backup-/Restore erhält Hardlinks | **OFFEN** — nicht QA-prüfbar, Handoff an devops-deploy (`rsync -H`/`tar`), bleibt offener AC-Punkt. |
|
|
| A5 | Root-Pfad-Operationen unverändert kompatibel | **PASS** — `filePath()`/`Load()`/`Delete()` unverändert am Root-Pfad; Tenant-Links rein additiv, kein bestehender Code liest daraus. |
|
|
| A6 | Backfill-Subcommand idempotent | **PASS** — `migrate-tenant-dirs` in main.go registriert; erster Lauf linked=50015/errors=0, zweiter Lauf linked=0/errors=0 (idempotent). |
|
|
|
|
### Tenant-Isolation / Sicherheit
|
|
- Tenant-Verzeichnisse werden mit **0700** angelegt (`MkdirAll(..., 0o700)`) —
|
|
live verifiziert (`drwx------ archivmail:archivmail` für tenant_1/2/3). Kein
|
|
Gruppen-/World-Zugriff, konsistent mit dem Defense-in-Depth-Ziel.
|
|
- Kein neuer Zugriffspfad in `internal/api/` — Hardlinks sind rein additiv,
|
|
DB-gestützte `tenantAccessAllowed()`-Kontrolle bleibt maßgeblich. Keine neue
|
|
IDOR-/Cross-Tenant-Angriffsfläche (kein per-ID-HTTP-Endpunkt hinzugekommen).
|
|
- `linkTenantDir()` ist best-effort (Warn-Log, kein Fehler-Return) — ein
|
|
Filesystem-Fehler kann Save/Import nicht zum Scheitern bringen. Korrekt für
|
|
eine additive Defense-in-Depth-Ebene.
|
|
|
|
### Regression
|
|
Bestehende `archivmail status`-Checks (PostgreSQL, Manticore, Storage,
|
|
Encryption, Audit-Log, Retention) weiterhin unverändert funktional; der neue
|
|
`Storage-Rechte`-Check ist additiv und beeinflusst den Exit-Code nicht.
|
|
|
|
### Bugs
|
|
Keine. Keine Blocker, keine Regressionen gefunden.
|
|
|
|
### Handoff / offene Punkte (nicht QA-Blocker)
|
|
- **devops-deploy:** (1) Produktiv-Deploy des PROJ-65-Binaries, (2) danach
|
|
einmalig `archivmail migrate-tenant-dirs` ausführen, (3) Backup-Tool auf
|
|
Hardlink-Erhalt prüfen (AC A4, `rsync -H`/`tar`).
|
|
- Laufzeit-Verifikation der Save-/Delete-Hardlink-Pfade des *laufenden* Dienstes
|
|
erst nach Deploy sinnvoll (aktuell läuft noch das alte Binary).
|
|
|
|
**Gesamtergebnis: 9 von 11 AC PASS, 2 OFFEN (A4 Backup-Handoff, A1/A2
|
|
Laufzeit-Verifikation nach Deploy) — 0 Bugs. Empfehlung: freigeben für Deploy.**
|
|
|
|
## Deployment
|
|
|
|
**2026-07-04, devops-deploy, Produktivserver 192.168.1.131**
|
|
|
|
- Commit `a15fa37` gepusht nach `origin/main` (war lokaler HEAD, noch nicht auf
|
|
Remote).
|
|
- `bash /opt/archivmail/update.sh` auf 131 ausgeführt: Quellcode aktualisiert,
|
|
Backend gebaut, Frontend gebaut, Cron-Jobs eingespielt, systemd-Units
|
|
synchronisiert. Ergebnis: `Backend ✓ läuft`, `Frontend ✓ läuft`.
|
|
- **Backfill** `archivmail migrate-tenant-dirs --config /etc/archivmail/config.yml`
|
|
ausgeführt: `linked=0, errors=0`. Kein Bug — auf 131 haben alle 151 Bestandsmails
|
|
`tenant_id = NULL` und `email_refs` ist leer (0 Zeilen); Produktiv läuft aktuell
|
|
ohne aktive Mandanten-Zuordnung, anders als der Testserver 132 (dort 51304 Mails
|
|
mit Tenant-Zuordnung, dort lief der Backfill im QA-Test mit `linked=50015`).
|
|
Sobald auf 131 Mails einem Tenant zugeordnet werden (`emails.tenant_id` oder
|
|
`email_refs`), legt `Save()` die Hardlinks automatisch an; ein erneuter
|
|
`migrate-tenant-dirs`-Lauf ist jederzeit gefahrlos wiederholbar.
|
|
- **Smoke-Test:**
|
|
- `archivmail status` zeigt neuen Check `[OK] Storage-Rechte /var/archivmail/store
|
|
Modus 0700 — nur Owner-Zugriff`.
|
|
- Backend-Health `GET /api/health` → 200, Frontend `GET /` → 200, beide
|
|
systemd-Units `active`.
|
|
- Hardlink-Stichprobe (Inode-Vergleich bestehende Mail Root- vs. Tenant-Pfad)
|
|
**nicht durchführbar auf 131**, da mangels Tenant-Zuordnung keine
|
|
`store/tenant_<id>/`-Verzeichnisse angelegt wurden (erwartetes Verhalten,
|
|
kein Fehlschlag). Die Hardlink-Semantik selbst wurde bereits im QA-Lauf auf
|
|
132 mit echten Tenant-Daten verifiziert (Inode-Gleichheit Root/Tenant-Pfad,
|
|
Linkzähler korrekt), siehe QA Test Results oben.
|
|
- **Backup-Prozess-Check (AC A4):** Auf 131 existiert **kein** dediziertes
|
|
Backup-Tool/-Skript für `/var/archivmail/store` — weder in `/etc/cron.d/`
|
|
(nur `archivmail`-Cron mit OCR-Pause, Purge, Reindex-Backlog, Reconciliation,
|
|
keine Backup-Zeile), noch als systemd-Timer, noch als eigenständiges Skript
|
|
unter `/opt/archivmail` oder `/usr/local/bin`. Die Hardlink-Erhalt-Frage
|
|
(`rsync -H` vs. `tar` vs. naives `cp -r`) ist damit aktuell **gegenstandslos,
|
|
weil es noch keinen Store-Backup-Prozess gibt** — das ist ein eigenständiger,
|
|
vom PROJ-65-Deploy unabhängiger offener Punkt und bleibt hier als Hinweis
|
|
stehen, nicht selbst behoben (kein Backup-Skript angelegt).
|
|
- **Restrisiko/offene Punkte für spätere Sprints:**
|
|
1. Store-Backup-Prozess für 131 fehlt komplett (nicht PROJ-65-Scope, aber im
|
|
Zuge dieses Deploys aufgefallen — sollte als eigenes Ticket nachgezogen
|
|
werden, inkl. Hardlink-Erhalt-Vorgabe für PROJ-65).
|
|
2. Save-/Delete-Hardlink-Pfade des jetzt laufenden Produktiv-Binaries sind auf
|
|
131 mangels Tenant-Zuordnung noch nicht mit echten Live-Daten beobachtet
|
|
worden (nur code-reviewed + auf 132 mit echten Daten verifiziert).
|