SSD Nodes Learn Hosting plans →
Anleitungen Matt ConnorVon Matt Connor · Aktualisiert 2026-09-02

systemd-Dienst mit ProtectSystem absichern und debuggen

ProtectSystem, PrivateTmp, DynamicUser und NoNewPrivileges erklärt: Erfahren Sie, was jede Direktive blockiert, bricht und wie Sie Startfehler debuggen.

Was systemd-Sandboxing bewirkt

systemd-Sandboxing ist die zweite Hälfte beim Erstellen einer Unit-Datei. Type= legt fest, wie ein Dienst startet, und Direktiven wie ProtectSystem= und PrivateTmp= bestimmen, worauf er nach dem Start zugreifen kann. Dabei handelt es sich um Kernel-Funktionen, Mount-Namespaces und seccomp-Filter, die systemd anwendet, bevor Ihr Prozess die Kontrolle erhält. Die Anwendung nimmt diese Einschränkungen nicht wahr und benötigt keine Codeänderungen.

Standardmäßig ist überhaupt kein Sandboxing aktiviert. Eine Unit ohne Sandboxing läuft als root, schreibt überall, liest jede Datei auf dem System und kann ein Kernel-Modul laden. Wenn es sich bei dieser Unit um eine aus dem Internet erreichbare Webanwendung handelt, kann eine Sicherheitslücke beim Datei-Upload dazu führen, dass der gesamte Server kompromittiert wird. Mit der folgenden Unit erhält dieselbe Anwendung ein schreibgeschütztes Dateisystem, ein leeres /home, ein /tmp, das kein anderer Prozess sehen kann, und keinen Weg zu root, selbst wenn sie eine setuid-Binärdatei findet.

All dies gilt jeweils nur für eine Unit. Die Absicherung eines Dienstes wirkt sich nicht auf benachbarte Dienste aus. Beginnen Sie daher mit dem Dienst, der an einem öffentlichen Port lauscht.

Die Unit-Datei vollständig

notes ist ein kleiner Webdienst. Er lauscht auf localhost, speichert eine SQLite-Datenbank unter /var/lib/notes und läuft hinter nginx. Type=exec ist geeignet, weil die Binärdatei im Vordergrund bleibt. Der Unterschied zwischen Type=simple, exec, forking und notify bestimmt, wie systemd den Startvorgang überwacht. Jede nachfolgende Direktive wird weiter unten erläutert, einschließlich dessen, was sie verhindert und welche Probleme sie häufig verursacht.

[Unit]
Description=Notes web service
After=network-online.target
Wants=network-online.target

[Service]
Type=exec
ExecStart=/opt/notes/bin/notes --listen 127.0.0.1:8080
DynamicUser=yes
StateDirectory=notes
Environment=NOTES_DB=/var/lib/notes/notes.db

ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectProc=invisible

NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
RestrictSUIDSGID=yes

ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes

RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=yes
LockPersonality=yes

[Install]
WantedBy=multi-user.target

Schreiben Sie die Datei nach /etc/systemd/system/notes.service, führen Sie sudo systemctl daemon-reload aus und anschließend sudo systemctl restart notes.service. Bearbeiten Sie bei einer Unit, die aus einem Paket stammt, nicht die Herstellerdatei. sudo systemctl edit notes.service öffnet ein Drop-in unter /etc/systemd/system/notes.service.d/override.conf. Es enthält nur Ihre [Service]-Ergänzungen und bleibt bei Paketaktualisierungen erhalten. systemctl cat notes.service gibt das zusammengeführte Ergebnis aus, zuerst die Herstellerdatei.

Unter welchem Benutzer der Dienst läuft

Ohne User= läuft ein Dienst als root, und jede andere Anweisung in diesem Abschnitt dient nur der Schadensbegrenzung. Es gibt zwei Möglichkeiten, das zu vermeiden.

