Harden remote verification against environment overrides and marker normalization

This commit is contained in:
admin committed 2026-09-10 16:23:53 +02:00
1 parent 0ec0d3b194
commit 3374019ad6
6 files changed
+248 -8

No files matched your search

+56 -3
View File
@@ -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.
+24
View File
@@ -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
View File
@@ -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
+14
View File
@@ -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
View File
@@ -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')
+9 -2
View File
@@ -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'