Heimserver & Self-Hosting

Dienstverzeichnisse mit systemd - Laufzeit-, Zustands- und Cache-Daten richtig ablegen

Dienstverzeichnisse mit systemd - Laufzeit-, Zustands- und Cache-Daten richtig ablegen

Ein kleiner Dienst benötigt Ablageorte für einen Unix-Socket, eine Datenbank, heruntergeladene Metadaten und möglicherweise eigene Protokolldateien. Ein einziges beschreibbares Verzeichnis für alles wirkt bequem, verbirgt aber mehrere unterschiedliche Zusagen. Welche Dateien dürfen beim Stoppen des Dienstes verschwinden? Welche müssen einen Neustart überstehen? Welche lassen sich neu erzeugen? Wer erstellt das Verzeichnis, bevor ein unprivilegierter Prozess startet?

Auf einem Debian-System mit systemd lassen sich diese Fragen häufig direkt in der Unit ausdrücken. Direktiven wie RuntimeDirectory= und StateDirectory= ermöglichen es dem Service Manager, Standardverzeichnisse mit bewusst gewählten Eigentümern und Berechtigungen anzulegen. tmpfiles.d bleibt nützlich, aber für eine andere Grenze: Dateisystemobjekte, deren Lebenszyklus nicht von einem einzelnen Dienst abhängt, oder Regeln, die altersbasierte Bereinigung und komplexere Operationen benötigen.

Das bedeutet nicht, dass jedes mkdir verschwinden muss. Es ist eine Methode, zuerst den Verantwortlichen für den Lebenszyklus eines Verzeichnisses und danach den Mechanismus zu bestimmen.

Daten klassifizieren, bevor der Pfad angelegt wird

Der Verzeichnisname sollte sich nach der Bedeutung der Daten richten und nicht nur danach, welches Programm sie schreibt. Für Systemdienste ordnet systemd.exec(5) fünf Unit-Einstellungen bekannten Bereichen des Dateisystems zu:

ZweckUnit-EinstellungSystempfadUmgebungsvariable
Flüchtige LaufzeitdatenRuntimeDirectory=/run/$RUNTIME_DIRECTORY
Persistenter AnwendungszustandStateDirectory=/var/lib/$STATE_DIRECTORY
Reproduzierbare Cache-DatenCacheDirectory=/var/cache/$CACHE_DIRECTORY
Diensteigene ProtokolleLogsDirectory=/var/log/$LOGS_DIRECTORY
Administrativ verwaltete KonfigurationConfigurationDirectory=/etc/$CONFIGURATION_DIRECTORY

Diese Unterschiede beeinflussen die Wiederherstellung. Ein Socket unter /run ist nur sinnvoll, solange ein Prozess verfügbar ist, der ihn bedient. Eine Warteschlangendatenbank unter /var/lib kann den Datensatz noch unerledigter Arbeit enthalten. Zwischengespeicherte entfernte Metadaten unter /var/cache sollten sich aus einer anderen Quelle rekonstruieren lassen. Alle drei lediglich als „Anwendungsdateien“ zu behandeln, macht Lösch- und Sicherungsentscheidungen unnötig riskant.

Protokolle erfordern eine eigene Entscheidung. Ein Dienst, der nur auf Standardausgabe und Standardfehler schreibt, wird möglicherweise bereits vom Journal erfasst und benötigt kein LogsDirectory=. Ebenso bedeutet ConfigurationDirectory= nicht, dass ein Daemon seine eigene Konfiguration überschreiben sollte. Die Konfiguration unter /etc gehört normalerweise in die Verantwortung der Administration; die Direktive macht in erster Linie den erwarteten Pfad ausdrücklich sichtbar.

Warum /run einen Verantwortlichen für den Lebenszyklus braucht

Abschnitt 9.1.4 der Debian Policy besagt, dass /run beim Booten normalerweise geleert wird, häufig weil es sich um ein temporäres Dateisystem handelt. Software kann daher nicht voraussetzen, dass ein gestern dort angelegtes Verzeichnis nach einem Neustart noch vorhanden ist. Der ältere Pfad /var/run bleibt als Kompatibilitätsumleitung erhalten und ist kein separates persistentes Zuhause.