Ein statischer Systembenutzer. Legen Sie ihn mit sudo useradd --system --no-create-home --shell /usr/sbin/nologin notes an und setzen Sie anschließend User=notes in der Unit. Die UID bleibt über Neustarts und Reboots hinweg gleich. Das ist wichtig, wenn der Dienst Dateien außerhalb seines eigenen Zustandsverzeichnisses besitzt oder ein Backup-Job diese Dateien lesen muss. Die Argumentation hinter einem eigenen unprivilegierten Konto für jeden Dienst gilt hier unverändert.

DynamicUser=yes. systemd weist beim Start des Dienstes eine UID aus einem reservierten Bereich zu und gibt sie beim Stoppen des Dienstes wieder frei. Auf der Festplatte existiert kein Konto. Daher bleibt beim Entfernen des Dienstes nichts zurück. Während der Dienst läuft, löst getent passwd notes den Namen über das NSS-Modul (Name Service Switch) von systemd auf. Nach dem Stoppen ist der Name nicht mehr vorhanden.

DynamicUser=yes aktiviert außerdem vier weitere Einstellungen für Sie: RemoveIPC=yes, PrivateTmp=yes, ProtectSystem=strict und ProtectHome=read-only. Das entspricht mit einer einzigen Zeile bereits weitgehend einer Sandbox. Deshalb bleibt die Beispiel-Unit kurz.

Was dadurch nicht mehr funktioniert. Eine dynamische UID kann keine Dateien an beliebigen Orten besitzen, weil die Nummer zwischen Diensten wiederverwendet wird. Persistente Daten müssen über StateDirectory=, CacheDirectory= oder LogsDirectory= eingebunden werden. systemd erstellt diese Verzeichnisse bei jedem Start und ändert ihren Eigentümer mit chown auf die aktuelle UID. Mit DynamicUser=yes ist das tatsächliche Verzeichnis /var/lib/private/notes, und /var/lib/notes ist ein symbolischer Link darauf. /var/lib/private hat die Berechtigung 0700 und gehört root. Ein Backup, das als normaler Benutzer läuft, erhält daher auf einem Pfad, der für root vollständig lesbar aussieht, Permission denied. Alles, was einen festen Eigentümer benötigt, etwa ein SSH-Schlüssel, ein NFS-Export oder eine Datei, die ein zweiter Dienst liest, erfordert stattdessen einen statischen Benutzer.

So sieht das Dateisystem aus: ProtectSystem, ProtectHome, PrivateTmp

ProtectSystem= akzeptiert drei Werte. yes bindet /usr und die Boot-Verzeichnisse schreibgeschützt ein. full fügt /etc hinzu. strict bindet die gesamte Hierarchie schreibgeschützt ein, abgesehen von den Kernel-API-Verzeichnissen /dev, /proc und /sys, die von anderen Direktiven abgedeckt werden. Beginnen Sie mit strict und öffnen Sie gezielt Ausnahmen. Mit einer lockeren Konfiguration zu beginnen und sie später zu verschärfen, geschieht in der Praxis nie.

Ausnahmen werden mit ReadWritePaths=/srv/notes/uploads definiert. Sie benötigen weniger, als Sie erwarten: StateDirectory=, LogsDirectory=, CacheDirectory= und RuntimeDirectory= bleiben unter ProtectSystem=strict automatisch schreibbar. Deshalb enthält die Beispiel-Unit überhaupt keine Zeile ReadWritePaths=. Unter strict ist auch /tmp schreibgeschützt, sofern PrivateTmp=yes der Unit nicht ein eigenes schreibbares Verzeichnis übergibt.

