Technischer Artikel

Mac-CI-Arbeitsbereiche mit verschlüsselten Sparse Images isolieren

Mac-CI-Arbeitsbereiche mit verschlüsselten Sparse Images isolieren

Wenn derselbe cloudbasierte Mac mehrere Build-Aufträge nacheinander ausführt, bleiben die gefährlichsten Probleme oft unbemerkt: Nicht der Build schlägt fehl, sondern ein nachfolgender Auftrag greift auf Quellcode, temporäre Zertifikate, Testdaten oder Caches seines Vorgängers zu. Ein einfaches rm -rf nach jedem Lauf ist dafür nicht zuverlässig genug. Prozesse können Dateien weiterhin geöffnet halten, versteckte Verzeichnisse werden leicht übersehen, und bei einem unerwarteten Abbruch wird die Bereinigung möglicherweise vollständig übersprungen. Eine klarere Grenze entsteht, wenn für jeden Auftrag ein separates verschlüsseltes Sparse Image eingebunden wird und Checkout, Build sowie temporäre Artefakte vollständig auf dieses Dateisystem beschränkt bleiben.

Warum Sparse Images zur Isolation verwenden?

Sparse Images können eine große logische Kapazität bereitstellen, während ihr Platzbedarf auf dem Host-Datenträger nur mit der tatsächlich geschriebenen Datenmenge wächst. Gleichzeitig bleiben sie gewöhnliche Dateien, die sich anhand einer Auftragsnummer leicht finden, auswerten und löschen lassen. Nach dem Einbinden verhalten sie sich wie eigenständige APFS-Volumes, sodass bestehende Build-Skripte üblicherweise nur ein anderes Arbeitsverzeichnis benötigen.

Die Verschlüsselung schützt vor einer Offenlegung der Daten, solange das Image nicht eingebunden ist. Der separate Einhängepunkt schafft eine klare Pfadgrenze zwischen den Aufträgen. Beides ersetzt weder getrennte Benutzerkonten für die Ausführung noch das Prinzip der geringsten Rechte, ist aber leichter zu prüfen als ein gemeinsam verwendetes, dauerhaft bestehendes Arbeitsverzeichnis.

Ein „verschlüsseltes Image“ ist während der Ausführung eines Auftrags nicht automatisch vor Zugriffen geschützt. Sobald es eingebunden ist, können Prozesse mit den entsprechenden Dateiberechtigungen seinen Inhalt lesen. Deshalb müssen auch der Runner-Benutzer, die Protokollierung und die Art der Geheimnisübergabe kontrolliert werden.

VerfahrenRückstände nach unerwartetem AbbruchAuftragsgrenzeGeeignet für
Gemeinsames Verzeichnis mit anschließender LöschungGeöffnete Dateien und versteckte Verzeichnisse werden leicht übersehenHängt von der Korrektheit des Skripts abKurze Aufträge ohne sensible Daten
Eigenes normales Verzeichnis pro AuftragDas Verzeichnis verbleibt auf dem Host-VolumeGetrennte Pfade, aber kein getrenntes DateisystemParallele Builds mit geringem Risiko
Verschlüsseltes Sparse ImageVerbliebene Einhängungen und Image-Dateien sind eindeutig erkennbarEigenständiges DateisystemCI-Systeme mit klar definierten Bereinigungsgrenzen

Einen nach Auftrag benannten APFS-Arbeitsbereich erstellen

Die Auftragsnummer muss zunächst auf einen zulässigen Zeichensatz beschränkt werden, damit keine Schrägstriche, Leerzeichen oder Befehlssubstitutionen in einen Pfad gelangen. Das Image-Verzeichnis sollte sich an einem Ort befinden, auf den ausschließlich der Runner-Benutzer lesend und schreibend zugreifen kann. Für jeden Auftrag wird außerdem ein eigener Einhängepunkt angelegt.

set -euo pipefail
set +x
umask 077

JOB_KEY="$(printf '%s' "${CI_JOB_ID:?}" | tr -cd 'A-Za-z0-9._-')"
IMAGE_ROOT="$HOME/ci-images"
IMAGE_PATH="$IMAGE_ROOT/$JOB_KEY.sparsebundle"
MOUNT_PATH="/Volumes/ci-$JOB_KEY"

mkdir -p "$IMAGE_ROOT" "$MOUNT_PATH"
chmod 700 "$IMAGE_ROOT"