Ein als root ausgeführtes Setup-Skript kann das Verzeichnis neu anlegen. Dann befinden sich jedoch Reihenfolge, Eigentümerschaft, Fehlerbehandlung und Löschrichtlinie an einer anderen Stelle als der Dienst. Eine Zeile wie ExecStartPre=/usr/bin/mkdir ... ist ebenfalls unpraktisch, wenn der Dienst unprivilegiert läuft: Entweder kann der Befehl den Pfad nicht anlegen, oder er erhält erhöhte Rechte, die der Hauptprozess nicht benötigt.

RuntimeDirectory=example-worker gibt PID 1 eine enger gefasste Anweisung. Vor dem Start des Prozesses legt systemd /run/example-worker an. Standardmäßig gehört das innerste Verzeichnis dem über User= und Group= konfigurierten Konto. Wenn der Dienst stoppt, wird dieses Laufzeitverzeichnis standardmäßig entfernt. Eigentümerschaft, Startreihenfolge und gewöhnliche Bereinigungsregel sind damit an dieselbe Unit gebunden.

Eine kleine Unit mit ausdrücklich definierten Schreibbereichen

Der folgende fiktive Worker verwendet Laufzeit-, Zustands- und Cache-Speicher. /usr/bin/sleep ist absichtlich ein harmloser Platzhalter, damit sich die Unit-Syntax ohne installierte Anwendung prüfen lässt:

[Unit]
Description=Example background worker

[Service]
Type=simple
User=example-worker
Group=example-worker
ExecStart=/usr/bin/sleep infinity
RuntimeDirectory=example-worker
RuntimeDirectoryMode=0750
StateDirectory=example-worker
StateDirectoryMode=0750
CacheDirectory=example-worker
CacheDirectoryMode=0750

Das in diesem Beispiel genannte Konto dient nur der Veranschaulichung. Ein dediziertes Systemkonto sollte über das Betriebssystem oder den Paketverwaltungsprozess angelegt und der Platzhalterbefehl durch das tatsächliche Programm ersetzt werden. Das Beispiel weist systemd an, folgende Pfade vorzubereiten:

  • /run/example-worker für einen Socket oder andere flüchtige Laufzeitobjekte;
  • /var/lib/example-worker für persistenten Zustand, der für die Korrektheit erforderlich ist;
  • /var/cache/example-worker für Daten, die der Worker neu erzeugen kann.

Die zugehörigen Umgebungsvariablen enthalten die absoluten Pfade. Eine entsprechend entwickelte Anwendung kann sie lesen und damit doppelte Pfadannahmen vermeiden. Bestehende Software, die einen festen konventionellen Pfad erwartet, kann diesen weiterhin verwenden; die Umgebungsvariable ist nützlich, aber nicht verpflichtend.

Die Modus-Direktiven sind wichtig, weil der dokumentierte Standardwert 0755 ist. 0750 ist sinnvoll, wenn nur das Dienstkonto und dessen Gruppe das Verzeichnis durchlaufen dürfen, aber kein universeller Sicherheitswert. Ein Webserver, Sicherungsagent oder Überwachungsprozess benötigt möglicherweise Gruppenzugriff. Der Modus sollte anhand der tatsächlichen Leser und Schreiber gewählt werden, nicht durch Kopieren der scheinbar strengsten Zahl.

Stoppen bedeutet nicht, alles zu löschen

Der wichtigste Unterschied zwischen diesen Einstellungen ist ihr Löschverhalten. Das innerste durch RuntimeDirectory= angelegte Verzeichnis wird beim Stoppen der Unit entfernt, sofern RuntimeDirectoryPreserve= diese Richtlinie nicht ändert. Selbst bei aktivierter Aufbewahrung ist das systemweite /run über einen Neustart hinweg normalerweise flüchtig.