Was dadurch nicht mehr funktioniert. Jeder Schreibzugriff außerhalb dieser Pfade schlägt nun mit Read-only file system fehl. Anwendungen, die ihre eigene Konfiguration zurück nach /etc schreiben, eine PID-Datei direkt in /run ablegen oder ein Plugin nach /opt entpacken, stoßen darauf. Übernehmen Sie den Pfad aus der Fehlermeldung und fügen Sie genau diesen Pfad hinzu, nicht dessen übergeordnetes Verzeichnis. Ein in ReadWritePaths= aufgeführter Pfad, der nicht existiert, führt zu einem Startfehler und nicht zu einer Warnung. Präfixieren Sie optionale Pfade daher mit einem Bindestrich: ReadWritePaths=-/srv/notes/uploads.

Schreibgeschützt bedeutet nicht verborgen. Unter ProtectSystem=strict kann der Dienst /etc/passwd weiterhin lesen, ebenso jedes weltweit lesbare Geheimnis einer anderen Anwendung. InaccessiblePaths=/etc/ssh /srv/otherapp entfernt einen Teilbaum vollständig aus der Sicht der Unit. Für die eigenen Geheimnisse des Dienstes kopiert LoadCredential=dbpass:/etc/notes/dbpass die Datei in ein Unit-spezifisches Verzeichnis, das nur dieser Dienst lesen kann. Die Anwendung findet sie unter $CREDENTIALS_DIRECTORY.

ProtectHome=yes lässt /home, /root und /run/user leer erscheinen. Ein Webdienst benötigt keinen Zugriff auf ein Home-Verzeichnis. Dadurch kann eine Schwachstelle für Directory Traversal nicht bis /root/.ssh vordringen. read-only und tmpfs sind die weniger strengen Werte. Dadurch funktionieren Anwendungen nicht mehr, deren Daten tatsächlich in einem Home-Verzeichnis liegen. Das betrifft viele manuell installierte Anwendungen unter /home/app. Verschieben Sie die Daten nach /var/lib, oder setzen Sie ProtectHome=read-only und akzeptieren Sie den geringeren Schutz.

PrivateTmp=yes gibt dem Dienst ein eigenes /tmp und /var/tmp. Diese werden beim Start angelegt und beim Stoppen gelöscht. Damit entfällt die gesamte Klasse von Race Conditions bei temporären Dateien zwischen Diensten. Nach einem Absturz können Geheimnisse außerdem nicht mehr in einem Verzeichnis zurückbleiben, dessen Inhalt jeder Benutzer auf dem System auflisten kann.

Was dadurch nicht mehr funktioniert. Alles, was /tmp als gemeinsamen Treffpunkt verwendet. Ein Dienst, der so konfiguriert ist, dass er MySQL über /tmp/mysql.sock erreicht, meldet nun Can't connect to local MySQL server through socket '/tmp/mysql.sock'. Die Datenbank hat ihren Socket im /tmp des Hosts angelegt, während der Dienst in seinem eigenen Verzeichnis sucht. Verweisen Sie den Dienst auf 127.0.0.1 oder auf den tatsächlichen Socket-Pfad unter /run. Dasselbe gilt beim Debugging: Dateien, die der Dienst nach /tmp schreibt, erscheinen nicht in /tmp Ihrer Shell. Um den Inhalt zu prüfen, wechseln Sie in den Mount-Namespace des Dienstes.

pid=$(systemctl show --property=MainPID --value notes.service)
sudo nsenter --target "$pid" --mount ls -l /tmp

PrivateDevices=yes ersetzt /dev durch eine kleine Auswahl virtueller Geräte wie /dev/null, /dev/zero und /dev/urandom. Physische Geräte sind dort nicht vorhanden. Gerätedateien für Datenträger, /dev/kvm, /dev/net/tun, Soundkarten und GPUs verschwinden vollständig. Ein Medienserver mit Hardware-Transcodierung kann /dev/dri/renderD128 daher nicht öffnen und wechselt entweder auf Software-Transcodierung oder beendet sich. Wenn ein Dienst tatsächlich eine Gerätedatei benötigt, deaktivieren Sie PrivateDevices= für diese Unit und geben Sie die Gerätedatei mit DeviceAllow=/dev/dri/renderD128 rw frei. Das ist weiterhin deutlich restriktiver als die Standardeinstellung, bei der jedes Gerät des Systems verfügbar ist.

