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>
12 KiB
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:
{
"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:
- 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.
- 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):
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_lastschü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_lastbei 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_idwiederholt die Kennung des Backups im Repository.POST …/retention/apply—confirm_deletionwiederholt 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
storageexistiert 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_secondswird 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.