Verzeichnisse aus StateDirectory=, CacheDirectory=, LogsDirectory= und ConfigurationDirectory= werden nicht allein deshalb entfernt, weil der Dienst stoppt. Das ist für Zustandsdaten notwendig, bedeutet aber auch: „Dienst deaktivieren“ ist keine Datenlöschung.

Systemd bietet für diese verwalteten Ressourcenklassen eine ausdrückliche Bereinigungsschnittstelle. Beispielsweise lässt sich der Cache einer gestoppten Unit folgendermaßen entfernen:

sudo systemctl clean --what=cache example-worker.service

Die Dokumentation zu systemctl(1) verlangt, dass die Unit für diesen Vorgang gestoppt ist. Die Auswahl von state, logs, configuration oder all hat wesentlich größere Folgen. Ein Befehl namens clean ist nicht automatisch sicher; zuerst sollten die Verzeichnisdeklarationen der Unit und die Sicherungen geprüft werden.

Wo tmpfiles.d weiterhin hingehört

Der Name tmpfiles ist enger als das Werkzeug. Laut tmpfiles.d(5) können seine Regeln Dateien, Verzeichnisse, Pipes und Links anlegen, Eigentümer und Modi anpassen sowie zeitbasierte Löschungen durchführen. Es wird häufig für flüchtige und temporäre Pfade eingesetzt, ist aber ein allgemeiner deklarativer Mechanismus zur Dateisystemverwaltung.

Dasselbe Handbuch nennt eine hilfreiche Trennlinie: Für Verzeichnisse, die einem einzelnen Dienst gehören, sollten RuntimeDirectory= und die verwandten Unit-Einstellungen bevorzugt werden, weil ihr Lebenszyklus direkt an diese Unit gebunden ist. tmpfiles.d eignet sich, wenn das Objekt einen unabhängigen Lebenszyklus oder eine komplexere Regel benötigt.

Eine altersbasierte Cache-Bereinigung ist ein Beispiel. Angenommen, die Unit verwaltet /var/cache/example-worker, während importierte Dateien in einem Unterverzeichnis nach 14 Tagen für die Bereinigung infrage kommen sollen:

# /etc/tmpfiles.d/example-worker.conf
d /var/cache/example-worker/imports 0750 example-worker example-worker 14d -

Das Konto muss vorhanden sein, wenn die Regel angewendet wird. Eine d-Zeile legt das Verzeichnis bei Bedarf an, passt den angegebenen Modus und die Eigentümerschaft an und unterwirft seinen Inhalt einer altersbasierten Bereinigung, wenn ein Alter angegeben ist. „Älter als 14 Tage“ beruht nicht auf einem einzigen einfachen Zeitstempel: Das Handbuch dokumentiert, wie Zugriffs-, Änderungs-, Statusänderungs- und Verzeichniszeitstempel berücksichtigt werden. Wenn Zugriffsmuster oder Sicherungsscans eine Rolle spielen, sollten diese Regeln vor der Wahl eines Alters gelesen werden.

Auch die Operation ist wichtig. systemd-tmpfiles(8) unterscheidet zwischen --create, --clean und --remove. Das Anlegen deklarierter Pfade bedeutet nicht, dass eine altersbasierte Bereinigung ausgeführt wurde, und Bereinigung ist nicht dasselbe wie das Anwenden ausdrücklicher Löschregeln. Statt umfassende Löschbefehle auf alle installierten Regeln anzuwenden, sollte eine einzelne Konfigurationsdatei gezielt getestet werden.

Migrationsrisiken, die eine Pause verdienen

Bestehende Eigentümerschaft kann rekursiv geändert werden

Bei den verwalteten beschreibbaren Verzeichnisklassen außer der Konfiguration prüft systemd die Eigentümerschaft gegen User= und Group=. Die Dokumentation warnt, dass systemd bei abweichender Eigentümerschaft eines vorhandenen innersten Verzeichnisses die Eigentümerschaft darunter rekursiv ändern kann. Das ist für ein einem Dienst gewidmetes Verzeichnis nützlich und für einen gemeinsam genutzten Baum gefährlich. Vor der Umstellung eines alten Pfads sollte dessen Inhalt inventarisiert werden.