Was der Dienst werden kann: NoNewPrivileges und Capabilities

NoNewPrivileges=yes setzt ein Prozess-Flag, das der Kernel niemals löscht. Ab diesem Zeitpunkt können der Prozess und alle von ihm gestarteten Kindprozesse keine Privilegien mehr über eine setuid-Binärdatei oder eine Dateicapability erlangen. Dies ist die wichtigste einzelne Zeile in der Unit, weil dadurch die meisten lokalen Privilege-Escalation-Ketten bereits beim ersten Schritt beendet werden.

Was dadurch nicht mehr funktioniert. Alles, was innerhalb des Dienstes sudo aufruft und nun sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the 'nosuid' option set or an NFS file system without root privileges? ausgibt. PAM-Passwortprüfungen (Pluggable Authentication Modules), die unix_chkpwd als externen Prozess aufrufen, schlagen aus demselben Grund fehl. Das gilt auch für Rootless-Container-Tools, die newuidmap benötigen. Wenn Ihr Dienst von einem dieser Aufrufe abhängt, entfernen Sie die Abhängigkeit, statt die Direktive zu entfernen.

CapabilityBoundingSet= beschränkt, welche Capabilities ein Prozess in der Unit überhaupt besitzen darf. Eine leere Zuweisung entfernt alle Capabilities. Bei einem Dienst, der bereits unter einem Benutzer ohne root-Rechte läuft, ist dies die zweite Sperre und nicht die erste, weil NoNewPrivileges=yes das übliche Erwerben einer Capability verhindert. Behalten Sie beide Einstellungen bei. Sie greifen auf unterschiedliche Weise, und der Fehler soll zweimal abgefangen werden.

Die eine Capability, die ein Webdienst häufig benötigt, ist CAP_NET_BIND_SERVICE für einen Port unter 1024. Ein Prozess ohne root-Rechte muss diese Capability zugewiesen bekommen. Es reicht nicht, sie nur zu erlauben. Daher sind beide Zeilen erforderlich.

CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

systemd wendet Ambient Capabilities an, bevor es die Privilegien abgibt. Daher funktioniert dies zusammen mit NoNewPrivileges=yes. Ohne diese Zeilen startet der Dienst und wird anschließend mit einer Fehlermeldung beendet, die den Port nennt, beispielsweise listen tcp :443: bind: permission denied. Bei den meisten VPS-Setups ist es besser, an 127.0.0.1:8080 zu binden und nginx oder Caddy Port 443 übernehmen zu lassen. Dadurch bleibt die Capability vollständig aus der Unit heraus. Ein führendes ~ kehrt die Liste um. CapabilityBoundingSet=~CAP_SYS_ADMIN blockiert dann diese eine Capability und erlaubt alle übrigen. Bevorzugen Sie die Allow-Liste. Eine Deny-Liste altert schlecht, weil ständig neue Capabilities hinzukommen.

Was der Kernel bereitstellt

ProtectKernelTunables=yes setzt /proc/sys, /sys und die beschreibbaren Dateien darunter für diese Unit auf schreibgeschützt. Dadurch schlagen Dienste fehl, die beim Start einen sysctl-Wert setzen: Das Startskript gibt sysctl: setting key "vm.max_map_count": Read-only file system aus und beendet sich. Tragen Sie den Wert stattdessen in /etc/sysctl.d/ ein. Dort gehört er hin und bleibt auch nach einem Reboot erhalten. Lassen Sie die Direktive anschließend aktiviert.

ProtectKernelModules=yes verhindert das Laden von Kernelmodulen. Eine Unit, die modprobe ausführt, erhält modprobe: ERROR: could not insert 'nf_conntrack': Operation not permitted. Laden Sie das Modul beim Booten über /etc/modules-load.d/ und nicht aus dem Dienst heraus.

