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/).
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
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
```
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.
@@ -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.
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:
- 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.
- 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.
+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).
## 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)
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
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.
2. Bestehende Einstellungen nach `/var/backups/app-backup-upgrade-XXXXXXXX` sichern.
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
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.
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.
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.
Die Tests enthalten echte MariaDB-Dumps/Imports und lokale rclone-Crypt-Roundtrips;
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
[[ $key == RCLONE_CONFIG_PASS ]] || die "Remote overrides through $key are unsupported; use the config file"
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
# 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"
+129 -1
View File
@@ -1,5 +1,6 @@
"""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 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)
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):
path=self.crypt_config()
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
echo '-- Remote readback passed; publishing completion marker'
rc copyto "$RUN_DIR/COMPLETE" "$remote_run/COMPLETE" --immutable
marker=$(rc cat "$remote_run/COMPLETE")
[[ $marker == "$(cat "$RUN_DIR/COMPLETE")" ]] || die 'Remote completion marker mismatch'
# Command substitution strips trailing newlines, which could accept a marker
# 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"
sync -f "$RUN_DIR"
echo '-- Remote backup fully read back and verified'