Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
244 lines
12 KiB
Markdown
244 lines
12 KiB
Markdown
# Unveränderlichkeit und Aufbewahrung
|
|
|
|
Phase 11. Der Teil der Anlage, der Backups gegen den Fall schützt, für den man
|
|
sie am dringendsten braucht: Jemand — Ransomware, ein Bedienfehler, ein
|
|
kompromittierter Control Server — will sie loswerden.
|
|
|
|
Der Implementierungsplan setzt für diese Phase eine Grenze, die den ganzen
|
|
Aufbau bestimmt:
|
|
|
|
> Do not claim true immutability if the underlying storage cannot enforce it.
|
|
|
|
## Die Durchsetzungsstufe wird gemessen, nicht behauptet
|
|
|
|
„Unveränderlich" ist in Backup-Produkten ein Wort, das oft eine Datenbankspalte
|
|
meint: Die Software weigert sich zu löschen, und jeder mit Dateizugriff löscht
|
|
trotzdem. Wer darauf eine Ransomware-Strategie baut, verliert seine Backups.
|
|
|
|
`POST /repositories/{id}/enforcement/measure` stellt deshalb fest, was das
|
|
Dateisystem **tatsächlich** verhindert. Die Messung legt Probedateien im
|
|
Metadatenverzeichnis an und versucht, sie zu löschen:
|
|
|
|
| Stufe | Bedeutung |
|
|
| --- | --- |
|
|
| `none` | Kein Schutz. |
|
|
| `advisory` | Nur diese Software hält sich daran. Wer Dateizugriff hat, löscht trotzdem. |
|
|
| `filesystem` | Das Dateisystem verweigert die Löschung. Ein Systemverwalter kann den Schutz weiterhin aufheben. |
|
|
| `storage` | Die Speicherebene verhindert es (S3 Object Lock, WORM). **Nicht umgesetzt** — wird nie vergeben. |
|
|
|
|
Der Bericht nennt die Versuche einzeln, nicht nur das Ergebnis:
|
|
|
|
```json
|
|
{
|
|
"level": "filesystem",
|
|
"observations": [
|
|
{ "attempt": "Eine Datei mit den Rechten 0400 loeschen", "prevented": false,
|
|
"detail": "Die Loeschung gelang. Dateirechte schuetzen unter POSIX nicht vor dem Loeschen …" },
|
|
{ "attempt": "Eine Datei mit Unveraenderlich-Kennzeichen loeschen", "prevented": true,
|
|
"detail": "Das Betriebssystem verweigerte die Loeschung: … operation not permitted" }
|
|
]
|
|
}
|
|
```
|
|
|
|
### Der Irrtum, mit dem diese Phase begann
|
|
|
|
Der ursprüngliche gehärtete Modus legte Manifeste mit den Rechten `0400` ab und
|
|
nannte das Löschschutz. **Das verhindert kein Löschen.** Unter POSIX hängt das
|
|
Entfernen einer Datei am Schreibrecht ihres *Verzeichnisses*, nicht an ihren
|
|
eigenen Rechten. Gemessen, bestätigt, und als Regressionstest festgehalten
|
|
(`TestReadOnlyPermissionsDoNotPreventDeletion`).
|
|
|
|
Den Löschschutz leistet das Unveränderlich-Kennzeichen des Dateisystems:
|
|
`UF_IMMUTABLE` über `chflags` auf macOS/BSD, `FS_IMMUTABLE_FL` über `ioctl` auf
|
|
Linux (ext2/3/4, xfs, btrfs; braucht `CAP_LINUX_IMMUTABLE`). Windows kennt nur
|
|
ein Schreibschutz-Attribut, das nicht vor Löschung schützt — dort meldet die
|
|
Messung `advisory`, statt etwas zu behaupten.
|
|
|
|
## Was geschützt wird — und warum alles davon
|
|
|
|
Zwei Angriffsversuche waren zunächst erfolgreich. Beide Male überlebte das
|
|
Manifest, und das Backup war trotzdem verloren:
|
|
|
|
1. **Die Chunks fehlten.** Zurück blieb ein Manifest, das auf nichts mehr zeigte
|
|
— ein Backup, das sich für vollständig ausgibt und leer ist.
|
|
2. **Descriptor und Datenschlüssel fehlten.** Die Daten waren da, aber weder als
|
|
Repository erkennbar noch entschlüsselbar. Der Datenschlüssel ist das
|
|
lohnendste Ziel des gesamten Repositorys: Wer diese eine kleine Datei löscht,
|
|
vernichtet jedes verschlüsselte Backup, ohne einen Datenblock anzufassen.
|
|
|
|
In einem gehärteten Repository trägt deshalb **jede** Datei das Kennzeichen, die
|
|
zur Wiederherstellung gebraucht wird:
|
|
|
|
| Datei | Geschützt | Grund |
|
|
| --- | --- | --- |
|
|
| `manifests/*.manifest.json` | ja | Das Backup selbst. |
|
|
| `manifests/*.hold.json` | ja | Sonst hübe ein `rm` einer kleinen Datei den Legal Hold auf. |
|
|
| `chunks/**` | ja | Ohne sie ist das Manifest wertlos. |
|
|
| `format/repository.json` | ja | Einstiegspunkt jedes Wiederaufbaus. |
|
|
| `metadata/data-key.json` | ja | Ohne ihn ist alles Geheimtext. |
|
|
| `indexes/catalog.json` | **nein** | Nur ein Beschleuniger, wird laufend überschrieben und bei Verlust neu gebaut. |
|
|
|
|
Nachgewiesen: `rm -rf` gegen ein gehärtetes Repository meldet 15 verweigerte
|
|
Löschungen, alle geschützten Dateien überleben, und der anschließende
|
|
Tiefenscan meldet das Backup als vollständig.
|
|
|
|
Die Kehrseite steht ausdrücklich hier: **Ein gehärtetes Repository lässt sich
|
|
nicht mit `rm -rf` entfernen.** Wer es ausmustern will, hebt den Schutz Datei für
|
|
Datei auf (`ReleaseImmutableFlagForMaintenance`). Eine rekursive Fassung gibt es
|
|
nicht — sie wäre genau das Werkzeug, das ein Angreifer sucht.
|
|
|
|
## Aufbewahrungsschutz und Legal Hold
|
|
|
|
Die Frist eines Repositorys steht in seinem Descriptor, nicht in der Datenbank:
|
|
Wer das Repository an einen fremden Server anhängt, soll die geltende Frist
|
|
vorfinden und nicht die des neuen Servers untergeschoben bekommen. Ohne Angabe
|
|
gelten 30 Tage.
|
|
|
|
- **Verlängern ja, verkürzen nie** — auch nicht für einen Administrator. Wäre es
|
|
möglich, bestünde der Schutz nur aus einer Zahl, die der Angreifer zuallererst
|
|
ändern würde.
|
|
- **Legal Hold** ist unbefristet und endet nicht von selbst. Er verlangt eine
|
|
Begründung: Ohne sie wäre er nach einem Jahr nicht mehr auflösbar, weil sich
|
|
niemand traut, einen Schutz aufzuheben, dessen Anlass unbekannt ist.
|
|
- Ein aufgehobener Legal Hold gibt ein Backup **nicht** frei, das noch unter
|
|
Frist steht. Beide Schutzarten gelten nebeneinander.
|
|
|
|
### Der Schutzvermerk liegt neben dem Manifest
|
|
|
|
Verlängerung und Legal Hold stehen in `manifests/<id>.hold.json`, nicht im
|
|
Manifest. Das Manifest ist mit seinem `ContentHash` **versiegelt** — es ist der
|
|
Abschlussvermerk des Backups. Ein Feld darin nachträglich zu ändern machte
|
|
entweder den Hash ungültig oder verlangte, ihn neu zu bilden; im zweiten Fall
|
|
wäre das Manifest nicht mehr das, was beim Commit geprüft wurde. Beides
|
|
untergrübe die Eigenschaft, auf der die gesamte Integritätsprüfung beruht.
|
|
|
|
Der Vermerk führt seine eigene Historie mit — wer wann was warum geändert hat.
|
|
Ein Repository muss ohne Control Server deutbar bleiben, also gehört die Frage
|
|
„warum wird das noch gehalten?" ins Repository und nicht nur in die Datenbank.
|
|
|
|
**Ein unlesbarer Schutzvermerk gilt als Schutz**, nicht als dessen Abwesenheit.
|
|
Das ist der gefährlichste Fehlerfall der Phase: Die umgekehrte Auslegung machte
|
|
aus einer beschädigten Datei einen Datenverlust.
|
|
|
|
## Aufbewahrungsregeln
|
|
|
|
Zwei Formen, kombinierbar (PROMPT.md §16):
|
|
|
|
```text
|
|
7 / 14 / 30 / 90 Tage, 1 Jahr → keep_within
|
|
GFS: 30 täglich, 8 wöchentlich, → keep_daily / keep_weekly /
|
|
12 monatlich, 7 jährlich keep_monthly / keep_yearly
|
|
```
|
|
|
|
Die Reihenfolge der Prüfungen ist die Sicherheitsarchitektur: Erst **alle**
|
|
Gründe zu behalten, dann erst die Löschung. Ein Backup fällt nur heraus, wenn
|
|
keine Regel es hält.
|
|
|
|
- **`keep_last` schützt das letzte vorhandene Backup.** Jede mitgelieferte Regel
|
|
enthält es. Ohne diese Ergänzung löschte „7 Tage" bei einem System, das drei
|
|
Wochen nicht gesichert hat, *jedes* Backup — die Regel träfe genau dann zu,
|
|
wenn man die Daten am dringendsten braucht.
|
|
- **Eine Regel ohne jede Haltevorgabe wird abgelehnt**, nicht als „behalte
|
|
nichts" ausgeführt. Der Unterschied ist der zwischen einem Tippfehler und
|
|
einem Datenverlust.
|
|
- Wird `keep_last` bei sonst gültiger Regel weggelassen, ergänzt die API eine 1
|
|
**und sagt es** in der Antwort. Eine stille Korrektur der Eingabe fällt sonst
|
|
erst auf, wenn die Regel etwas anderes tut als gedacht.
|
|
- **Die Zeitzone gehört zur Regel.** Ohne sie liefe die Tageseinteilung in UTC,
|
|
und wer täglich um 00:30 deutscher Zeit sichert, verlöre systematisch die
|
|
falschen Backups.
|
|
- **Der Aufbewahrungsschutz steht über jeder Regel.** Ein fälliges, aber
|
|
geschütztes Backup erscheint im Plan als „wäre fällig, ist aber geschützt" —
|
|
das erklärt, warum der Speicher trotz Aufräumen nicht kleiner wird.
|
|
|
|
### Elternbackups einer Kette
|
|
|
|
Bei Syncova trägt **jedes** Manifest die Blockverweise aller Objekte, auch das
|
|
einer Zusatzsicherung (Phase 6). Ein Elternbackup zu löschen beschädigt das Kind
|
|
nicht. Diese Zusicherung steht seit Phase 11 im Manifest selbst
|
|
(`self_contained_restore`).
|
|
|
|
Der Grund ist ein Fund aus dem Nachweis: Ohne die Angabe wäre in einer
|
|
fortlaufenden Kette **jedes** Backup außer dem jüngsten ein Elternteil — die
|
|
Aufbewahrung räumte nie auf, während der Betreiber glaubt, eine zu haben. Ein
|
|
Manifest ohne das Feld (ältere Läufe, fremde Repositories) bedeutet weiterhin
|
|
„unbekannt" und schützt seine Kette.
|
|
|
|
Der Regressionstest löscht das Elternbackup und liest anschließend jeden Block
|
|
des Kindes — die Behauptung allein genügt nicht.
|
|
|
|
### „Gelöscht, aber nichts frei"
|
|
|
|
Wegen der Deduplizierung gibt ein gelöschtes Backup nur den Speicher frei, den
|
|
kein anderes mehr braucht. Bei wachsenden Daten verweist das jüngste Backup auf
|
|
sämtliche Blöcke der älteren; dann werden Backups gelöscht und **kein Byte**
|
|
frei. Die Zusammenfassung sagt das ausdrücklich, sonst erzeugt jede solche
|
|
Ausgabe eine Rückfrage.
|
|
|
|
## Endpunkte
|
|
|
|
| Methode | Pfad | Recht |
|
|
| --- | --- | --- |
|
|
| GET | `/api/v1/retention-policies` | `repositories.read` |
|
|
| POST | `/api/v1/retention-policies` | `retention.write` |
|
|
| PATCH | `/api/v1/retention-policies/{id}` | `retention.write` |
|
|
| DELETE | `/api/v1/retention-policies/{id}` | `retention.write` |
|
|
| POST | `/api/v1/repositories/{id}/retention/preview` | `repositories.read` |
|
|
| POST | `/api/v1/repositories/{id}/retention/apply` | `retention.write` |
|
|
| POST | `/api/v1/repositories/{id}/enforcement/measure` | `repositories.write` |
|
|
| GET | `/api/v1/backups/{id}/protection` | `backups.read` |
|
|
| POST | `/api/v1/backups/{id}/retention/extend` | `immutability.manage` |
|
|
| POST | `/api/v1/backups/{id}/legal-hold` | `immutability.manage` |
|
|
| DELETE | `/api/v1/backups/{id}/legal-hold` | `immutability.manage` |
|
|
| DELETE | `/api/v1/backups/{id}` | `backups.delete` |
|
|
|
|
**Die Trennung von `backups.delete` und `immutability.manage` ist der Kern der
|
|
Zugriffssteuerung dieser Phase.** Wer aufräumen darf, darf deshalb noch lange
|
|
keinen Aufbewahrungsschutz aufheben: Das Aufheben ist der Schritt, der einem
|
|
Angreifer den Weg öffnet. `immutability.manage` haben nur Super Administrator
|
|
und Security Administrator.
|
|
|
|
### Bestätigungen
|
|
|
|
Zwei Stellen verlangen eine wörtliche Wiederholung:
|
|
|
|
- `DELETE /backups/{id}` — `confirm_backup_id` wiederholt die Kennung des
|
|
Backups im Repository.
|
|
- `POST …/retention/apply` — `confirm_deletion` wiederholt die **Zahl** der zu
|
|
löschenden Backups aus der Vorschau.
|
|
|
|
Löscht ein Lauf nichts, entfällt die Bestätigung: Ein Ritual ohne Anlass gewöhnt
|
|
das Wegklicken an.
|
|
|
|
## Nachgewiesen
|
|
|
|
Gegen den laufenden Dienst mit echten Repositories:
|
|
|
|
| Versuch | Ergebnis |
|
|
| --- | --- |
|
|
| Durchsetzungsstufe messen | `filesystem`, mit beiden Beobachtungen belegt |
|
|
| Backup im gehärteten Repository löschen (Recht + Bestätigung) | 409 — Aufbewahrungsschutz |
|
|
| Löschen ohne / mit falscher Bestätigung | 422 |
|
|
| Legal Hold ohne Begründung | 422 |
|
|
| Frist verkürzen | 422 — „verlängern ja, verkürzen nie" |
|
|
| `rm -rf` gegen das gehärtete Repository | 15 Löschungen verweigert, Backup danach vollständig |
|
|
| Aufbewahrung: 4 Backups, Regel „nur das letzte" | 2 gelöscht, 2 behalten, Tiefenscan sauber |
|
|
| `backup_operator` setzt Legal Hold | 403 |
|
|
| Abgewehrter Löschversuch | `BACKUP_DELETION_DENIED` im Audit |
|
|
|
|
## Bekannte Grenzen
|
|
|
|
- **Kein S3 Object Lock, kein WORM.** Die Stufe `storage` existiert als Begriff
|
|
und wird von der Messung nie vergeben.
|
|
- **Der Schutz gilt gegenüber dem Dienstbenutzer, nicht gegenüber root.** Wer
|
|
Systemverwalter ist, kann das Kennzeichen entfernen. Das steht in jeder
|
|
Erklärung der Stufe `filesystem` — verschwiegen wird es nicht. Ein echter
|
|
Schutz gegen root verlangt ein zweites System (Repository-Appliance mit
|
|
eigenem Benutzerkreis) oder Speicher-WORM.
|
|
- **Aufbewahrung läuft nicht nach Zeitplan.** Sie wird über die API angestoßen;
|
|
eine Anbindung an den Scheduler fehlt. Bis dahin gilt: Wer nicht anwendet,
|
|
räumt nicht auf.
|
|
- **`minimum_retention_seconds` wird gespeichert, aber noch nicht erzwungen.**
|
|
Die Sperre gegen das Absenken der Repository-Frist fehlt; die Frist eines
|
|
einzelnen Backups lässt sich bereits nicht verkürzen.
|