printf '%s' "${CI_VOLUME_PASSWORD:?}" |
  hdiutil create \
    -size 80g \
    -type SPARSEBUNDLE \
    -fs APFS \
    -volname "ci-$JOB_KEY" \
    -encryption AES-256 \
    -stdinpass \
    "$IMAGE_PATH"

80g ist die logische Obergrenze und bedeutet nicht, dass sofort 80 GB belegt werden. Die Grenze muss Quellcode, Abhängigkeiten, abgeleitete Daten und Lastspitzen bei der Archivierung abdecken und zusätzlich Platz für Fehlerprotokolle lassen. Das Passwort darf weder in Befehlsargumenten noch in Dateinamen oder Build-Protokollen stehen. Deaktivieren Sie zuerst die Befehlsausgabe und übergeben Sie das Passwort anschließend über die Standardeingabe aus einer geschützten CI-Variablen.

Einhängung sofort überprüfen

Ein erfolgreich erstelltes Image bedeutet noch nicht, dass der Einhängepunkt korrekt ist. Das Skript sollte prüfen, ob der Zielpfad tatsächlich ein eingebundenes Volume ist, und den Dateisystemtyp bestätigen.

printf '%s' "$CI_VOLUME_PASSWORD" |
  hdiutil attach \
    -stdinpass \
    -nobrowse \
    -mountpoint "$MOUNT_PATH" \
    "$IMAGE_PATH"

mount | grep -F "on $MOUNT_PATH "
diskutil info "$MOUNT_PATH" | grep -E 'File System Personality|Volume Name'
mkdir -p "$MOUNT_PATH/src" "$MOUNT_PATH/output" "$MOUNT_PATH/tmp"

Danach werden das Checkout-Verzeichnis, die Build-Ausgaben und das auftragsspezifische temporäre Verzeichnis gemeinsam auf dieses Volume gelegt. Gemeinsam genutzte, schreibgeschützte Caches von Paketmanagern können außerhalb des Volumes verbleiben. Jeder Cache, den ein Auftrag verändern könnte, sollte dagegen auf das Volume kopiert werden, damit parallele Schreibzugriffe keine Daten verunreinigen.

Aushängen als Bestandteil des Lebenszyklus behandeln

Die Bereinigung darf nicht nur in der letzten Skriptzeile stehen, da Kompilierungsfehler, Zeitüberschreitungen und Abbruchsignale den Auftrag vorzeitig beenden können. Ein gemeinsamer Exit-Hook sollte Synchronisierung, Belegungsprüfung und Aushängen ausführen und bei einem Fehler genügend Diagnoseinformationen hinterlassen.

cleanup_workspace() {
  set +e
  sync
  if mount | grep -Fq "on $MOUNT_PATH "; then
    lsof +D "$MOUNT_PATH" > "$IMAGE_ROOT/$JOB_KEY.lsof.txt" 2>/dev/null
    hdiutil detach "$MOUNT_PATH"
  fi
  rmdir "$MOUNT_PATH" 2>/dev/null
}

trap cleanup_workspace EXIT INT TERM
export TMPDIR="$MOUNT_PATH/tmp"
cd "$MOUNT_PATH/src"

lsof +D kann bei großen Verzeichnissen langsam sein. Daher können zunächst bekannte Build-, Test- und Paketierungsprozesse geprüft werden; eine vollständige Suche ist dann erst erforderlich, wenn das Aushängen fehlschlägt. Verwenden Sie nicht sofort ein erzwungenes Aushängen. Dadurch könnten weiterhin schreibende Prozesse unbemerkt bleiben und unvollständige Artefakte hinterlassen.

Parallelität, Kapazität und Rückstände nach Fehlern handhaben

Bei einer Wiederholung desselben Auftrags kann das vorherige Image noch vorhanden sein. Es direkt zu überschreiben wäre unsicher. Zuerst muss geprüft werden, ob es noch eingebunden ist: Ist es eingebunden, wird der neue Auftrag blockiert und der Konflikt protokolliert. Ist es nicht eingebunden, wird es gemäß der Aufbewahrungsrichtlinie archiviert oder gelöscht. Die Auftragsnummer sollte außerdem die Laufnummer der aktuellen Ausführung enthalten, damit zwei Läufe nicht auf dasselbe Image verweisen.

Drei Prüfungen einrichten