ProtectKernelLogs=yes entzieht dem Dienst dmesg. ProtectControlGroups=yes setzt /sys/fs/cgroup auf schreibgeschützt. Das erkennen Containerlaufzeiten und alle Komponenten, die ihre eigenen cgroups verwalten, sofort. ProtectProc=invisible blendet in /proc die Prozesse anderer Benutzer aus. Dadurch kann ein kompromittierter Dienst weder die Kommandozeile eines anderen Daemons noch das darauf übergebene Passwort lesen. Monitoring-Agenten, die /proc durchsuchen, benötigen diese Option deaktiviert.

RestrictNamespaces=yes verhindert, dass der Dienst neue Namespaces erstellt. Genau das benötigt eine Containerlaufzeit, und genau das nutzt ein Angreifer zum Aufbau eines Escape. LockPersonality=yes verhindert Änderungen an der Kernel-Ausführungsdomäne, und RestrictSUIDSGID=yes verhindert, dass der Dienst setuid-Dateien erstellt. Beide Optionen sind kostengünstig und führen bei gewöhnlichen Anwendungen nur selten zu Problemen.

Was der Dienst erreichen kann

RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 erlaubt lokale Sockets sowie IPv4 und IPv6. Für alle anderen Verbindungen schlägt socket() mit EAFNOSUPPORT fehl. Dabei handelt es sich um einen seccomp-Filter. Er funktioniert daher auf x86-64 und arm64 und deckt damit alle aktuellen VPS ab.

Was dadurch beeinträchtigt wird. AF_NETLINK tritt wesentlich häufiger auf, als viele erwarten. getifaddrs() von glibc verwendet einen Netlink-Socket. Das gilt auch für die Pfade zur Schnittstellenauflistung in den Go-, Java- und .NET-Laufzeitumgebungen. Ein Dienst, der lediglich seine eigene IP-Adresse ermitteln wollte, beendet sich dann mit OSError: [Errno 97] Address family not supported by protocol oder dem entsprechenden Fehler der jeweiligen Sprache. In diesem Fall wird die Liste zu AF_UNIX AF_INET AF_INET6 AF_NETLINK. Sie ist weiterhin deutlich eingeschränkter als die Standardeinstellung. AF_PACKET wird für die Erfassung roher Netzwerkpakete benötigt und sollte für fast nichts anderes aktiviert sein.

IPAddressDeny=any mit IPAddressAllow=localhost verwendet einen anderen Mechanismus: einen BPF-Filter, der an die cgroup der Unit angehängt wird. Der Filter gilt pro Unit und ist für nft list ruleset nicht sichtbar. Dadurch eignet er sich für einen Dienst, der ausschließlich eine Datenbank auf demselben Host erreichen soll. Für die Person, die das System anschließend debuggt, kann das jedoch verwirrend sein. Auf einem Kernel ohne Unterstützung für cgroup-BPF protokolliert systemd, dass die Unit eine IP-Firewall konfiguriert, das lokale System jedoch kein BPF-/cgroup-Firewalling unterstützt. Die Regeln bleiben dann wirkungslos. Prüfen Sie deshalb das Journal, statt vom Gegenteil auszugehen.

Warum startet eine gehärtete Unit nicht mehr?

Jede der oben genannten Direktiven kann einen funktionierenden Dienst in einen fehlschlagenden Dienst verwandeln. Der Fehler sieht oft völlig anders aus als die Direktive, die ihn verursacht hat. Das Vorgehen ist immer gleich: Lesen Sie das Journal, lockern Sie genau eine Direktive und testen Sie erneut.

sudo systemctl restart notes.service
systemctl status notes.service
sudo journalctl -u notes.service -n 50 --no-pager

Die Zeilen, die die Sandbox definieren, sehen so aus:

notes.service: Failed to set up mount namespacing: No such file or directory
notes.service: Failed at step NAMESPACE spawning /opt/notes/bin/notes: No such file or directory
notes.service: Main process exited, code=exited, status=226/NAMESPACE

226/NAMESPACE bedeutet, dass systemd die Dateisystemansicht nicht erstellen konnte. Ihre Binärdatei wurde daher überhaupt nicht ausgeführt. Die häufigste Ursache ist ein Pfad in ReadWritePaths=, BindPaths= oder InaccessiblePaths=, der nicht existiert. 228/SECCOMP weist darauf hin, dass SystemCallFilter= oder SystemCallArchitectures= nicht angewendet werden konnte. code=killed, status=31/SYS ist wiederum ein anderer Fall: Der Prozess wurde gestartet, führte dann einen Systemaufruf aus, den der Filter verweigerte, und der Kernel beendete ihn. Lassen Sie die Referenz zu systemd-Exit-Codes für Units, die nicht starten geöffnet, während Sie dies untersuchen. Die Zahl zeigt am schnellsten, ob es sich um ein Namespace-Problem oder ein Anwendungsproblem handelt.

Lockern Sie in einem Drop-in immer nur eine Direktive, damit das Ergebnis aussagekräftig bleibt. Führen Sie sudo systemctl edit notes.service aus und fügen Sie eine einzelne Zeile ein:

[Service]
ProtectSystem=full

Starten Sie den Dienst neu. Wenn er jetzt startet, wissen Sie, welche Direktive Sie gezielt einschränken müssen, anstatt sie zu löschen. Setzen Sie sie wieder auf strict, fügen Sie die Zeile ReadWritePaths= für den einen Pfad hinzu, den die Anwendung tatsächlich benötigt, und starten Sie den Dienst erneut. Den gesamten Block zu löschen, weil der Dienst nicht starten wollte, führt dazu, dass eine Unit ohne Schutz und mit einem Kommentar zurückbleibt, an den sich niemand mehr erinnert.

Um eine Sandbox ohne einen daran beteiligten Dienst zu testen, starten Sie eine Shell darin:

sudo systemd-run --pty -p ProtectSystem=strict -p ProtectHome=yes -p PrivateTmp=yes /bin/bash

In dieser Shell liefert touch /etc/test den Wert Read-only file system, und ls /home zeigt nichts an. So finden Sie am schnellsten heraus, was eine Anwendung sehen wird. Sie können ihre Befehle manuell ausführen und beobachten, welcher davon fehlschlägt.

Ein Fehlerfall erzeugt überhaupt keine Fehlermeldung. Eine falsch geschriebene Direktive führt nur zu einer Warnung, und der Dienst startet ohne sie:

/etc/systemd/system/notes.service:14: Unknown key name 'ProtectSytem' in section 'Service', ignoring.

Die Unit läuft, die Sandbox fehlt, und es gibt keine weiteren Hinweise. Zwei Befehle erkennen diesen Fehler. sudo systemd-analyze verify /etc/systemd/system/notes.service gibt dieselbe Warnung auf Anforderung aus, und systemctl show zeigt, was der laufende Dienst tatsächlich erhalten hat:

systemctl show notes.service -p User -p ProtectSystem -p PrivateTmp -p NoNewPrivileges

Wenn dieser Befehl ProtectSystem=no zurückgibt, obwohl Sie strict geschrieben haben, hat die Unit nicht das geladen, was Sie erwarten.

systemd-analyze security als Checkliste verwenden

sudo systemd-analyze security notes.service gibt jede Sandbox-Einstellung aus, die eine Unit verwenden könnte, den aktuellen Zustand dieser Unit für jede Einstellung und eine Bewertung pro Zeile. Führen Sie den Befehl ohne Argument aus, um alle geladenen Dienste auf dem System aufzulisten. Ergänzen Sie --offline=true mit einem Pfad, um eine Unit-Datei vor der Installation zu prüfen.

