Fix SELinux backup execution context and probe source metadata before downtime
This commit is contained in:
1 parent
5c8a7a30be
commit
c0a24b63fc
9 files changed
+193
-14
No files matched your search
+34
-2
@@ -54,6 +54,35 @@ flowchart TD
|
|||||||
|
|
||||||
Dateikopien erhalten mit `rsync -aHAX --numeric-ids` Eigentümer, Modus, Symlinks, Hardlinks innerhalb einer Komponente, ACLs und Extended Attributes. Tar erhält diese Metadaten einschließlich SELinux. Plattform-/Dateisystemunterstützung ist Voraussetzung. Externe Symlink-Ziele werden nicht automatisch gesichert; absolute Symlinks als einzelne Einträge bleiben erhalten. Geräte, FIFOs und andere Spezialdateien werden von der Archivprüfung abgelehnt. Zusätzliche Pfade sind deshalb Datenverzeichnisse, kein Ersatz für ein vollständiges Systemimage.
|
Dateikopien erhalten mit `rsync -aHAX --numeric-ids` Eigentümer, Modus, Symlinks, Hardlinks innerhalb einer Komponente, ACLs und Extended Attributes. Tar erhält diese Metadaten einschließlich SELinux. Plattform-/Dateisystemunterstützung ist Voraussetzung. Externe Symlink-Ziele werden nicht automatisch gesichert; absolute Symlinks als einzelne Einträge bleiben erhalten. Geräte, FIFOs und andere Spezialdateien werden von der Archivprüfung abgelehnt. Zusätzliche Pfade sind deshalb Datenverzeichnisse, kein Ersatz für ein vollständiges Systemimage.
|
||||||
|
|
||||||
|
Vor jedem Backup und bei `--check` prüft rsync das Auflisten der Quellwurzel sowie
|
||||||
|
das Übertragen ihrer Verzeichnisattribute in einen privaten temporären Prüfbereich
|
||||||
|
unter `STAGING_ROOT`. Alle Kindobjekte sind von der Attributprobe ausgeschlossen;
|
||||||
|
Datei-Inhalte werden nicht kopiert. Die Probe wird danach entfernt. Das erkennt
|
||||||
|
beispielsweise eine SELinux-Ablehnung von `opendir` oder `relabelfrom` vor dem
|
||||||
|
Wartungsmodus. Es belegt nicht den Zugriff auf jede untergeordnete Datei oder
|
||||||
|
jedes dort abweichende Attribut. Der echte Kopierlauf bleibt strikt fehlergeprüft.
|
||||||
|
Die rsync-Kindprozesse erhalten keine geerbten Update-/Backup-Sperrdeskriptoren;
|
||||||
|
der übergeordnete Prozess hält seine Sperren unverändert weiter.
|
||||||
|
|
||||||
|
Backup-Dienst und abgekoppelter Update-Auftrag starten über `/bin/bash` mit
|
||||||
|
`SELinuxContext=system_u:system_r:unconfined_service_t:s0`. Das erlaubt dem
|
||||||
|
privilegierten Backup, unterschiedliche Web-, Mail- und Home-Labels zu lesen
|
||||||
|
und Sicherheitsattribute zu erhalten. **Diese Prozesse und ihre Kindprozesse
|
||||||
|
sind damit bewusst nicht durch SELinux eingeschränkt**; die übrigen Dienste
|
||||||
|
bleiben ihrer bestehenden Policy unterworfen. Das ist keine eigens eingeschränkte
|
||||||
|
Backup-Policy. Es werden keine globalen rsync-Booleans, Quell-Labels oder
|
||||||
|
Dateizugriffsrechte geändert. Ein verweigerter Kontext wird nicht mit dem
|
||||||
|
systemd-Präfix `-` übergangen. Bei deaktiviertem SELinux ignoriert systemd die
|
||||||
|
Kontexteinstellung.
|
||||||
|
|
||||||
|
Auf Gemini wurde am 10.09.2026 unter Enforcing die Startkette Bash → rsync mit
|
||||||
|
diesem Kontext und erfolgreichem Auflisten von `/var/www/html` bestätigt. Der
|
||||||
|
direkte Start von rsync als Unit-Einstiegspunkt scheiterte dagegen mit 203/EXEC;
|
||||||
|
deshalb verwenden beide Units ausdrücklich Bash. Dieser Lesetest bestätigt
|
||||||
|
noch keine vollständige Attributübertragung oder Sicherung. Referenzen:
|
||||||
|
[systemd SELinuxContext](https://github.com/systemd/systemd/blob/v256/man/systemd.exec.xml),
|
||||||
|
[Fedora-SELinux-Domain und Einstiegspunkte](https://github.com/fedora-selinux/selinux-policy/blob/rawhide/policy/modules/system/unconfined.te).
|
||||||
|
|
||||||
Die Kompression schreibt direkt aus tar in `.partial`-Dateien. Erst erfolgreiche Pipelines werden umbenannt. zstd nutzt standardmäßig Stufe 6 und zwei Threads. `sync -f` vor Fortsetzung bzw. Retention fordert die Persistierung des lokalen Satzes an; Hardware-/Storagefehler bleiben eine eigene Ebene.
|
Die Kompression schreibt direkt aus tar in `.partial`-Dateien. Erst erfolgreiche Pipelines werden umbenannt. zstd nutzt standardmäßig Stufe 6 und zwei Threads. `sync -f` vor Fortsetzung bzw. Retention fordert die Persistierung des lokalen Satzes an; Hardware-/Storagefehler bleiben eine eigene Ebene.
|
||||||
|
|
||||||
## Format und Komponenten
|
## Format und Komponenten
|
||||||
@@ -331,10 +360,13 @@ 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.
|
||||||
|
|
||||||
Ergebnis des lokalen Abschlusslaufs am 10.09.2026: **41 Tests erfolgreich**, 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.
|
Ergebnis des lokalen Abschlusslaufs am 10.09.2026: **44 Tests erfolgreich**, 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.
|
||||||
|
|
||||||
Geprüfte Fälle umfassen:
|
Geprüfte Fälle umfassen:
|
||||||
|
|
||||||
|
- Verweigerter rsync-Quellzugriff bricht das Update vor Installation und Dienst-Stopp ab.
|
||||||
|
- Verweigerte Verzeichnisattribute werden vor Wartungsmodus erkannt; der private Prüfbereich wird entfernt.
|
||||||
|
- Attributprobe kopiert keine Kindobjekte; rsync erbt bei Prüfung, Backup und Restore keine Update-/Backup-Sperrdeskriptoren.
|
||||||
- Fehlendes globales `RELOAD` bei eingeschränktem Konto: Vorprüfungsabbruch ohne Installation/Writer-Stopp, anschließend erfolgreicher Paketlauf nach gezieltem Grant. Aktive verschachtelte MariaDB-Rollen werden anerkannt, inaktive Rollen abgewiesen.
|
- Fehlendes globales `RELOAD` bei eingeschränktem Konto: Vorprüfungsabbruch ohne Installation/Writer-Stopp, anschließend erfolgreicher Paketlauf nach gezieltem Grant. Aktive verschachtelte MariaDB-Rollen werden anerkannt, inaktive Rollen abgewiesen.
|
||||||
- Fehlender Zugriff auf MariaDB-Routinen-Metadaten vor Wartungsmodus erkannt; nach gezielter Berechtigung tatsächlicher Dump mit gespeicherter Prozedur geprüft.
|
- Fehlender Zugriff auf MariaDB-Routinen-Metadaten vor Wartungsmodus erkannt; nach gezielter Berechtigung tatsächlicher Dump mit gespeicherter Prozedur geprüft.
|
||||||
- Fehlender Mailstore ohne Live-Änderung, explizite Pfadkorrektur, Homes-only-Update samt tatsächlichem Maildatei-Restore und Ablehnung unvollständiger Home-Konfigurationen.
|
- Fehlender Mailstore ohne Live-Änderung, explizite Pfadkorrektur, Homes-only-Update samt tatsächlichem Maildatei-Restore und Ablehnung unvollständiger Home-Konfigurationen.
|
||||||
@@ -354,4 +386,4 @@ Geprüfte Fälle umfassen:
|
|||||||
- Abgewiesene systemd-Übergabe, zurückgezogene Freigabe bei bereits eingereihtem Auftrag und persistente Fehlerprotokolle bei Sperr-Timeout; jeweils ohne Installationsänderung oder Timer-Stopp.
|
- Abgewiesene systemd-Übergabe, zurückgezogene Freigabe bei bereits eingereihtem Auftrag und persistente Fehlerprotokolle bei Sperr-Timeout; jeweils ohne Installationsänderung oder Timer-Stopp.
|
||||||
- Ausdrückliche Cron-Policy und Writer-Wartezeit/Timeout.
|
- Ausdrückliche Cron-Policy und Writer-Wartezeit/Timeout.
|
||||||
|
|
||||||
**Serverabnahme steht separat aus:** echte Rocky-Linux-10-Umgebung, SELinux-/ACL-Verhalten auf den verwendeten Dateisystemen, installierte Nextcloud-/Gitea-Versionen, vollständige Writer-/Pfadliste, Berechtigungen des echten DB-Backupaccounts, tatsächlicher OneDrive-Upload großer Archive, Laufzeit/Speicherbedarf und fachliche Restore-Probe. Es wurde kein Produktivbackup ausgeführt und keine Serverinstallation verändert.
|
**Serverabnahme steht separat aus:** SELinux-/ACL-Verhalten beim vollständigen Kopieren auf den verwendeten Dateisystemen, installierte Nextcloud-/Gitea-Versionen, vollständige Writer-/Pfadliste, tatsächlicher OneDrive-Upload großer Archive, Laufzeit/Speicherbedarf und fachliche Restore-Probe. Die Entwicklungstests verändern keinen Produktivserver. Vom Betreiber ausgeführte Paketläufe haben die Installation auf Gemini bereits aktualisiert; alle drei DB-Dumps waren erfolgreich. Eine vollständige erfolgreiche Sicherung und fachliche Wiederherstellung sind noch nicht bestätigt.
|
||||||
@@ -37,6 +37,13 @@ abfragbar. Diagnose und Details stehen in der Update-Anleitung.
|
|||||||
|
|
||||||
Die folgende manuelle Anleitung bleibt als Alternative für individuelle Updates.
|
Die folgende manuelle Anleitung bleibt als Alternative für individuelle Updates.
|
||||||
|
|
||||||
|
Auf Rocky/RHEL starten Backup-Dienst und Update-Auftrag über Bash mit
|
||||||
|
`SELinuxContext=system_u:system_r:unconfined_service_t:s0`. SELinux bleibt systemweit
|
||||||
|
aktiv; **diese privilegierten Backup-Prozesse sind bewusst nicht durch SELinux
|
||||||
|
eingeschränkt**. Dadurch können sie Web-/Mail-/Home-Daten samt Sicherheitsattributen
|
||||||
|
sichern. Der Installer ändert keine globalen rsync-Booleans und keine Quell-Labels.
|
||||||
|
Details und geprüfte Grenzen stehen in der Update-Anleitung.
|
||||||
|
|
||||||
## Wesentliche Änderungen
|
## Wesentliche Änderungen
|
||||||
|
|
||||||
- Funktionierender Restore mit Prüflauf, Entpackmodus, Komponentenauswahl und ausdrücklichem `--apply --yes`.
|
- Funktionierender Restore mit Prüflauf, Entpackmodus, Komponentenauswahl und ausdrücklichem `--apply --yes`.
|
||||||
@@ -96,8 +103,11 @@ sudo cat /var/backups/app-backup/last-backup.json
|
|||||||
`--check` prüft Werkzeuge, Quellpfade, Dateirechte, DB-Verbindungen, das globale
|
`--check` prüft Werkzeuge, Quellpfade, Dateirechte, DB-Verbindungen, das globale
|
||||||
DB-Recht `RELOAD` für die Dump-Sperre, bei MariaDB den Zugriff auf Routinen-Metadaten,
|
DB-Recht `RELOAD` für die Dump-Sperre, bei MariaDB den Zugriff auf Routinen-Metadaten,
|
||||||
Speicherreserve und Crypt-Zugriff.
|
Speicherreserve und Crypt-Zugriff.
|
||||||
Die Rechteprüfung liest nur Metadaten; sie stoppt keine Anwendungen, sperrt keine
|
Die Prüfung liest Quellverzeichnisse über rsync und überträgt nur deren
|
||||||
Datenbank und führt weder Backup noch Upload aus. Sie ersetzt keinen vollständigen
|
Verzeichnisattribute in einen privaten temporären Prüfbereich im Staging.
|
||||||
|
Dabei werden keine Datei-Inhalte kopiert. Die DB-Rechteprüfung liest Metadaten;
|
||||||
|
es werden keine Anwendungen gestoppt, keine globale DB-Sperre genommen und kein
|
||||||
|
Backup oder Upload ausgeführt. Die Vorprüfung ersetzt keinen vollständigen
|
||||||
Probelauf. Fehlt `RELOAD`, die tatsächlich verwendeten Backup-Konten ermitteln und
|
Probelauf. Fehlt `RELOAD`, die tatsächlich verwendeten Backup-Konten ermitteln und
|
||||||
gezielt berechtigen; siehe [DB-Rechte korrigieren](UPDATE_ANLEITUNG.md#fehlendes-db-recht-reload).
|
gezielt berechtigen; siehe [DB-Rechte korrigieren](UPDATE_ANLEITUNG.md#fehlendes-db-recht-reload).
|
||||||
|
|
||||||
|
|||||||
+42
-3
@@ -24,7 +24,9 @@ Backups mit zeitweiliger Unterbrechung der konfigurierten Anwendungen.
|
|||||||
5. Crypt-Konfiguration, DB-Dateien, globales DB-Recht `RELOAD` und bei MariaDB
|
5. Crypt-Konfiguration, DB-Dateien, globales DB-Recht `RELOAD` und bei MariaDB
|
||||||
Zugriff auf Routinen-Metadaten prüfen;
|
Zugriff auf Routinen-Metadaten prüfen;
|
||||||
vorhandene Schlüssel/Passwörter erhalten. Keine automatischen DB-Grants.
|
vorhandene Schlüssel/Passwörter erhalten. Keine automatischen DB-Grants.
|
||||||
6. Vorprüfung mit geschützten Kopien durchführen. Bei Fehlern bleibt das installierte
|
6. Vorprüfung mit geschützten Kopien durchführen. rsync prüft außerdem den Zugriff
|
||||||
|
auf Quellverzeichnisse und deren Attribute in einem privaten temporären
|
||||||
|
Prüfbereich, ohne Datei-Inhalte zu kopieren. Bei Fehlern bleibt das installierte
|
||||||
Paket samt Anwendungskonfiguration und Timer unverändert.
|
Paket samt Anwendungskonfiguration und Timer unverändert.
|
||||||
7. Einen von SSH unabhängigen systemd-Auftrag `app-backup-upgrade.service` mit
|
7. Einen von SSH unabhängigen systemd-Auftrag `app-backup-upgrade.service` mit
|
||||||
`--no-block` einreihen und die Sperren an ihn übergeben.
|
`--no-block` einreihen und die Sperren an ihn übergeben.
|
||||||
@@ -274,6 +276,41 @@ im Journal protokolliert. Eine längere Pause ohne CPU-Last lässt sich so einem
|
|||||||
konkreten Dienst zuordnen; ohne dessen Journal lässt sich die Ursache der Pause
|
konkreten Dienst zuordnen; ohne dessen Journal lässt sich die Ursache der Pause
|
||||||
nicht aus ihrer Dauer allein bestimmen.
|
nicht aus ihrer Dauer allein bestimmen.
|
||||||
|
|
||||||
|
## SELinux: rsync-Zugriff verweigert (Exit 23)
|
||||||
|
|
||||||
|
Der Gemini-Lauf vom 10.09.2026 um 14:58 Uhr scheiterte nach drei erfolgreichen
|
||||||
|
DB-Dumps beim Kopieren von `/var/www/html`. Das Audit bestätigt rsync im Kontext
|
||||||
|
`rsync_t`: Lesen von `httpd_sys_content_t` sowie Attributänderungen und Relabeling
|
||||||
|
im Staging wurden verweigert. Zusätzlich wurden geerbte Sperrdeskriptoren beanstandet.
|
||||||
|
Die gestoppten Dienste wurden laut Journal wieder gestartet und Nextcloud verließ
|
||||||
|
den Wartungsmodus. Es entstand kein vollständiger neuer Sicherungssatz.
|
||||||
|
|
||||||
|
Das aktuelle Paket setzt für den Update-Auftrag und den installierten Backup-Dienst
|
||||||
|
`SELinuxContext=system_u:system_r:unconfined_service_t:s0` und startet beide über
|
||||||
|
`/bin/bash`. **Diese privilegierten Prozesse und ihre Kindprozesse laufen damit
|
||||||
|
ohne SELinux-Einschränkung.** SELinux bleibt systemweit aktiv; andere Dienste
|
||||||
|
behalten ihre bestehende Policy. Der Installer ändert keine globalen rsync-Booleans,
|
||||||
|
keine Quell-Labels und keine Quellberechtigungen. Ein nicht zugelassener Kontext
|
||||||
|
führt zu einem sichtbaren Startfehler.
|
||||||
|
|
||||||
|
Auf Gemini ist die Bash-Startkette mit diesem Kontext und erfolgreichem
|
||||||
|
rsync-Auflisten des Webverzeichnisses unter Enforcing geprüft. Der direkte
|
||||||
|
Unit-Start von `/usr/bin/rsync` scheiterte dagegen mit 203/EXEC und bildet die
|
||||||
|
Startkette des Pakets nicht ab. Manuelle Unit-Anpassungen sind nicht erforderlich:
|
||||||
|
das aktuelle Paket mit denselben Gitea-/Cron-/Mail-Optionen erneut starten.
|
||||||
|
|
||||||
|
Vor dem Wartungsmodus prüft das Paket über rsync die Quellverzeichnisse und die
|
||||||
|
Übernahme ihrer Verzeichnisattribute in einen privaten temporären Prüfbereich.
|
||||||
|
Dabei werden keine Dateiinhalte kopiert. Die Probe erfasst nicht jede untergeordnete
|
||||||
|
Datei; der vollständige Kopierlauf und die Remote-Prüfung bleiben erforderlich.
|
||||||
|
rsync erhält keine geerbten Sperrdeskriptoren; die Elternprozesse halten die Sperren
|
||||||
|
weiter. Der SELinux-Kontext des Backup-Prozesses erscheint im Protokoll.
|
||||||
|
|
||||||
|
Bei einem erneuten Fehler `update.log`, die ausgegebene Ergebnisdatei und die
|
||||||
|
passenden AVC-Meldungen prüfen. Maßgeblich für den erfolgreichen Abschluss bleibt
|
||||||
|
`SUCCESS` in `result.txt`; erst danach aktiviert der Installer den Backup-Timer.
|
||||||
|
Eine Wiederherstellungsprobe auf einem separaten System steht weiterhin aus.
|
||||||
|
|
||||||
## Paket reproduzieren
|
## Paket reproduzieren
|
||||||
|
|
||||||
Aus einem sauberen, eingecheckten Repository:
|
Aus einem sauberen, eingecheckten Repository:
|
||||||
@@ -290,12 +327,14 @@ Das ausführbare Paket ausschließlich über euren vertrauenswürdigen Kanal bez
|
|||||||
|
|
||||||
## Verifikation der Lieferung
|
## Verifikation der Lieferung
|
||||||
|
|
||||||
41 Container-Tests sowie ShellCheck und Shell-Syntaxprüfung erfolgreich.
|
44 Container-Tests sowie ShellCheck und Shell-Syntaxprüfung erfolgreich.
|
||||||
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
|
||||||
berücksichtigt jetzt das Warten auf den oneshot-Startauftrag: Der Regressionstest
|
berücksichtigt jetzt das Warten auf den oneshot-Startauftrag: Der Regressionstest
|
||||||
scheiterte mit dem alten Code und war nach dem Fix erfolgreich. Geprüft sind auch
|
scheiterte mit dem alten Code und war nach dem Fix erfolgreich. Geprüft sind auch
|
||||||
abgewiesene Übergaben, eingereihte Worker trotz fehlgeschlagener Antwort und
|
abgewiesene Übergaben, eingereihte Worker trotz fehlgeschlagener Antwort und
|
||||||
persistente Fehlerprotokolle bei belegter Update-Sperre. Die tatsächliche
|
persistente Fehlerprotokolle bei belegter Update-Sperre. Die tatsächliche
|
||||||
Server-/OneDrive-Prüfung erfolgt beim ersten Paketlauf. Ein fachlicher Restore
|
SELinux-Durchsetzung wird im Debian-Testcontainer nicht nachgebildet; der erfolgreiche
|
||||||
|
Bash-/rsync-Lesetest auf Gemini wurde separat vom Betreiber durchgeführt. Die vollständige
|
||||||
|
Server-/OneDrive-Prüfung erfolgt beim Paketlauf. Ein fachlicher Restore
|
||||||
auf einem separaten Testsystem bleibt erforderlich.
|
auf einem separaten Testsystem bleibt erforderlich.
|
||||||
@@ -118,6 +118,8 @@ init_runtime() {
|
|||||||
# Journald already captures service stdout. No dependency on systemd-cat in terminals.
|
# Journald already captures service stdout. No dependency on systemd-cat in terminals.
|
||||||
exec > >(tee -a "$LOG_FILE") 2>&1
|
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||||
echo "== $OPERATION $RUN_ID =="
|
echo "== $OPERATION $RUN_ID =="
|
||||||
|
local security_context
|
||||||
|
if security_context=$(id -Z 2>/dev/null); then echo "-- SELinux process context: $security_context"; fi
|
||||||
need rsync; need tar; need "$COMPRESSOR"
|
need rsync; need tar; need "$COMPRESSOR"
|
||||||
STOPPED_SERVICES=(); NC_CHANGED=false; NC_WAS_ON=false; MUTATION_STARTED=false
|
STOPPED_SERVICES=(); NC_CHANGED=false; NC_WAS_ON=false; MUTATION_STARTED=false
|
||||||
trap 'exit 130' INT
|
trap 'exit 130' INT
|
||||||
@@ -190,8 +192,31 @@ check_db_credentials() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_source_access() (
|
||||||
|
# This subshell owns only a fresh private probe directory. Keep source data
|
||||||
|
# untouched and clean probe objects even if an attribute operation fails.
|
||||||
|
local probe p
|
||||||
|
probe=$(mktemp -d "$STAGING_ROOT/.access-probe-XXXXXXXX")
|
||||||
|
trap 'rm -rf -- "$probe"' EXIT
|
||||||
|
# Test through rsync itself: under SELinux it can enter a different domain
|
||||||
|
# than this root shell. A shell test -r / realpath alone misses that denial.
|
||||||
|
# --dirs lists one directory level only, without reading file contents,
|
||||||
|
# recursing through live mailboxes, or creating a destination.
|
||||||
|
for p in "${SOURCES[@]}"; do
|
||||||
|
rsync --list-only --dirs -- "$p/" 8>&- 9>&- >/dev/null || die "rsync cannot list source: $p; check permissions and SELinux audit records before retrying"
|
||||||
|
mkdir "$probe/root"
|
||||||
|
# Exclude ALL children: copy only root-directory metadata into the private
|
||||||
|
# probe. Exercise chmod/chown/times/ACLs/xattrs, including SELinux labels,
|
||||||
|
# before maintenance. This is not a recursive/full content access test.
|
||||||
|
rsync -aHAX --numeric-ids --exclude='/*' -- "$p/" "$probe/root/" 8>&- 9>&- || die "rsync cannot preserve directory metadata for: $p; inspect SELinux/filesystem permissions"
|
||||||
|
rm -rf -- "$probe/root"
|
||||||
|
done
|
||||||
|
echo '-- Source directory access and staging metadata probe passed'
|
||||||
|
)
|
||||||
|
|
||||||
preflight() {
|
preflight() {
|
||||||
for p in "${SOURCES[@]}"; do check_source_path "$p"; done
|
for p in "${SOURCES[@]}"; do check_source_path "$p"; done
|
||||||
|
if [[ $OPERATION == backup ]]; then check_source_access; fi
|
||||||
check_db_credentials
|
check_db_credentials
|
||||||
if (( ${#DB_LABELS[@]} )); then
|
if (( ${#DB_LABELS[@]} )); then
|
||||||
need "$DB_DUMP_BIN"; need "$DB_CLIENT_BIN"
|
need "$DB_DUMP_BIN"; need "$DB_CLIENT_BIN"
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ After=network-online.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
ExecStart=/usr/local/sbin/app-backup.sh
|
# Bash is an allowed entry point for this SELinux service domain. Running rsync
|
||||||
|
# directly as the entry point can fail even when child execution is permitted.
|
||||||
|
ExecStart=/bin/bash /usr/local/sbin/app-backup.sh
|
||||||
|
# A privileged backup crosses web/mail/user labels and preserves those labels.
|
||||||
|
# Keep enforcing globally; scope unconfined execution to this backup service.
|
||||||
|
# This deliberately removes SELinux confinement for this unit and its children.
|
||||||
|
# No '-' prefix: an unavailable/denied context must fail visibly.
|
||||||
|
SELinuxContext=system_u:system_r:unconfined_service_t:s0
|
||||||
# Timeout sends TERM: Bash runs its recovery trap; grace time permits service restart.
|
# Timeout sends TERM: Bash runs its recovery trap; grace time permits service restart.
|
||||||
TimeoutStartSec=12h
|
TimeoutStartSec=12h
|
||||||
TimeoutStopSec=5min
|
TimeoutStopSec=5min
|
||||||
|
|||||||
+66
-2
@@ -251,6 +251,67 @@ QUIESCE_SERVICES=(nc-cron)
|
|||||||
self.assertFalse(list((self.base/'work/archives').glob('appbackup_*')))
|
self.assertFalse(list((self.base/'work/archives').glob('appbackup_*')))
|
||||||
self.assertEqual(json.loads(self.state.read_text()),self.initial)
|
self.assertEqual(json.loads(self.state.read_text()),self.initial)
|
||||||
|
|
||||||
|
def test_rsync_source_denial_fails_preflight_before_stopping_services(self):
|
||||||
|
canonical=self.configure_upgrade()
|
||||||
|
original=canonical.read_bytes()
|
||||||
|
wrapper=self.bin/'rsync'
|
||||||
|
wrapper.write_text('''#!/usr/bin/env bash
|
||||||
|
if [[ " $* " == *" --list-only "* ]]; then
|
||||||
|
echo 'rsync: simulated source opendir permission denied' >&2
|
||||||
|
exit 23
|
||||||
|
fi
|
||||||
|
exec /usr/bin/rsync "$@"
|
||||||
|
''')
|
||||||
|
wrapper.chmod(0o755)
|
||||||
|
package=self.build_package()
|
||||||
|
result=subprocess.run(['bash',str(package),'--foreground'],env=self.env,capture_output=True,text=True,timeout=60)
|
||||||
|
self.assertNotEqual(result.returncode,0,result.stdout+result.stderr)
|
||||||
|
self.assertIn('rsync cannot list source',result.stdout+result.stderr)
|
||||||
|
self.assertEqual(canonical.read_bytes(),original)
|
||||||
|
self.assertEqual(json.loads(self.state.read_text())['services']['app-backup.timer'],'active')
|
||||||
|
self.assertNotIn('systemctl stop',self.events.read_text())
|
||||||
|
self.assertNotIn('maintenance:mode --on',self.events.read_text())
|
||||||
|
|
||||||
|
def test_directory_attribute_denial_is_caught_before_maintenance(self):
|
||||||
|
wrapper=self.bin/'rsync'
|
||||||
|
wrapper.write_text('''#!/usr/bin/env bash
|
||||||
|
if [[ " $* " == *" --exclude=/* "* ]]; then
|
||||||
|
echo 'rsync: simulated security.selinux relabel denial' >&2
|
||||||
|
exit 23
|
||||||
|
fi
|
||||||
|
exec /usr/bin/rsync "$@"
|
||||||
|
''')
|
||||||
|
wrapper.chmod(0o755)
|
||||||
|
failed=self.run_script('app-backup.sh','--check',ok=False)
|
||||||
|
self.assertIn('cannot preserve directory metadata',failed.stdout)
|
||||||
|
self.assertNotIn('systemctl stop',self.events.read_text())
|
||||||
|
self.assertEqual(json.loads(self.state.read_text()),self.initial)
|
||||||
|
self.assertFalse(list((self.base/'work/staging').iterdir()))
|
||||||
|
|
||||||
|
def test_metadata_probe_copies_no_children_and_rsync_inherits_no_locks(self):
|
||||||
|
wrapper=self.bin/'rsync'
|
||||||
|
wrapper.write_text('''#!/usr/bin/env python3
|
||||||
|
import os,subprocess,sys
|
||||||
|
from pathlib import Path
|
||||||
|
for fd in (8,9):
|
||||||
|
try: os.fstat(fd)
|
||||||
|
except OSError: pass
|
||||||
|
else: sys.exit('rsync inherited a coordinator lock descriptor')
|
||||||
|
result=subprocess.run(['/usr/bin/rsync',*sys.argv[1:]])
|
||||||
|
if result.returncode: sys.exit(result.returncode)
|
||||||
|
if '--exclude=/*' in sys.argv:
|
||||||
|
destination=Path(sys.argv[-1])
|
||||||
|
assert not list(destination.iterdir()), 'probe copied source contents'
|
||||||
|
source=Path(sys.argv[-2])
|
||||||
|
assert destination.stat().st_mode == source.stat().st_mode
|
||||||
|
''')
|
||||||
|
wrapper.chmod(0o755)
|
||||||
|
self.run_script('app-backup.sh','--check')
|
||||||
|
self.assertFalse(list((self.base/'work/staging').iterdir()))
|
||||||
|
self.assertNotIn('systemctl stop',self.events.read_text())
|
||||||
|
run=self.backup()
|
||||||
|
self.run_script('app-restore.sh','--local-run',run,'--components','wordpress','--apply','--yes')
|
||||||
|
|
||||||
def test_maintenance_and_inactive_service_preserved(self):
|
def test_maintenance_and_inactive_service_preserved(self):
|
||||||
state=dict(services=dict(self.initial['services'],gitea='inactive'),maintenance=True)
|
state=dict(services=dict(self.initial['services'],gitea='inactive'),maintenance=True)
|
||||||
self.state.write_text(json.dumps(state))
|
self.state.write_text(json.dumps(state))
|
||||||
@@ -296,10 +357,12 @@ QUIESCE_SERVICES=(nc-cron)
|
|||||||
|
|
||||||
def test_term_recovers_services(self):
|
def test_term_recovers_services(self):
|
||||||
wrapper=self.bin/'rsync'
|
wrapper=self.bin/'rsync'
|
||||||
wrapper.write_text('#!/usr/bin/env bash\necho waiting > "$TEST_EVENTS.waiting"\nsleep 2\nexec /usr/bin/rsync "$@"\n')
|
# Signal only during the real copy, after services have stopped. The
|
||||||
|
# new access probes run earlier and must not trigger this recovery test.
|
||||||
|
wrapper.write_text('#!/usr/bin/env bash\nif [[ " $* " != *" --list-only "* && " $* " != *" --exclude=/* "* ]]; then\n echo waiting > "$TEST_EVENTS.waiting"\n sleep 2\nfi\nexec /usr/bin/rsync "$@"\n')
|
||||||
proc=subprocess.Popen(['bash',str(REPO/'usr-local-sbin/app-backup.sh'),'--config',str(self.config)],env=self.env,text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
|
proc=subprocess.Popen(['bash',str(REPO/'usr-local-sbin/app-backup.sh'),'--config',str(self.config)],env=self.env,text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
|
||||||
try:
|
try:
|
||||||
for _ in range(100):
|
for _ in range(200):
|
||||||
if Path(str(self.events)+'.waiting').exists(): break
|
if Path(str(self.events)+'.waiting').exists(): break
|
||||||
time.sleep(.05)
|
time.sleep(.05)
|
||||||
else: self.fail('Backup did not reach rsync')
|
else: self.fail('Backup did not reach rsync')
|
||||||
@@ -612,6 +675,7 @@ from pathlib import Path
|
|||||||
args=sys.argv[1:]
|
args=sys.argv[1:]
|
||||||
assert '--unit=app-backup-upgrade' in args
|
assert '--unit=app-backup-upgrade' in args
|
||||||
assert '--property=Type=oneshot' in args
|
assert '--property=Type=oneshot' in args
|
||||||
|
assert '--property=SELinuxContext=system_u:system_r:unconfined_service_t:s0' in args
|
||||||
command=args[args.index('/bin/bash'):]
|
command=args[args.index('/bin/bash'):]
|
||||||
state=Path(command[-1])
|
state=Path(command[-1])
|
||||||
if os.environ.get('TEST_SYSTEMD_SUBMIT') == 'reject':
|
if os.environ.get('TEST_SYSTEMD_SUBMIT') == 'reject':
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ if [[ $UP_WORKER == false ]]; then
|
|||||||
# Only enqueue here; result.txt, not systemd-run's exit code, proves success.
|
# Only enqueue here; result.txt, not systemd-run's exit code, proves success.
|
||||||
# The launcher must not keep copies of our locks; systemd starts a fresh worker.
|
# The launcher must not keep copies of our locks; systemd starts a fresh worker.
|
||||||
systemd-run --no-block --unit=app-backup-upgrade --collect --property=Type=oneshot \
|
systemd-run --no-block --unit=app-backup-upgrade --collect --property=Type=oneshot \
|
||||||
|
--property=SELinuxContext=system_u:system_r:unconfined_service_t:s0 \
|
||||||
--property=TimeoutStartSec=24h --property=TimeoutStopSec=5min --property=KillMode=mixed \
|
--property=TimeoutStartSec=24h --property=TimeoutStopSec=5min --property=KillMode=mixed \
|
||||||
/bin/bash "$UPDATE_ROOT/update.sh" --worker "$UP_STATE" 8>&- 9>&- || {
|
/bin/bash "$UPDATE_ROOT/update.sh" --worker "$UP_STATE" 8>&- 9>&- || {
|
||||||
# Revoke the queued state while still holding the coordinator lock. Even
|
# Revoke the queued state while still holding the coordinator lock. Even
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ init_runtime
|
|||||||
build_plan
|
build_plan
|
||||||
preflight
|
preflight
|
||||||
if [[ $CHECK == true ]]; then
|
if [[ $CHECK == true ]]; then
|
||||||
echo 'Preflight passed. No source copy, service stop, dump, upload or deletion performed.'
|
echo 'Preflight passed. Directory metadata probed; no application backup, service stop or upload performed.'
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
STAGING_DIR=$(mktemp -d "$STAGING_ROOT/${RUN_ID}.XXXXXX")
|
STAGING_DIR=$(mktemp -d "$STAGING_ROOT/${RUN_ID}.XXXXXX")
|
||||||
@@ -58,7 +58,8 @@ for i in "${!LABELS[@]}"; do
|
|||||||
exclusions "$label"
|
exclusions "$label"
|
||||||
echo "-- Copy: $label ($src)"
|
echo "-- Copy: $label ($src)"
|
||||||
# Never accept vanished-file (24) or partial-copy (23) as success.
|
# Never accept vanished-file (24) or partial-copy (23) as success.
|
||||||
rsync -aHAX --numeric-ids "${EXCLUDES[@]}" -- "$src/" "$STAGING_DIR/$root/"
|
# Parent retains both locks; rsync itself needs neither inherited descriptor.
|
||||||
|
rsync -aHAX --numeric-ids "${EXCLUDES[@]}" -- "$src/" "$STAGING_DIR/$root/" 8>&- 9>&-
|
||||||
printf '%s\t%s\n' "$label" "$src" >> "$PLAN"
|
printf '%s\t%s\n' "$label" "$src" >> "$PLAN"
|
||||||
done
|
done
|
||||||
# The full consistency window includes all dumps AND copies. Compression/upload
|
# The full consistency window includes all dumps AND copies. Compression/upload
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ else
|
|||||||
input_real=$(realpath -e "$LOCAL_RUN")
|
input_real=$(realpath -e "$LOCAL_RUN")
|
||||||
stage_real=$(realpath -e "$STAGING_DIR")
|
stage_real=$(realpath -e "$STAGING_DIR")
|
||||||
[[ $stage_real != "$input_real" && $stage_real != "$input_real/"* ]] || die 'Backup source contains restore workspace'
|
[[ $stage_real != "$input_real" && $stage_real != "$input_real/"* ]] || die 'Backup source contains restore workspace'
|
||||||
rsync -a -- "$LOCAL_RUN/" "$INPUT/"
|
rsync -a -- "$LOCAL_RUN/" "$INPUT/" 8>&- 9>&-
|
||||||
fi
|
fi
|
||||||
if [[ $LEGACY == true ]]; then
|
if [[ $LEGACY == true ]]; then
|
||||||
echo 'WARNING: Legacy backup has no trusted manifest/completeness proof.'
|
echo 'WARNING: Legacy backup has no trusted manifest/completeness proof.'
|
||||||
@@ -172,7 +172,7 @@ for i in "${!SELECTED[@]}"; do
|
|||||||
else
|
else
|
||||||
exclusions "$label"
|
exclusions "$label"
|
||||||
if [[ $RESTORE_STRICT_DELETE == true ]]; then EXCLUDES+=(--delete-delay); fi
|
if [[ $RESTORE_STRICT_DELETE == true ]]; then EXCLUDES+=(--delete-delay); fi
|
||||||
rsync -aHAX --numeric-ids "${EXCLUDES[@]}" -- "$source_path/" "${TARGETS[i]}/"
|
rsync -aHAX --numeric-ids "${EXCLUDES[@]}" -- "$source_path/" "${TARGETS[i]}/" 8>&- 9>&-
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
# A backup captured maintenance=true. Enforce the recorded pre-restore state even
|
# A backup captured maintenance=true. Enforce the recorded pre-restore state even
|
||||||
|
|||||||
Reference in new issue
Block a user