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>
134 lines
4.6 KiB
Go
134 lines
4.6 KiB
Go
package recovery
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestBlocksSystemDirectories ist der Kerntest gegen die Rechteausweitung.
|
|
//
|
|
// Vor Phase 19 galt eine Wiederherstellung nach /etc als durchfuehrbar — real
|
|
// gegen die laufende Anlage nachgewiesen. Der Angriff braucht keine Luecke im
|
|
// Code: Wer Backups zurueckschreiben darf, legt eine Datei in /etc/cron.d oder
|
|
// eine authorized_keys in ein fremdes Benutzerverzeichnis. Aus einem
|
|
// Anwendungsrecht wird ein Systemzugang.
|
|
func TestBlocksSystemDirectories(testInstance *testing.T) {
|
|
targetGuard := NewTargetGuard(nil)
|
|
|
|
blockedTargets := []string{
|
|
"/etc", "/etc/cron.d", "/usr/local/bin", "/bin", "/sbin",
|
|
"/boot/grub", "/var/lib/postgresql", "/System/Library",
|
|
"/private/etc/hosts", "/root/.ssh",
|
|
}
|
|
|
|
for _, target := range blockedTargets {
|
|
if guardError := targetGuard.Validate(target); guardError == nil {
|
|
testInstance.Errorf("das Systemverzeichnis %q wurde als Ziel zugelassen", target)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBlocksFilesystemRoot prueft das Wurzelverzeichnis.
|
|
func TestBlocksFilesystemRoot(testInstance *testing.T) {
|
|
targetGuard := NewTargetGuard(nil)
|
|
|
|
if guardError := targetGuard.Validate("/"); guardError == nil {
|
|
testInstance.Error("das Wurzelverzeichnis wurde als Ziel zugelassen")
|
|
}
|
|
}
|
|
|
|
// TestAllowsOrdinaryTargets prueft die Gegenprobe.
|
|
//
|
|
// Ein Schutz, der auch die legitimen Ziele sperrt, macht die Wiederherstellung
|
|
// unbrauchbar — und wer ihn deswegen abschaltet, hat gar keinen mehr.
|
|
func TestAllowsOrdinaryTargets(testInstance *testing.T) {
|
|
targetGuard := NewTargetGuard(nil)
|
|
|
|
allowedTargets := []string{
|
|
"/srv/wiederherstellung", "/home/benutzer/daten", "/mnt/ziel",
|
|
"/data/restore-2026", testInstance.TempDir(),
|
|
}
|
|
|
|
for _, target := range allowedTargets {
|
|
if guardError := targetGuard.Validate(target); guardError != nil {
|
|
testInstance.Errorf("das gewöhnliche Ziel %q wurde gesperrt: %v", target, guardError)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPrefixMatchingIsNotFooled prueft den Vergleich der Pfade.
|
|
//
|
|
// Ein Vergleich ueber strings.HasPrefix haelt "/etchen" fuer einen Teil von
|
|
// "/etc" und "/var/library" fuer einen Teil von "/var/lib". Der erste Fall
|
|
// sperrt zu viel, und beide zeigen, dass ein Pfadvergleich ueber die
|
|
// Pfadtrennung laufen muss.
|
|
func TestPrefixMatchingIsNotFooled(testInstance *testing.T) {
|
|
targetGuard := NewTargetGuard(nil)
|
|
|
|
lookalikeTargets := []string{"/etchen", "/var/library", "/usrdaten", "/bindungen"}
|
|
|
|
for _, target := range lookalikeTargets {
|
|
if guardError := targetGuard.Validate(target); guardError != nil {
|
|
testInstance.Errorf("das Ziel %q wurde fälschlich gesperrt: %v", target, guardError)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAllowedRootsAreExclusive prueft die konfigurierte Erlaubnisliste.
|
|
//
|
|
// Ist sie gesetzt, gilt ausschliesslich sie: Ein Betreiber, der die Ziele auf
|
|
// ein Verzeichnis begrenzt, will nicht, dass daneben noch der ganze Rest des
|
|
// Dateisystems offen bleibt.
|
|
func TestAllowedRootsAreExclusive(testInstance *testing.T) {
|
|
allowedRoot := testInstance.TempDir()
|
|
targetGuard := NewTargetGuard([]string{allowedRoot})
|
|
|
|
if guardError := targetGuard.Validate(filepath.Join(allowedRoot, "ziel")); guardError != nil {
|
|
testInstance.Errorf("das erlaubte Ziel wurde gesperrt: %v", guardError)
|
|
}
|
|
|
|
// Ein gewoehnliches Verzeichnis ausserhalb der Liste ist jetzt gesperrt,
|
|
// obwohl es kein Systemverzeichnis ist.
|
|
if guardError := targetGuard.Validate("/srv/anderswo"); guardError == nil {
|
|
testInstance.Error("ein Ziel außerhalb der Erlaubnisliste wurde zugelassen")
|
|
}
|
|
}
|
|
|
|
// TestEscapeAttemptsAreResolved prueft Pfade mit Rueckwaertsschritten.
|
|
//
|
|
// "/srv/../etc" ist "/etc". Ohne die Bereinigung liefe der Vergleich gegen die
|
|
// Zeichenkette statt gegen den tatsaechlichen Pfad.
|
|
func TestEscapeAttemptsAreResolved(testInstance *testing.T) {
|
|
targetGuard := NewTargetGuard(nil)
|
|
|
|
escapeAttempts := []string{
|
|
"/srv/../etc",
|
|
"/tmp/../etc/cron.d",
|
|
"/home/benutzer/../../etc",
|
|
}
|
|
|
|
for _, target := range escapeAttempts {
|
|
if guardError := targetGuard.Validate(target); guardError == nil {
|
|
testInstance.Errorf("der Ausbruchsversuch %q wurde zugelassen", target)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestErrorExplainsTheAlternative prueft die Fehlermeldung.
|
|
//
|
|
// Eine Sperre ohne Ausweg ist eine Sackgasse. Wer wirklich ein fremdes /etc
|
|
// zurueckschreiben will, muss erfahren, wie es geht.
|
|
func TestErrorExplainsTheAlternative(testInstance *testing.T) {
|
|
targetGuard := NewTargetGuard(nil)
|
|
|
|
guardError := targetGuard.Validate("/etc/beliebig")
|
|
if guardError == nil {
|
|
testInstance.Fatal("das Systemverzeichnis wurde zugelassen")
|
|
}
|
|
|
|
if !strings.Contains(guardError.Error(), "anderen stelle") {
|
|
testInstance.Errorf("die Meldung nennt keinen Ausweg: %v", guardError)
|
|
}
|
|
}
|