Lesen Sie die Ausgabe als Aufgabenliste. Gehen Sie die markierten Zeilen durch und beantworten Sie für jede eine Frage: Benötigt dieser Dienst diesen Zugriff? In den meisten Fällen lautet die Antwort nein, und Sie fügen die betreffende Zeile hinzu. Manchmal lautet die Antwort ja. Ein Medienserver benötigt einen Geräteknoten. Ein Backup-Agent muss /home lesen können. Diese Zeilen bleiben dauerhaft markiert. Das ist das korrekte Ergebnis und kein Fehler.

Die Zusammenfassungszahl am Ende fasst die darüberstehenden Zeilen zusammen. Sie weiß nicht, was Ihr Dienst tut, welche Daten er verarbeitet oder ob die Anwendung grundsätzlich einen Fehler enthält. Eine Unit kann jede vom Werkzeug geprüfte Zeile erfüllen und trotzdem die schwächste Komponente auf dem Server sein. Denn gemessen wird die Angriffsfläche der Unit-Datei und nicht die Angriffsfläche der Software. Ein selbst gehosteter Passwortmanager macht diese Lücke deutlich: Eine Vaultwarden-Unit kann jede Zeile erfüllen, während ihr Admin-Token und ihre Backup-Datei weiterhin darüber entscheiden, ob der Tresor geschützt ist. Wer nur die Zahl verbessern will, fügt häufig Direktiven ein, deren Bedeutung nicht verstanden wird. Genau diese Direktiven verursachen dann nach einem Paket-Upgrade Probleme, und niemand kann mehr erklären, warum die Zeile vorhanden war.

Wo die Sandbox endet

Diese Direktiven legen fest, worauf ein Dienst zugreifen kann. Sie sagen nichts darüber aus, wie viele Ressourcen er verbrauchen darf. Eine vollständig isolierte Unit kann daher weiterhin jeden CPU-Kern und den gesamten Arbeitsspeicher des Systems belegen. Dafür gibt es einen separaten Satz von Einstellungen, der in der Anleitung zu CPU- und Arbeitsspeicherlimits für einen systemd-Dienst beschrieben wird.

Sie ersetzen außerdem keine Mandatory Access Control. Ein Namespace gilt pro Unit und wird von der Person festgelegt, die die Unit-Datei schreibt, während SELinux im gesamten System eine einheitliche Richtlinie erzwingt. Beide Mechanismen ergänzen sich. Keiner von ihnen macht den anderen überflüssig.

Schließlich gelten sie nur für die Prozesse, die systemd innerhalb dieser Unit startet. Ein Dienst, der Aufgaben über einen Socket an einen Hilfs-Daemon übergibt, hat den Hilfsprozess damit nicht isoliert. Übernehmen Sie denselben Block auch in diese Unit und prüfen Sie das Ergebnis mit systemctl show, statt der Datei zu vertrauen.

FAQ

Was wird durch ProtectSystem=strict tatsächlich schreibgeschützt?

Die gesamte Dateisystemhierarchie mit Ausnahme von /dev, /proc und /sys. Diese werden stattdessen durch PrivateDevices=, ProtectKernelTunables= und ProtectControlGroups= abgedeckt. Dazu gehören auch /etc, /var, /srv, /opt und /tmp. Die Ausnahmen, die systemd automatisch wieder hinzufügt, sind die von systemd verwalteten Verzeichnisse: StateDirectory=, CacheDirectory=, LogsDirectory= und RuntimeDirectory=. In alle anderen Pfade, in die der Dienst schreiben muss, ist ein expliziter ReadWritePaths=-Eintrag erforderlich. Schreibgeschützt bedeutet nicht unlesbar. Eine geheime Datei an einer anderen Stelle auf dem Server bleibt für den Dienst daher zugänglich, sofern Sie sie nicht in InaccessiblePaths= aufführen.

Warum schlägt mein Dienst mit status=226/NAMESPACE fehl?

