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>
124 lines
4.1 KiB
Go
124 lines
4.1 KiB
Go
package recovery
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// TargetGuard prüft, wohin eine Wiederherstellung schreiben darf
|
|
// (SYNCOVA_IMPLEMENTATION_PLAN.md §21, filesystem permissions).
|
|
//
|
|
// Der Angriff, gegen den diese Prüfung steht, braucht keine Lücke im Code:
|
|
// Jemand mit dem Recht, Backups zurückzuschreiben, gibt als Ziel `/etc` an. Der
|
|
// nächste Lauf schreibt dann eine Datei nach `/etc/cron.d/` oder eine
|
|
// `authorized_keys` in ein fremdes Benutzerverzeichnis — und aus einem
|
|
// Anwendungsrecht ist ein Systemzugang geworden.
|
|
//
|
|
// Die Prüfung ersetzt **nicht** die Rechte des Dienstbenutzers. Sie ist die
|
|
// zweite Linie: Ein Dienst, der als eigener, unprivilegierter Benutzer läuft,
|
|
// käme ohnehin nicht nach /etc. Nur verlässt sich niemand gern darauf, dass
|
|
// jede Installation das richtig gemacht hat.
|
|
type TargetGuard struct {
|
|
// allowedRoots sind die ausdrücklich erlaubten Wurzelverzeichnisse.
|
|
//
|
|
// Ist die Liste gesetzt, gilt ausschließlich sie: Ein Ziel muss unterhalb
|
|
// eines dieser Verzeichnisse liegen.
|
|
allowedRoots []string
|
|
}
|
|
|
|
// NewTargetGuard erzeugt die Prüfung.
|
|
func NewTargetGuard(allowedRoots []string) *TargetGuard {
|
|
cleanedRoots := make([]string, 0, len(allowedRoots))
|
|
|
|
for _, allowedRoot := range allowedRoots {
|
|
trimmedRoot := strings.TrimSpace(allowedRoot)
|
|
if trimmedRoot == "" {
|
|
continue
|
|
}
|
|
|
|
absoluteRoot, absoluteError := filepath.Abs(trimmedRoot)
|
|
if absoluteError != nil {
|
|
continue
|
|
}
|
|
|
|
cleanedRoots = append(cleanedRoots, filepath.Clean(absoluteRoot))
|
|
}
|
|
|
|
return &TargetGuard{allowedRoots: cleanedRoots}
|
|
}
|
|
|
|
// blockedSystemRoots sind die Verzeichnisse des laufenden Systems.
|
|
//
|
|
// Sie sind gesperrt, auch wenn keine Erlaubnisliste konfiguriert ist: Eine
|
|
// Wiederherstellung dorthin überschreibt das System, auf dem die Anlage selbst
|
|
// läuft. Wer ein fremdes /etc zurückschreiben will, entpackt es an eine andere
|
|
// Stelle und kopiert es von dort — ein Zwischenschritt, der einen Blick
|
|
// erzwingt.
|
|
var blockedSystemRoots = []string{
|
|
"/bin", "/boot", "/dev", "/etc", "/lib", "/lib32", "/lib64", "/libx32",
|
|
"/proc", "/root", "/sbin", "/sys", "/usr", "/var/lib", "/var/run",
|
|
"/System", "/Library", "/Applications", "/private/etc", "/private/var/db",
|
|
}
|
|
|
|
// Validate prüft ein Zielverzeichnis.
|
|
func (guard *TargetGuard) Validate(targetPath string) error {
|
|
absoluteTarget, absoluteError := filepath.Abs(strings.TrimSpace(targetPath))
|
|
if absoluteError != nil {
|
|
return fmt.Errorf("der zielpfad ist unlesbar: %w", absoluteError)
|
|
}
|
|
|
|
absoluteTarget = filepath.Clean(absoluteTarget)
|
|
|
|
// Das Wurzelverzeichnis selbst ist niemals ein Ziel.
|
|
if absoluteTarget == string(filepath.Separator) {
|
|
return fmt.Errorf("das wurzelverzeichnis ist als ziel nicht zulaessig")
|
|
}
|
|
|
|
// Eine gesetzte Erlaubnisliste ist abschliessend.
|
|
if len(guard.allowedRoots) > 0 {
|
|
for _, allowedRoot := range guard.allowedRoots {
|
|
if isWithinDirectory(absoluteTarget, allowedRoot) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("das ziel %q liegt ausserhalb der erlaubten verzeichnisse (%s)",
|
|
absoluteTarget, strings.Join(guard.allowedRoots, ", "))
|
|
}
|
|
|
|
for _, blockedRoot := range blockedSystemRoots {
|
|
if isWithinDirectory(absoluteTarget, blockedRoot) {
|
|
return fmt.Errorf("das ziel %q liegt in einem systemverzeichnis (%s). eine "+
|
|
"wiederherstellung dorthin ueberschreibt das laufende system. legen sie die "+
|
|
"daten an einer anderen stelle ab und uebernehmen sie sie von dort",
|
|
absoluteTarget, blockedRoot)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// isWithinDirectory meldet, ob ein Pfad in einem Verzeichnis liegt.
|
|
//
|
|
// Der Vergleich geht über die Pfadtrennung, nicht über strings.HasPrefix: Sonst
|
|
// gälte "/etchen" als innerhalb von "/etc", und "/var/library" als innerhalb
|
|
// von "/var/lib".
|
|
func isWithinDirectory(candidatePath string, directoryPath string) bool {
|
|
if candidatePath == directoryPath {
|
|
return true
|
|
}
|
|
|
|
relativePath, relativeError := filepath.Rel(directoryPath, candidatePath)
|
|
if relativeError != nil {
|
|
return false
|
|
}
|
|
|
|
if relativePath == "." {
|
|
return true
|
|
}
|
|
|
|
// Beginnt der relative Pfad mit "..", liegt das Ziel ausserhalb.
|
|
return !strings.HasPrefix(relativePath, "..")
|
|
}
|