DynamicUser= verändert das Schutzmodell

Mit DynamicUser= schützt systemd persistente Zustands-, Cache- und Protokollverzeichnisse vor wiederverwendeten Benutzer-IDs, indem es geschützte hostseitige Ablageorte verwendet und dem Dienst die erwarteten Standardpfade präsentiert. Diese Wechselwirkung spricht für die verwalteten Verzeichniseinstellungen, ist aber kein Grund, DynamicUser=yes blind hinzuzufügen. Datenbankzugriff, Unix-Sockets, ergänzende Gruppen und Dateien außerhalb verwalteter Pfade müssen möglicherweise neu entworfen werden.

Verzeichniserstellung ist keine vollständige Richtlinie

Diese Direktiven entscheiden nicht, was in eine Sicherung gehört, rotieren keine Anwendungsprotokolle, setzen keine Speicherquoten, verschlüsseln keine Daten und machen nicht jeden anderen Dateisystempfad unzugänglich. Sie können mit Sandboxing-Optionen wie ProtectSystem= zusammenarbeiten, aber drei korrekt zugeordnete Verzeichnisse beweisen nicht, dass der Prozess ausschließlich dort schreiben kann.

Auch Versionsunterschiede sind relevant. Die grundlegenden Verzeichniseinstellungen existieren seit Jahren, während neuere optionale Flags und Bereinigungsfunktionen spätere Versionen voraussetzen. Vor der Verwendung einer Direktive aus aktueller Online-Dokumentation sollten das lokale Handbuch und systemctl --version geprüft werden.

Deklaration und Laufzeitergebnis prüfen

Vor der Installation einer Unit kann systemd-analyze verify unbekannte Direktiven, fehlende erforderliche Abhängigkeiten und Probleme mit Programmen melden:

systemd-analyze verify ./example-worker.service

Ein sauberes Prüfergebnis bestätigt nur die Fehlerklassen, die das Werkzeug tatsächlich untersucht. Es beweist nicht, dass der Worker die Pfade versteht, sein Zustand wiederherstellbar ist oder die gewählten Berechtigungen für jeden beteiligten Prozess geeignet sind.

Nach der Installation und einem kontrollierten Start sollten sowohl die normalisierten Eigenschaften von systemd als auch das Dateisystem geprüft werden:

systemctl show example-worker.service \
  --property=RuntimeDirectory,StateDirectory,CacheDirectory

stat -c '%A %U:%G %n' \
  /run/example-worker \
  /var/lib/example-worker \
  /var/cache/example-worker

Danach sollte der Lebenszyklus getestet statt vorausgesetzt werden: den Dienst stoppen und bestätigen, dass Laufzeitartefakte verschwinden, während erforderlicher Zustand erhalten bleibt; ihn erneut starten und prüfen, ob der Laufzeitpfad wieder erscheint. Dieses Experiment sollte nicht mit Produktionszustand durchgeführt werden, bevor Erwartungen an Löschung und Wiederherstellung dokumentiert sind.

Eine zurückhaltende Regel für die Wahl des Mechanismus

Wenn ein Dienst ein konventionelles Verzeichnis benötigt und dessen Lebenszyklus dieser Unit folgt, ist die passende systemd-Verzeichnisdirektive ein guter Ausgangspunkt. So bleiben Pfad, Eigentümerschaft, Modus, Startreihenfolge und gewöhnliches Löschverhalten nahe bei dem Prozess, der davon abhängt. Wenn ein Dateisystemobjekt mehreren Diensten gehört, unabhängig bestehen muss, altersbasierte Bereinigung benötigt oder einen Typ außerhalb dieser Verzeichnisklassen erfordert, sollte eine gezielte tmpfiles.d-Regel geprüft werden.

Das Ziel sind nicht um jeden Preis weniger Zeilen. Es ist eine wahrheitsgetreuere Deklaration: Sockets sind temporär, Zustand bleibt erhalten, Caches sind ersetzbar, Konfiguration gehört zur Administration und Bereinigung erfolgt nur nach einer Richtlinie, die benennt, was verloren gehen darf.

Quellen