systemd konnte den Mount-Namespace nicht erstellen. Die ausführbare Datei wurde daher nicht gestartet. Die Journalzeile darüber lautet normalerweise Failed to set up mount namespacing: No such file or directory. In fast allen Fällen existiert ein Pfad in ReadWritePaths=, BindPaths= oder InaccessiblePaths= nicht auf dem Datenträger. Erstellen Sie das Verzeichnis, oder stellen Sie dem Eintrag einen Bindestrich voran (ReadWritePaths=-/srv/notes/uploads), damit systemd den Eintrag bei fehlendem Pfad überspringt. Wenn der Pfad existiert, prüfen Sie die Unit auf einen Tippfehler und bestätigen Sie die Konfiguration mit systemctl cat notes.service. Ein Drop-in kann eine Zeile hinzufügen, die Sie in der Unit nicht sehen.

Kann ein Dienst mit PrivateTmp weiterhin Dateien über /tmp gemeinsam verwenden?

Nein, genau das ist der Zweck dieser Option. Der Dienst erhält ein eigenes /tmp und /var/tmp, die nur für die Laufzeit des Dienstes existieren. Ein Socket oder eine Datei, die ein anderer Prozess im /tmp des Hosts erstellt hat, ist für den Dienst daher nicht sichtbar. Häufig betroffen ist ein Datenbank-Socket unter /tmp/mysql.sock. Verbinden Sie sich stattdessen über 127.0.0.1 oder verweisen Sie den Client auf den tatsächlichen Socket unter /run. Um die temporären Dateien des Dienstes zu untersuchen, ermitteln Sie seine Haupt-PID mit systemctl show --property=MainPID --value und betreten Sie seinen Mount-Namespace mit sudo nsenter --target <pid> --mount.

Wie kann ein Dienst in einer Sandbox ohne root auf Port 443 lauschen?

Gewähren Sie eine einzelne Capability statt des vollständigen root-Kontos. Setzen Sie AmbientCapabilities=CAP_NET_BIND_SERVICE zusammen mit CapabilityBoundingSet=CAP_NET_BIND_SERVICE und behalten Sie User= oder DynamicUser=yes bei. Dann kann der Prozess niedrige Ports binden, aber keine weiteren Aktionen mit dieser Berechtigung ausführen. Nur den Bounding Set zu setzen, ist der übliche Fehler: Die Capability ist dann zwar erlaubt, wird dem Prozess aber nie gewährt. Der Dienst beendet sich mit einem Berechtigungsfehler, der den Port nennt. Wenn auf einem VPS bereits ein Reverse Proxy läuft, ist es sauberer, den Dienst an 127.0.0.1:8080 zu binden und nginx Port 443 überlassen. Die Unit benötigt dann überhaupt keine Capabilities.

Sollte ich DynamicUser verwenden, statt einen Systembenutzer anzulegen?

Verwenden Sie DynamicUser, wenn der Dienst alle seine Daten in StateDirectory=, CacheDirectory= oder LogsDirectory= speichert. Das trifft auf die meisten kleinen Self-Hosting-Webanwendungen zu. Sie erhalten eine UID, die nur während der Laufzeit des Dienstes existiert. Zusätzlich werden PrivateTmp=, ProtectSystem=strict, ProtectHome=read-only und RemoveIPC= automatisch aktiviert. Verwenden Sie einen statischen Systembenutzer, wenn die UID dauerhaft gleich bleiben muss. Das gilt beispielsweise für Dateien außerhalb dieser Verzeichnisse, einen SSH-Schlüssel, einen NFS-Mount oder einen zweiten Prozess, der dieselben Daten liest. Beachten Sie, dass die Daten mit DynamicUser=yes tatsächlich in /var/lib/private/notes liegen. Dieses Verzeichnis hat den Modus 0700 und gehört root. Deshalb schlagen Backup-Jobs ohne root-Rechte fehl.