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.targetSchreiben 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.
Als welcher Benutzer der Dienst ausgeführt wird
Ohne User= wird ein Dienst als root ausgeführt. Jede weitere Direktive in diesem Abschnitt dient dann nur noch der Schadensbegrenzung. Dafür gibt es zwei Möglichkeiten.
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 bei Neustarts und Reboots gleich. Das ist wichtig, wenn der Dienst Dateien außerhalb seines eigenen Zustandsverzeichnisses besitzt oder ein Backup diese Dateien lesen muss. Die Überlegungen hinter einem eigenen unprivilegierten Konto für jeden Dienst gelten hier unverändert.
DynamicUser=yes. systemd weist dem Dienst beim Start eine UID aus einem reservierten Bereich zu und gibt sie beim Beenden wieder frei. Auf der Festplatte existiert kein Konto. Beim Entfernen des Dienstes bleibt daher nichts zurück. Solange der Dienst läuft, löst getent passwd notes den Namen über das NSS-Modul (name service switch) von systemd auf. Nach dem Beenden ist der Name nicht mehr vorhanden.
DynamicUser=yes aktiviert außerdem mehrere weitere Einstellungen: RemoveIPC=yes, NoNewPrivileges=yes, RestrictSUIDSGID=yes, ProtectSystem=strict, ProtectHome=read-only und ein privates /tmp. Bei aktuellem systemd ist dieses private /tmp ein PrivateTmp=disconnected, also ein neues tmpfs, sofern Sie nicht selbst PrivateTmp=yes setzen, wie es die Beispiel-Unit tut. Damit erhalten Sie mit einer einzigen Zeile den größten Teil einer Sandbox. Deshalb bleibt die Beispiel-Unit trotzdem 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 dorthin. /var/lib/private hat den Modus 0700 und gehört root. Ein Backup, das als normaler Benutzer läuft, erhält daher Permission denied für einen Pfad, der für root vollständig lesbar aussieht. 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 /tmpPrivateDevices=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 aus dem 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 über eine setuid-Binärdatei oder eine File Capability keine Privilegien mehr erlangen. Das ist die wertvollste einzelne Zeile in der Unit, weil dadurch die meisten lokalen Privilege-Escalation-Ketten bereits an ihrem ersten Schritt abbrechen.
Was dadurch nicht mehr funktioniert. Alles, was innerhalb des Dienstes sudo aufruft. Aktuelle sudo-Versionen prüfen das Flag vor allen anderen Aktionen und beenden sich mit The "no new privileges" flag is set, which prevents sudo from running as root.. Der ältere Fehler effective uid is not 0 ... 'nosuid' option, den Sie möglicherweise an anderer Stelle zitiert finden, weist dagegen auf ein Mount- oder Installationsproblem hin und wird nicht von dieser Direktive erzeugt. PAM-Passwortprüfungen (Pluggable Authentication Modules), die unix_chkpwd als externen Prozess aufrufen, schlagen aus demselben Grund fehl. Dasselbe gilt für Rootless-Container-Tools, die newuidmap benötigen. Wenn Ihr Dienst von einem dieser Aufrufe abhängt, entfernen Sie die Abhängigkeit und nicht die Direktive.
CapabilityBoundingSet= begrenzt, welche Capabilities ein Prozess in der Unit jemals besitzen darf. Eine leere Zuweisung entfernt alle Capabilities. Für einen Dienst, der bereits unter einem Nicht-root-Benutzer läuft, ist dies eine zusätzliche Sperre und nicht die erste, weil NoNewPrivileges=yes den üblichen Weg zum Übernehmen einer Capability blockiert. Behalten Sie beide Einstellungen bei. Sie schlagen auf unterschiedliche Weise fehl, sodass der Fehler doppelt erkannt wird.
Die eine Capability, die ein Webdienst häufig benötigt, ist CAP_NET_BIND_SERVICE für einen Port unterhalb von 1024. Ein Nicht-root-Prozess muss diese Capability erhalten. Es reicht nicht, sie lediglich zu erlauben. Daher sind beide Zeilen erforderlich.
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICEsystemd wendet Ambient Capabilities an, bevor es die Privilegien abgibt. Daher funktioniert dies zusammen mit NoNewPrivileges=yes. Ohne diese Zeilen startet der Dienst und beendet sich anschließend mit einem Fehler, der den Port nennt, zum Beispiel listen tcp :443: bind: permission denied. Auf den meisten VPS-Systemen 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 außerhalb der Unit. Ein führendes ~ invertiert die Liste. CapabilityBoundingSet=~CAP_SYS_ADMIN blockiert dann diese eine Capability und erlaubt alle übrigen. Bevorzugen Sie die Allow-Form. Eine Deny-Liste veraltet schnell, 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.
Womit der Dienst kommunizieren kann
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 erlaubt lokale Sockets sowie IPv4 und IPv6 und lässt socket() bei allen anderen Verbindungen mit EAFNOSUPPORT fehlschlagen. 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 nicht mehr funktioniert. AF_NETLINK, deutlich häufiger als erwartet. glibc's getifaddrs() öffnet einen Netlink-Socket und hat keinen Fallback. Go's net.Interfaces() liest denselben Netlink-Dump direkt. .NET ermittelt Schnittstellen über getifaddrs(). Ein Dienst, der lediglich seine eigene IP-Adresse ermitteln möchte, bricht daher mit OSError: [Errno 97] Address family not supported by protocol oder dem entsprechenden Fehler der jeweiligen Sprache ab. Java ist die Ausnahme: OpenJDK's java.net.NetworkInterface verwendet unter Linux ein ioctl für IPv4 und liest /proc/net/if_inet6 für IPv6. Daher funktioniert es weiterhin ohne AF_NETLINK. Wenn der Fehler auftritt, wird die Liste zu AF_UNIX AF_INET AF_INET6 AF_NETLINK. Sie ist damit weiterhin deutlich eingeschränkter als die Standardeinstellung. AF_PACKET wird für die Erfassung roher Pakete benötigt. Fast kein anderer Dienst sollte diese Berechtigung jemals besitzen.
IPAddressDeny=any mit IPAddressAllow=localhost verwendet einen anderen Mechanismus: einen BPF-Filter, der an die cgroup der Unit gebunden ist. Der Filter gilt pro Unit und ist für nft list ruleset unsichtbar. Das eignet sich für einen Dienst, der ausschließlich eine Datenbank auf demselben Host erreichen soll. Für die nächste Person, die das System debuggt, kann diese Konfiguration jedoch verwirrend sein. Auf einem Kernel ohne Unterstützung für cgroup-BPF protokolliert systemd, dass die Unit eine IP-Firewall konfiguriert, während das lokale System kein BPF-/cgroup-Firewalling unterstützt. Die Regeln bleiben dann wirkungslos. Prüfen Sie daher das Journal, anstatt 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-pagerDie 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/NAMESPACE226/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=fullStarten 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/bashIn 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 NoNewPrivilegesWenn 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 macht ProtectSystem=strict tatsächlich schreibgeschützt?
Die gesamte Dateisystemhierarchie mit Ausnahme von /dev, /proc und /sys. Für diese gelten stattdessen PrivateDevices=, ProtectKernelTunables= und ProtectControlGroups=. Dazu gehören auch /etc, /var, /srv, /opt und /tmp. Die Ausnahmen, die systemd für Sie wieder hinzufügt, sind die von systemd verwalteten Verzeichnisse: StateDirectory=, CacheDirectory=, LogsDirectory= und RuntimeDirectory=. In alle anderen Pfade, in die der Dienst schreiben muss, benötigen Sie einen expliziten Eintrag in ReadWritePaths=. Schreibgeschützt bedeutet nicht, dass der Pfad nicht gelesen werden kann. Eine geheime Datei an anderer Stelle auf dem Server bleibt für den Dienst daher lesbar, 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 nie gestartet. Die darüberstehende Journalzeile 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 (ReadWritePaths=-/srv/notes/uploads) voran. Dann überspringt systemd den Eintrag, wenn der Pfad fehlt. Wenn der Pfad existiert, prüfen Sie die Unit auf einen Tippfehler und bestätigen Sie die effektive 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 nutzen?
Nein, genau das ist der Zweck dieser Einstellung. Der Dienst erhält ein neues /tmp und ein neues /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 betrifft das einen Datenbank-Socket unter /tmp/mysql.sock. Die Lösung besteht darin, über 127.0.0.1 zu verbinden oder den Client auf den tatsächlichen Socket unter /run zu verweisen. Um die temporären Dateien des Dienstes zu untersuchen, ermitteln Sie seine Haupt-PID mit systemctl show --property=MainPID --value und wechseln Sie mit sudo nsenter --target <pid> --mount in seinen Mount-Namespace.
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 verwenden Sie User= oder DynamicUser=yes. Dann kann der Prozess an niedrige Ports binden, erhält aber keine weiteren Berechtigungen. Nur die Bounding Set zu setzen, ist ein häufiger Fehler: Die Capability ist dann 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 übernehmen zu lassen. Die Unit benötigt dann überhaupt keine Capabilities.
Sollte ich DynamicUser verwenden, statt einen Systembenutzer anzulegen?
Verwenden Sie diese Option, 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. Außerdem werden PrivateTmp=, ProtectSystem=strict, ProtectHome=read-only, NoNewPrivileges=, RestrictSUIDSGID= und RemoveIPC= automatisch aktiviert. Verwenden Sie einen statischen Systembenutzer, wenn die UID dauerhaft stabil bleiben muss. Das gilt etwa 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-Berechtigungen fehl.