Erstens sollte vor Beginn des Auftrags die Ausgabe von hdiutil info geprüft werden, um auszuschließen, dass bereits ein gleichnamiges Volume vorhanden ist. Zweitens muss während des Builds der freie Speicherplatz sowohl auf dem Host-Volume als auch auf dem eingebundenen Volume überwacht werden. Ein Sparse Image wächst mit seinen Daten; freier Platz im logischen Volume bedeutet daher nicht automatisch, dass auch auf dem Host-Volume genügend Platz verfügbar ist. Drittens ist das Image-Verzeichnis nach Abschluss des Auftrags zu durchsuchen. Zurückbleiben dürfen nur fehlgeschlagene Beispiel-Images, die ausdrücklich zu Diagnosezwecken markiert wurden.

Mit den folgenden Befehlen lassen sich die beiden Kapazitätsebenen unterscheiden:

df -h "$MOUNT_PATH"
df -h "$IMAGE_ROOT"
du -sh "$IMAGE_PATH"
hdiutil info

Wenn eine bestimmte Auftragsart regelmäßig ihre Obergrenze erreicht, sollten zunächst Zwischenartefakte entfernt oder ausgelagert werden, die nicht langfristig aufbewahrt werden müssen. Erst danach ist die logische Kapazität anzupassen. Das Image blind zu vergrößern verschiebt lediglich den Zeitpunkt, zu dem der Host-Datenträger voll ist.

Sicherheitsgrenzen und Checkliste für den Betrieb

Das Geheimnis sollte nur während der Erstellung und Einhängung in den Prozess gelangen und anschließend im aktuellen Shell-Prozess sofort wieder aus dem Export entfernt werden. Build-Skripte dürfen keine Umgebungsvariablen ausgeben und das Passwort nicht auf das Volume kopieren. Die Berechtigungen der Image-Datei müssen auf 600 oder restriktiver gesetzt sein, die des Image-Stammverzeichnisses auf 700.

Vor der Inbetriebnahme sind folgende Punkte einzeln zu prüfen:

  • Der Runner verwendet ein eigenes Konto ohne Administratorrechte;
  • die Auftragsnummer wird durch eine Positivliste gefiltert, sodass keine Pfadtraversierung möglich ist;
  • Erstellung, Einhängung, Build und Aushängung können unabhängig voneinander fehlschlagen und liefern jeweils einen eindeutigen Status;
  • EXIT, INT und TERM rufen dieselbe Bereinigungsfunktion auf;
  • schlägt das Aushängen fehl, werden zuerst die zugreifenden Prozesse protokolliert, anstatt sofort ein erzwungenes Aushängen auszuführen;
  • die Kapazitäten des APFS-Volumes, der Sparse-Image-Datei und des Host-Volumes werden gleichzeitig überwacht;
  • für fehlgeschlagene Beispiel-Images gilt eine Aufbewahrungsfrist, nach deren Ablauf das gesamte Image gelöscht wird;
  • Protokolle enthalten keine Passwörter, privaten Schlüssel, vollständigen Zugangsdaten oder sensiblen Quellcodeausschnitte.

Testen Sie das Verfahren zunächst mit einem kleinen Projekt ohne sensible Daten. Dabei sollten vier Abläufe geprobt werden: erfolgreicher Abschluss, Kompilierungsfehler, manueller Abbruch und ein fast vollständig belegter Datenträger. Erst wenn das Image in allen vier Fällen gefunden, sein Zustand nachvollziehbar erklärt und der Speicherplatz vollständig zurückgewonnen werden kann, ist diese Isolation zuverlässig betreibbar.

Häufig gestellte Fragen

Ersetzt ein verschlüsseltes Sparse Image die Trennung durch Systemrechte?

Nein. Es schützt vor allem ruhende Arbeitsdaten. Zusätzlich sind ein eigener Runner-Benutzer, minimale Rechte, kontrollierte Geheimnisübergabe und zuverlässiges Aushängen erforderlich.

Was ist bei einem nach einem Fehler verbliebenen Mount zu tun?

Ermitteln Sie mit lsof die Prozesse am Mountpunkt, beenden Sie nur die Prozesse des betroffenen Auftrags und führen Sie sync aus. Erzwingen Sie das Aushängen erst nach dem Ende aller Schreibvorgänge.

Dedizierter Cloud-Mac

Wählen Sie für Entwicklung, Builds und Remote-Desktop einen dedizierten physischen Rechner.

Vergleichen Sie drei Apple-Silicon-Konfigurationen und wählen Sie beim Bestellen Knoten, Mietdauer und zusätzliche Speicheroptionen aus.

Mietoption auswählen