Harden remote verification against environment overrides and marker normalization
This commit is contained in:
1 parent
0ec0d3b194
commit
3374019ad6
6 files changed
+248
-8
No files matched your search
+56
-3
@@ -181,6 +181,56 @@ sudo rclone --config /root/.config/rclone/rclone.conf lsd OneDriveCrypt:
|
|||||||
|
|
||||||
`rclone check --download` vergleicht die heruntergeladenen Inhalte und benötigt keinen gemeinsamen Backend-Hash. Es verändert die geprüften Dateien nicht. Das Paket verwendet es vor dem abschließenden Marker. Quelle: [rclone check](https://rclone.org/commands/rclone_check/).
|
`rclone check --download` vergleicht die heruntergeladenen Inhalte und benötigt keinen gemeinsamen Backend-Hash. Es verändert die geprüften Dateien nicht. Das Paket verwendet es vor dem abschließenden Marker. Quelle: [rclone check](https://rclone.org/commands/rclone_check/).
|
||||||
|
|
||||||
|
Exportierte `RCLONE_*`-Variablen sind ausschließlich für die vom Paket ausdrücklich
|
||||||
|
unterstützten Einstellungen und `RCLONE_CONFIG_PASS` erlaubt. Andere Optionen
|
||||||
|
werden vor dem ersten rclone-Aufruf und vor dem Writer-Stopp mit ihrem Namen,
|
||||||
|
ohne ihren Wert, abgewiesen. Insbesondere kann `RCLONE_SIZE_ONLY=true` in rclone
|
||||||
|
auch bei `check --download` den Inhaltsvergleich überspringen. Ein Regressionstest
|
||||||
|
hat diesen Fehler vor der Härtung mit einer veränderten Remote-Datei gleicher Größe
|
||||||
|
reproduziert. Zusätzliche Filter, Dry-run- und TLS-Overrides aus der Umgebung werden
|
||||||
|
ebenfalls abgewiesen. Einstellungen über die dokumentierte Backup-Konfiguration
|
||||||
|
und Remote-Definitionen verwalten; eine absichtlich manipulierte root-eigene
|
||||||
|
Bash-Konfiguration liegt außerhalb dieser Absicherung.
|
||||||
|
Quelle: [rclone 1.74.3 Inhaltsprüfung](https://github.com/rclone/rclone/blob/v1.74.3/fs/operations/check.go).
|
||||||
|
|
||||||
|
### Upload-Fehlerbehandlung und Betriebsgrenzen
|
||||||
|
|
||||||
|
| Ereignis | Verhalten und Konsequenz |
|
||||||
|
| --- | --- |
|
||||||
|
| Vorübergehender Upload-Fehler | rclone wiederholt den Kopierauftrag bis zu `RCLONE_RETRIES=6` Versuche, soweit der Fehler wiederholbar ist; Wartezeit standardmäßig `RCLONE_RETRIES_SLEEP=30s`, zusätzlich gegebenenfalls Provider-Backoff. Kein erneuter DB-Dump oder Writer-Stopp innerhalb dieser Versuche. |
|
||||||
|
| Upload endgültig fehlgeschlagen | Fehlerexit; lokaler vollständiger Satz und Staging bleiben erhalten; kein neuer Remote-Abschlussmarker und keine Retention in diesem Lauf. |
|
||||||
|
| Rückleseprüfung findet andere oder fehlende Inhalte | Fehlerexit; keine Veröffentlichung des Abschlussmarkers. Auch gleich große Dateien werden inhaltlich verglichen. |
|
||||||
|
| Abschlussmarker kann nicht geschrieben oder bestätigt werden | Fehlerexit; kein lokales `UPLOADED`, kein neuer Erfolgsstatus, keine Retention. Der Vergleich prüft die exakten Bytes einschließlich abschließender Zeilenumbrüche, passend zum strikten Restore-Parser. Ist der Marker schon geschrieben und nur seine Bestätigung fehlgeschlagen, kann der Remote-Satz vollständig vorliegen; der Lauf meldet konservativ trotzdem einen Fehler. |
|
||||||
|
| Wiederholungen erfolgreich und Rücklesen bestanden | Remote-Marker wird zuletzt geschrieben und zurückgelesen; erst danach gilt der Upload als erfolgreich. Ein früheres `ERROR` im Log allein ist kein Endergebnis. |
|
||||||
|
| Prozess hängt oder wird abgebrochen | systemd begrenzt den regulären Backup-Dienst auf 12 Stunden, den Update-Auftrag auf 24 Stunden, mit 5 Minuten Stop-Frist. Direkte CLI-/Foreground-Aufrufe besitzen diese systemd-Gesamtfrist nicht. |
|
||||||
|
|
||||||
|
HTTP 409 benötigt nach dem beobachteten erfolgreichen Wiederholungsversuch keinen
|
||||||
|
zusätzlichen allgemeinen Retry-Wrapper. Dieser würde Versuche vervielfachen und
|
||||||
|
dauerhafte Fehler länger verdecken. Der Auslöser des OneDrive-Konflikts bleibt
|
||||||
|
unbekannt. Häufen sich Konflikte, den betroffenen Objektnamen, Zeitpunkte, rclone-Version,
|
||||||
|
andere Zugriffe auf den Backup-Ordner und die vollständigen Versuchsergebnisse prüfen.
|
||||||
|
Keine automatischen Remote-Löschungen oder Überschreibungen zur Konfliktbehebung.
|
||||||
|
`--immutable` schützt vor Änderungen durch diesen Kopierauftrag, ist aber kein
|
||||||
|
serverseitiger WORM-/Ransomware-Schutz gegen andere berechtigte Zugriffe.
|
||||||
|
|
||||||
|
Die sechs Gesamtversuche gelten nicht pauschal für jeden rclone-Befehl:
|
||||||
|
`check --download` verwendet in rclone 1.74.3 keine Wiederholung des ganzen Befehls,
|
||||||
|
jedoch Wiederholungen fehlgeschlagener Datei-Leseoperationen. Ein verbleibender
|
||||||
|
Prüffehler beendet das Backup sichtbar. Quellen:
|
||||||
|
[Kopierversuche](https://github.com/rclone/rclone/blob/v1.74.3/cmd/cmd.go),
|
||||||
|
[Check-Aufruf](https://github.com/rclone/rclone/blob/v1.74.3/cmd/check/check.go),
|
||||||
|
[Immutable](https://rclone.org/docs/#immutable).
|
||||||
|
|
||||||
|
Ein automatischer späterer Upload eines bereits lokal gesicherten, aber nicht
|
||||||
|
veröffentlichten Satzes ist bislang nicht implementiert. Nach ausgeschöpften
|
||||||
|
Versuchen bleibt er zur gezielten Prüfung erhalten; der nächste Timer-Lauf
|
||||||
|
erstellt einen neuen Satz. Wiederholte Fehler können deshalb lokalen Speicher
|
||||||
|
belegen. Erfolgsalter, Fehlerexit, Speicher und Recovery-Dateien unabhängig
|
||||||
|
überwachen; eine erfolgreiche Mailübergabe an den lokalen Mailserver beweist noch
|
||||||
|
keine Zustellung. Eine fachliche Restore-Probe und separat gesicherte Zugangsdaten
|
||||||
|
sind weiterhin erforderlich. Der erfolgreiche Paketlauf allein rechtfertigt keine
|
||||||
|
uneingeschränkte Zusage für beliebige Serverkonfigurationen oder Katastrophenfälle.
|
||||||
|
|
||||||
### Datenbanken und Writer
|
### Datenbanken und Writer
|
||||||
|
|
||||||
Es wird je aktivierter Anwendung ein Dump benötigt, sofern `ENABLE_DB_DUMPS=true`. Fehlende DB-Namen oder CNF-Dateien brechen ab. Der Dump enthält Routinen, Events, Trigger und Binärdaten. Leere Ausgaben sowie Fehler beim Schreiben werden abgelehnt.
|
Es wird je aktivierter Anwendung ein Dump benötigt, sofern `ENABLE_DB_DUMPS=true`. Fehlende DB-Namen oder CNF-Dateien brechen ab. Der Dump enthält Routinen, Events, Trigger und Binärdaten. Leere Ausgaben sowie Fehler beim Schreiben werden abgelehnt.
|
||||||
@@ -298,7 +348,7 @@ sudo cat /var/backups/app-backup/last-successful-backup.json
|
|||||||
systemctl list-timers app-backup.timer
|
systemctl list-timers app-backup.timer
|
||||||
```
|
```
|
||||||
|
|
||||||
Exit `0` bedeutet, dass die ausgeführten erforderlichen Schritte erfolgreich waren. Jeder Fehlercode ist ein Fehler; `130`/`143` kennzeichnen INT/TERM. `last-backup.json` zeigt den letzten gestarteten operativen Lauf, `last-successful-backup.json` wird ausschließlich nach einem vollständig erfolgreichen Backup aktualisiert. `--check` aktualisiert diese Erfolgsmarkierung nicht. Sehr frühe Fehler vor der Laufinitialisierung/Sperrübernahme können nur auf stderr bzw. im Journal erscheinen; deshalb sowohl Servicefehler als auch Alter des letzten Erfolgs überwachen.
|
Exit `0` bedeutet, dass die ausgeführten erforderlichen Schritte erfolgreich waren. Jeder Fehlercode ist ein Fehler; `130`/`143` kennzeichnen INT/TERM. `last-backup.json` wird beim Beenden geschrieben und zeigt während eines laufenden Backups noch den vorherigen beendeten Lauf. `last-successful-backup.json` wird ausschließlich nach einem vollständig erfolgreichen Backup aktualisiert. `--check` aktualisiert diese Erfolgsmarkierung nicht. Sehr frühe Fehler vor der Laufinitialisierung/Sperrübernahme können nur auf stderr bzw. im Journal erscheinen; deshalb sowohl Servicefehler als auch Alter des letzten Erfolgs überwachen. Bei `ENABLE_UPLOAD=false` belegt ein Erfolg nur eine lokale Sicherung; für Offsite-Überwachung zusätzlich die Upload-Konfiguration und `UPLOADED` des gemeldeten Satzes berücksichtigen.
|
||||||
|
|
||||||
Für den täglichen Zeitplan z. B. bei mehr als 36 Stunden ohne erfolgreichen Lauf alarmieren, zusätzlich freien Speicher, offene Recovery-Dateien und wachsende Partial-Verzeichnisse prüfen. Diese Überwachung läuft idealerweise außerhalb des gesicherten Servers. `TimeoutStartSec=12h` und `TimeoutStopSec=5min` sind Ausgangswerte und müssen zum ersten gemessenen Vollbackup plus Rückleseprüfung passen.
|
Für den täglichen Zeitplan z. B. bei mehr als 36 Stunden ohne erfolgreichen Lauf alarmieren, zusätzlich freien Speicher, offene Recovery-Dateien und wachsende Partial-Verzeichnisse prüfen. Diese Überwachung läuft idealerweise außerhalb des gesicherten Servers. `TimeoutStartSec=12h` und `TimeoutStopSec=5min` sind Ausgangswerte und müssen zum ersten gemessenen Vollbackup plus Rückleseprüfung passen.
|
||||||
|
|
||||||
@@ -433,12 +483,15 @@ Die Entfernung der drei DB-Dateien aus dem aktuellen Baum löscht deren Historie
|
|||||||
|
|
||||||
Reproduzierbar mit `tests/Dockerfile` und `tests/run.sh`. Der Container bekommt das Repository nur lesbar und benötigt während der Tests kein Netzwerk. Die MariaDB läuft ohne TCP-Port in einem temporären Verzeichnis. systemd/occ werden simuliert; alle eigentlichen Archive, Dateikopien, Dumps/Imports und Crypt-Operationen sind real.
|
Reproduzierbar mit `tests/Dockerfile` und `tests/run.sh`. Der Container bekommt das Repository nur lesbar und benötigt während der Tests kein Netzwerk. Die MariaDB läuft ohne TCP-Port in einem temporären Verzeichnis. systemd/occ werden simuliert; alle eigentlichen Archive, Dateikopien, Dumps/Imports und Crypt-Operationen sind real.
|
||||||
|
|
||||||
Verifikation am 10.09.2026: **52 Testfälle erfolgreich** — 50 im vollständigen Durchlauf, anschließend beide zusätzlichen Abbruchtests für Kopie und Upload auf dem ergänzten Stand. Shell-Syntax und ShellCheck abschließend ohne Befund. Testumgebung: Debian 13.6, MariaDB 11.8.6, rclone 1.60.1-DEV (Distributionspaket), ShellCheck 0.10.0. Diese Versionsangaben beschreiben die Testumgebung, keine Empfehlung für die Produktivversion.
|
Verifikation am 10.09.2026: **59 Testfälle erfolgreich** — 58 im vollständigen Durchlauf (524,6 Sekunden), dazu der ergänzte Test für zusätzliche Marker-Zeilenumbrüche. Die vier betroffenen Markertests und der echte Crypt-Upload mit Remote-Restore wurden nach der Marker-Korrektur nochmals gezielt erfolgreich ausgeführt. Shell-Syntax und ShellCheck ohne Befund. Testumgebung: Debian 13.6, MariaDB 11.8.6, rclone 1.60.1-DEV (Distributionspaket), ShellCheck 0.10.0. Diese Versionsangaben beschreiben die Testumgebung, keine Empfehlung für die Produktivversion.
|
||||||
|
|
||||||
Für die anschließende Ergänzung der Upload-Statistiken wurden drei betroffene Tests erneut erfolgreich ausgeführt: echter gedrosselter Crypt-Upload samt Remote-Restore, fehlgeschlagene Rückleseprüfung ohne Remote-Abschlussmarker und TERM während eines Uploads mit gestoppt bleibendem Timer. Shell-Syntax und ShellCheck bestanden ebenfalls erneut. Der vollständige 52-Fälle-Umfang wurde für diese Protokollergänzung nicht nochmals durchlaufen.
|
Die Regressionstests für `RCLONE_SIZE_ONLY` und zusätzliche Marker-Zeilenumbrüche scheiterten vor den jeweiligen Korrekturen mit einem fälschlich erfolgreichen Backup und bestanden danach. Upload-/Markerfehler werden an den Befehlsgrenzen injiziert, während Crypt-Übertragung und Inhaltsvergleich real bleiben. Die Tests bilden keine OneDrive-API nach; das tatsächliche HTTP-409-Wiederholungsverhalten ist durch den Betreiberlauf und den rclone-1.74.3-Quellcode separat belegt.
|
||||||
|
|
||||||
Geprüfte Fälle umfassen:
|
Geprüfte Fälle umfassen:
|
||||||
|
|
||||||
|
- Endgültiger Upload-Fehler trotz bereits übertragener Bytes: vollständiger lokaler Satz, vorheriger Erfolgsstatus und alte lokale/entfernte Retention-Kandidaten bleiben erhalten.
|
||||||
|
- Veränderte Remote-Datei gleicher Größe, Marker-Schreibfehler, abweichender Marker-Inhalt und zusätzliche Zeilenumbrüche verhindern den lokalen Upload-Erfolg und die Retention.
|
||||||
|
- Nicht unterstützte rclone-Umgebungsoptionen werden vor Remote-Zugriff und Writer-Stopp abgewiesen; unterstützte exportierte Einstellungen bleiben nutzbar.
|
||||||
- Periodische NOTICE-Statistiken bei gedrosseltem echten Crypt-Upload, erfolgreicher Rückleseprüfung und Remote-Restore; die Statistik verändert den maschinell gelesenen Abschlussmarker nicht.
|
- Periodische NOTICE-Statistiken bei gedrosseltem echten Crypt-Upload, erfolgreicher Rückleseprüfung und Remote-Restore; die Statistik verändert den maschinell gelesenen Abschlussmarker nicht.
|
||||||
- Fremde Startversuche während realer rsync-Kopien werden durch das systemd-Testmodell übersprungen; zuvor inaktive Writer bleiben nach dem Lauf inaktiv.
|
- Fremde Startversuche während realer rsync-Kopien werden durch das systemd-Testmodell übersprungen; zuvor inaktive Writer bleiben nach dem Lauf inaktiv.
|
||||||
- Aktive sowie zwischenzeitlich gestartete und wieder gestoppte Writer verhindern die Veröffentlichung eines vollständigen Satzes.
|
- Aktive sowie zwischenzeitlich gestartete und wieder gestoppte Writer verhindern die Veröffentlichung eines vollständigen Satzes.
|
||||||
|
|||||||
@@ -4,6 +4,30 @@ Sichert WordPress, Nextcloud, native Gitea-Installationen mit MariaDB, Maildaten
|
|||||||
|
|
||||||
Die vollständige Analyse, Architektur, Konfiguration, Betriebsanleitung und verbleibenden Grenzen stehen in [Dokumentation.md](Dokumentation.md).
|
Die vollständige Analyse, Architektur, Konfiguration, Betriebsanleitung und verbleibenden Grenzen stehen in [Dokumentation.md](Dokumentation.md).
|
||||||
|
|
||||||
|
## Produktionsstand und verbleibende Abnahme
|
||||||
|
|
||||||
|
Auf Gemini bestand am 10.09.2026 ein vollständiger Paketlauf mit geschützter
|
||||||
|
Dateikopie, DB-Dumps, OneDrive-Upload, vollständigem Rücklesen und separater
|
||||||
|
Archivprüfung (14 Archive). Dienste und Backup-Timer wurden anschließend gestartet.
|
||||||
|
Das belegt die technische Abnahme für die konfigurierte Umgebung. Eine fachliche
|
||||||
|
Wiederherstellung mit funktionierenden Anwendungen auf einem Testsystem sowie
|
||||||
|
unabhängige Alarmierung und separat verfügbare Crypt-Schlüssel bleiben Voraussetzungen
|
||||||
|
für einen belastbaren Produktionsbetrieb; ihre Abnahme ist noch nicht bestätigt.
|
||||||
|
|
||||||
|
Ein OneDrive-HTTP-409 wurde im zweiten Upload-Versuch überwunden. Der Code erlaubt
|
||||||
|
standardmäßig bis zu sechs Kopierversuche und veröffentlicht den Remote-Abschlussmarker
|
||||||
|
erst nach erfolgreichem Inhaltsvergleich. Wiederkehrende Konflikte oder endgültige
|
||||||
|
Fehler müssen untersucht werden; ein aktiver Timer allein belegt kein aktuelles Backup.
|
||||||
|
Details: [Fehlerbehandlung und Betriebsgrenzen](Dokumentation.md#upload-fehlerbehandlung-und-betriebsgrenzen).
|
||||||
|
|
||||||
|
Der aktuelle Stand ergänzt sichtbaren Fortschritt und lehnt nicht unterstützte
|
||||||
|
exportierte `RCLONE_*`-Optionen ab, die etwa die Inhaltsprüfung oder Dateiauswahl
|
||||||
|
verändern könnten. Diese Ergänzungen sind erst nach Installation des aktuellen
|
||||||
|
Pakets auf dem Server wirksam; der erfolgreiche ältere Paketlauf installiert sie
|
||||||
|
nicht nachträglich.
|
||||||
|
Der Abschlussmarker wird außerdem bytegenau bestätigt, einschließlich seiner
|
||||||
|
Zeilenumbrüche, damit Backup und Restore dieselbe strikte Prüfung verwenden.
|
||||||
|
|
||||||
## Automatisches Update-Paket (empfohlen)
|
## Automatisches Update-Paket (empfohlen)
|
||||||
|
|
||||||
Das neue Paket übernimmt Konfigurationsabgleich, Rechteprüfung, Installation,
|
Das neue Paket übernimmt Konfigurationsabgleich, Rechteprüfung, Installation,
|
||||||
|
|||||||
+16
-2
@@ -17,6 +17,21 @@ Backups mit zeitweiliger Unterbrechung der konfigurierten Anwendungen.
|
|||||||
|
|
||||||
## Was automatisch passiert
|
## Was automatisch passiert
|
||||||
|
|
||||||
|
Nach dem erfolgreichen Gemini-Lauf wurden Fortschrittsmeldungen, eine Prüfung
|
||||||
|
exportierter rclone-Optionen und der bytegenaue Vergleich des Abschlussmarkers
|
||||||
|
ergänzt. Für diese Härtung das aktuelle Paket im
|
||||||
|
vereinbarten Wartungsfenster über denselben Installer installieren; die bestehenden
|
||||||
|
Zugangsdaten und der Zeitplan bleiben erhalten. Der Installer führt erneut ein
|
||||||
|
vollständiges Abnahmebackup durch. Die technische Abnahme des bisherigen Satzes
|
||||||
|
bleibt gültig; der einmalige, überwundene HTTP-409 allein erfordert keinen Neustart.
|
||||||
|
|
||||||
|
Falls die neue Vorprüfung `Unsupported rclone environment override` meldet,
|
||||||
|
die genannte Einstellung in Shell oder systemd-Umgebung prüfen und entfernen
|
||||||
|
beziehungsweise in eine unterstützte Backup-Einstellung überführen. Keine
|
||||||
|
Optionswerte oder Zugangsdaten ins Protokoll kopieren. Globale rclone-Filter und
|
||||||
|
`RCLONE_SIZE_ONLY` können Sicherungsumfang beziehungsweise Inhaltsprüfung
|
||||||
|
abschwächen und sind im Backup-Prozess nicht unterstützt.
|
||||||
|
|
||||||
1. Paketinhalt per SHA-256 prüfen und geschützt unter `/var/tmp` entpacken.
|
1. Paketinhalt per SHA-256 prüfen und geschützt unter `/var/tmp` entpacken.
|
||||||
2. Bestehende Einstellungen nach `/var/backups/app-backup-upgrade-XXXXXXXX` sichern.
|
2. Bestehende Einstellungen nach `/var/backups/app-backup-upgrade-XXXXXXXX` sichern.
|
||||||
3. Aktuelle Pfade und Einstellungen übernehmen; sichere Betriebsoptionen ergänzen.
|
3. Aktuelle Pfade und Einstellungen übernehmen; sichere Betriebsoptionen ergänzen.
|
||||||
@@ -418,8 +433,7 @@ Das ausführbare Paket ausschließlich über euren vertrauenswürdigen Kanal bez
|
|||||||
|
|
||||||
## Verifikation der Lieferung
|
## Verifikation der Lieferung
|
||||||
|
|
||||||
52 Testfälle erfolgreich: vollständiger Durchlauf mit 50 Tests, anschließend die zwei zusätzlichen Abbruchtests für Kopie und Upload. ShellCheck und Shell-Syntaxprüfung abschließend erfolgreich.
|
59 Testfälle erfolgreich: 58 im vollständigen Durchlauf (524,6 Sekunden), dazu der neue Test für zusätzliche Marker-Zeilenumbrüche. Nach der Marker-Korrektur bestanden die vier betroffenen Markertests und der echte Crypt-Upload mit Remote-Restore nochmals gezielt. ShellCheck und Shell-Syntaxprüfung erfolgreich. Die Tests für eine über Umgebungsvariablen abgeschwächte Inhaltsprüfung und den nicht bytegenauen Marker-Vergleich reproduzierten die Fehler vor der Korrektur und bestanden danach.
|
||||||
Für die danach ergänzten Upload-Statistiken bestanden drei gezielt wiederholte Tests: gedrosselter echter Crypt-Upload mit Remote-Restore, fehlgeschlagene Rückleseprüfung ohne Abschlussmarker und TERM während des Uploads. ShellCheck und Shell-Syntax bestanden erneut; kein erneuter vollständiger Durchlauf für diese Protokollergänzung.
|
|
||||||
|
|
||||||
Die Tests enthalten echte MariaDB-Dumps/Imports und lokale rclone-Crypt-Roundtrips;
|
Die Tests enthalten echte MariaDB-Dumps/Imports und lokale rclone-Crypt-Roundtrips;
|
||||||
systemd/occ und die systemd-run-Übergabe sind simuliert. Das Übergabemodell
|
systemd/occ und die systemd-run-Übergabe sind simuliert. Das Übergabemodell
|
||||||
|
|||||||
@@ -254,6 +254,20 @@ check_remote() {
|
|||||||
for key in ${!RCLONE_CONFIG_@}; do
|
for key in ${!RCLONE_CONFIG_@}; do
|
||||||
[[ $key == RCLONE_CONFIG_PASS ]] || die "Remote overrides through $key are unsupported; use the config file"
|
[[ $key == RCLONE_CONFIG_PASS ]] || die "Remote overrides through $key are unsupported; use the config file"
|
||||||
done
|
done
|
||||||
|
# rclone reads global flags from exported RCLONE_* variables too. For example,
|
||||||
|
# RCLONE_SIZE_ONLY bypasses byte comparison even with check --download; filters
|
||||||
|
# can hide missing archives from both copy and check. Refuse such ambient flags
|
||||||
|
# before the first remote operation, rather than silently weakening verification.
|
||||||
|
# Allow only our explicit settings (passed as CLI flags) and config decryption.
|
||||||
|
while IFS= read -r key; do
|
||||||
|
[[ $key == RCLONE_* ]] || continue
|
||||||
|
case $key in
|
||||||
|
RCLONE_BIN|RCLONE_CONFIG|RCLONE_CONFIG_PASS|RCLONE_REMOTE_BASE|RCLONE_RETRIES|\
|
||||||
|
RCLONE_LOW_LEVEL_RETRIES|RCLONE_RETRIES_SLEEP|RCLONE_TIMEOUT|RCLONE_CONTIMEOUT|\
|
||||||
|
RCLONE_STATS_INTERVAL|RCLONE_TRANSFERS|RCLONE_CHECKERS|RCLONE_ONEDRIVE_CHUNK_SIZE|RCLONE_BWLIMIT) ;;
|
||||||
|
*) die "Unsupported rclone environment override: $key; use documented app-backup settings and the remote config" ;;
|
||||||
|
esac
|
||||||
|
done < <(compgen -e)
|
||||||
# Pipe directly into the parser: compatibility with older rclone without
|
# Pipe directly into the parser: compatibility with older rclone without
|
||||||
# config redacted. Secret values never go to the log or a temporary file.
|
# config redacted. Secret values never go to the log or a temporary file.
|
||||||
"$RCLONE_BIN" --ask-password=false --config "$RCLONE_CONFIG" config show "$name" | helper crypt "$name"
|
"$RCLONE_BIN" --ask-password=false --config "$RCLONE_CONFIG" config show "$name" | helper crypt "$name"
|
||||||
|
|||||||
+129
-1
@@ -1,5 +1,6 @@
|
|||||||
"""Integration tests use real tar/rsync/rclone crypt and a real isolated MariaDB.
|
"""Integration tests use real tar/rsync/rclone crypt and a real isolated MariaDB.
|
||||||
Only systemd/occ are simulated: production services never receive test commands.
|
systemd/occ are simulated; error paths inject failures around real commands.
|
||||||
|
Production services never receive test commands.
|
||||||
"""
|
"""
|
||||||
import gzip
|
import gzip
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -971,6 +972,133 @@ if '--no-block' not in args:
|
|||||||
proc=subprocess.run(['/usr/bin/rclone','--config',str(self.base/'rclone.conf'),'cat',f'crypt:sets/{run.name}/COMPLETE'],capture_output=True)
|
proc=subprocess.run(['/usr/bin/rclone','--config',str(self.base/'rclone.conf'),'cat',f'crypt:sets/{run.name}/COMPLETE'],capture_output=True)
|
||||||
self.assertNotEqual(proc.returncode,0)
|
self.assertNotEqual(proc.returncode,0)
|
||||||
|
|
||||||
|
def remote_failure(self, mode, **env):
|
||||||
|
# Keep real crypt transfers. Inject errors at the command boundary, not
|
||||||
|
# a pretend OneDrive server: provider retry behaviour is checked separately.
|
||||||
|
wrapper = self.bin/'rclone'
|
||||||
|
wrapper.write_text('''#!/usr/bin/env python3
|
||||||
|
import os, subprocess, sys
|
||||||
|
args=sys.argv[1:]
|
||||||
|
mode=os.environ.get('TEST_REMOTE_FAILURE','')
|
||||||
|
def rc(arguments, **kw):
|
||||||
|
return subprocess.run(['/usr/bin/rclone',*arguments], **kw)
|
||||||
|
if 'copyto' in args and mode=='marker-write':
|
||||||
|
sys.exit(44)
|
||||||
|
if 'cat' in args and mode=='marker-read':
|
||||||
|
print('incorrect completion marker')
|
||||||
|
sys.exit(0)
|
||||||
|
if 'cat' in args and mode=='marker-newline':
|
||||||
|
data=rc(args,capture_output=True,check=True).stdout
|
||||||
|
sys.stdout.buffer.write(data+b'\\n')
|
||||||
|
sys.exit(0)
|
||||||
|
result=rc(args)
|
||||||
|
if result.returncode:
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
if 'copy' in args:
|
||||||
|
if mode=='upload':
|
||||||
|
# Models an unsuccessful command even though the remote received bytes.
|
||||||
|
sys.exit(44)
|
||||||
|
if mode=='corrupt':
|
||||||
|
config=args[args.index('--config')+1]
|
||||||
|
remote=args[args.index('copy')+2]+'/wordpress.tar.zst'
|
||||||
|
prefix=['--config',config]
|
||||||
|
data=bytearray(rc([*prefix,'cat',remote],capture_output=True,check=True).stdout)
|
||||||
|
data[len(data)//2]^=1
|
||||||
|
# Same-size plaintext corruption, re-encrypted through the real crypt backend.
|
||||||
|
# The backup must read contents back, not accept a matching size alone.
|
||||||
|
rc([*prefix,'rcat',remote],input=bytes(data),check=True)
|
||||||
|
''')
|
||||||
|
wrapper.chmod(0o755)
|
||||||
|
return self.run_script('app-backup.sh',ok=False,TEST_REMOTE_FAILURE=mode,**env)
|
||||||
|
|
||||||
|
def assert_remote_failure_retained(self, result, marker_present=False):
|
||||||
|
status=json.loads((self.base/'work/last-backup.json').read_text())
|
||||||
|
self.assertNotEqual(status['exit_code'],0)
|
||||||
|
run=self.base/'work/archives'/status['run']
|
||||||
|
HELPER.load_set(run) # Local hashes/manifest must still be valid.
|
||||||
|
self.assertFalse((run/'UPLOADED').exists())
|
||||||
|
self.assertEqual(json.loads(self.state.read_text()),self.initial)
|
||||||
|
self.assertFalse(list(self.runtime.glob('app-backup-guard-*')))
|
||||||
|
self.assertNotIn('-- Local retention:',result.stdout)
|
||||||
|
self.assertNotIn('-- Remote retention:',result.stdout)
|
||||||
|
marker=subprocess.run(['/usr/bin/rclone','--config',str(self.base/'rclone.conf'),
|
||||||
|
'cat',f'crypt:sets/{run.name}/COMPLETE'],capture_output=True)
|
||||||
|
self.assertEqual(marker.returncode==0,marker_present)
|
||||||
|
return run
|
||||||
|
|
||||||
|
def test_upload_failure_preserves_previous_success_and_retention_candidates(self):
|
||||||
|
self.crypt_config()
|
||||||
|
current=self.backup()
|
||||||
|
previous=(self.base/'work/last-successful-backup.json').read_bytes()
|
||||||
|
old=[]
|
||||||
|
for day in range(1,4):
|
||||||
|
name=f'appbackup_2020-01-0{day}_00-00-00_aaaaaaaa'
|
||||||
|
path=current.parent/name
|
||||||
|
shutil.copytree(current,path)
|
||||||
|
manifest=json.loads((path/'manifest.json').read_text())
|
||||||
|
manifest['run_id']=name
|
||||||
|
(path/'manifest.json').write_text(json.dumps(manifest))
|
||||||
|
(path/'COMPLETE').write_text('APP_BACKUP_V2 '+HELPER.digest(path/'manifest.json')+'\n')
|
||||||
|
subprocess.run(['/usr/bin/rclone','--config',str(self.base/'rclone.conf'),
|
||||||
|
'copy',str(path),'crypt:sets/'+name],check=True,capture_output=True)
|
||||||
|
old.append(path)
|
||||||
|
with self.config.open('a') as f:
|
||||||
|
f.write('ENABLE_REMOTE_RETENTION=true\nRETENTION_KEEP_LAST=2\nLOCAL_RETENTION_DAYS=1\nREMOTE_RETENTION_DAYS=1\n')
|
||||||
|
result=self.remote_failure('upload')
|
||||||
|
self.assert_remote_failure_retained(result)
|
||||||
|
self.assertEqual(result.returncode,44)
|
||||||
|
self.assertEqual((self.base/'work/last-successful-backup.json').read_bytes(),previous)
|
||||||
|
remote=subprocess.check_output(['/usr/bin/rclone','--config',str(self.base/'rclone.conf'),
|
||||||
|
'lsf','crypt:sets','--dirs-only'],text=True)
|
||||||
|
for path in old:
|
||||||
|
self.assertTrue(path.is_dir())
|
||||||
|
self.assertIn(path.name,remote)
|
||||||
|
|
||||||
|
def test_same_size_remote_corruption_blocks_completion(self):
|
||||||
|
self.crypt_config()
|
||||||
|
result=self.remote_failure('corrupt')
|
||||||
|
self.assert_remote_failure_retained(result)
|
||||||
|
self.assertFalse((self.base/'work/last-successful-backup.json').exists())
|
||||||
|
|
||||||
|
def test_environment_cannot_weaken_remote_readback(self):
|
||||||
|
self.crypt_config()
|
||||||
|
result=self.remote_failure('corrupt',RCLONE_SIZE_ONLY='true')
|
||||||
|
self.assertIn('Unsupported rclone environment override: RCLONE_SIZE_ONLY',result.stdout)
|
||||||
|
self.assertNotIn('systemctl stop',self.events.read_text())
|
||||||
|
|
||||||
|
def test_unsupported_rclone_environment_rejected_before_remote_access(self):
|
||||||
|
self.crypt_config()
|
||||||
|
for key,value in [('RCLONE_EXCLUDE','*'),('RCLONE_DRY_RUN','true'),
|
||||||
|
('RCLONE_IGNORE_ERRORS','true'),('RCLONE_NO_CHECK_CERTIFICATE','true')]:
|
||||||
|
with self.subTest(key=key):
|
||||||
|
self.events.write_text('')
|
||||||
|
result=self.run_script('app-backup.sh','--check',ok=False,**{key:value})
|
||||||
|
self.assertIn('Unsupported rclone environment override: '+key,result.stdout)
|
||||||
|
self.assertNotIn('rclone',self.events.read_text())
|
||||||
|
self.assertNotIn('systemctl stop',self.events.read_text())
|
||||||
|
# Supported settings remain usable when an administrator exports them.
|
||||||
|
self.run_script('app-backup.sh','--check',RCLONE_RETRIES='2',RCLONE_STATS_INTERVAL='1m')
|
||||||
|
|
||||||
|
def test_marker_write_failure_blocks_upload_success(self):
|
||||||
|
self.crypt_config()
|
||||||
|
result=self.remote_failure('marker-write')
|
||||||
|
self.assert_remote_failure_retained(result)
|
||||||
|
self.assertEqual(result.returncode,44)
|
||||||
|
|
||||||
|
def test_marker_read_mismatch_blocks_upload_success(self):
|
||||||
|
self.crypt_config()
|
||||||
|
result=self.remote_failure('marker-read')
|
||||||
|
# Archives passed readback and COMPLETE was written, but its final
|
||||||
|
# confirmation failed. Local success and retention must remain blocked.
|
||||||
|
self.assert_remote_failure_retained(result,marker_present=True)
|
||||||
|
self.assertIn('Remote completion marker mismatch',result.stdout)
|
||||||
|
|
||||||
|
def test_marker_extra_newline_blocks_upload_success(self):
|
||||||
|
self.crypt_config()
|
||||||
|
result=self.remote_failure('marker-newline')
|
||||||
|
self.assert_remote_failure_retained(result,marker_present=True)
|
||||||
|
self.assertIn('Remote completion marker mismatch',result.stdout)
|
||||||
|
|
||||||
def test_unsafe_crypt_and_source_overlap_rejected(self):
|
def test_unsafe_crypt_and_source_overlap_rejected(self):
|
||||||
path=self.crypt_config()
|
path=self.crypt_config()
|
||||||
with path.open('a') as f: f.write('no_data_encryption = true\n')
|
with path.open('a') as f: f.write('no_data_encryption = true\n')
|
||||||
|
|||||||
@@ -104,8 +104,15 @@ if [[ $ENABLE_UPLOAD == true ]]; then
|
|||||||
rc check "$RUN_DIR" "$remote_run" --download --exclude /COMPLETE --exclude /UPLOADED
|
rc check "$RUN_DIR" "$remote_run" --download --exclude /COMPLETE --exclude /UPLOADED
|
||||||
echo '-- Remote readback passed; publishing completion marker'
|
echo '-- Remote readback passed; publishing completion marker'
|
||||||
rc copyto "$RUN_DIR/COMPLETE" "$remote_run/COMPLETE" --immutable
|
rc copyto "$RUN_DIR/COMPLETE" "$remote_run/COMPLETE" --immutable
|
||||||
marker=$(rc cat "$remote_run/COMPLETE")
|
# Command substitution strips trailing newlines, which could accept a marker
|
||||||
[[ $marker == "$(cat "$RUN_DIR/COMPLETE")" ]] || die 'Remote completion marker mismatch'
|
# that the strict restore parser rejects. Compare the exact returned bytes.
|
||||||
|
rc cat "$remote_run/COMPLETE" > "$STAGING_DIR/remote-COMPLETE"
|
||||||
|
python3 - "$RUN_DIR/COMPLETE" "$STAGING_DIR/remote-COMPLETE" <<'PY' || die 'Remote completion marker mismatch'
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
local, remote = map(Path, sys.argv[1:])
|
||||||
|
sys.exit(0 if local.stat().st_size == remote.stat().st_size and local.read_bytes() == remote.read_bytes() else 1)
|
||||||
|
PY
|
||||||
date -u -Is > "$RUN_DIR/UPLOADED"
|
date -u -Is > "$RUN_DIR/UPLOADED"
|
||||||
sync -f "$RUN_DIR"
|
sync -f "$RUN_DIR"
|
||||||
echo '-- Remote backup fully read back and verified'
|
echo '-- Remote backup fully read back and verified'
|
||||||
|
|||||||
Reference in new issue
Block a user